voodoo-framework 1.2.0__tar.gz → 1.3.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.
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/PKG-INFO +1 -1
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/__init__.py +1 -1
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/data/__init__.py +7 -4
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/data/base.py +32 -10
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/data/model.py +5 -7
- voodoo_framework-1.3.0/src/voodoo/storage/__init__.py +15 -0
- voodoo_framework-1.3.0/src/voodoo/storage/database/__init__.py +27 -0
- voodoo_framework-1.3.0/src/voodoo/storage/database/interfaces.py +80 -0
- voodoo_framework-1.3.0/src/voodoo/storage/database/sqlite.py +174 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo_framework.egg-info/PKG-INFO +1 -1
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo_framework.egg-info/SOURCES.txt +5 -1
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/README.md +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/pyproject.toml +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/setup.cfg +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/adapters/__init__.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/adapters/tailwind.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/adapters/voodoo_css.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/agent.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ai/__init__.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ai/agent.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ai/providers/__init__.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ai/providers/anthropic.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ai/providers/gemini.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ai/providers/mock.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ai/providers/ollama.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ai/providers/openai.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ai/tools/__init__.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ai/tools/registry.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/api.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/auth/__init__.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/auth/cookies.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/auth/guards.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/auth/jwt.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/auth/middleware.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/auth/passwords.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/auth/user.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/cli/__init__.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/cli/__main__.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/cli/ai.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/cli/auth.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/cli/dev.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/cli/doctor.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/cli/generate.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/cli/inspect.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/cli/new.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/cli/recover.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/cli/routes.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/cli/scaffolding.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/cli/terminal.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/cli/version.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/components.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/config.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/core/__init__.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/core/app.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/core/errors.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/core/events.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/core/render.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/core/routing.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/core/sitemap.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/core/state.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/i18n.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/mcp/__init__.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/mcp/client.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/mesh/__init__.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/mesh/client.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/primitives/__init__.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/primitives/capability.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/primitives/compute.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/primitives/constraint.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/primitives/effect.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/primitives/intent.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/primitives/resource.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/primitives/state.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/primitives/time.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/py.typed +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/queue.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/routing/api.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/routing/pages.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/runtime/__init__.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/runtime/adaptive.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/runtime/capability.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/runtime/constraint.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/runtime/context.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/runtime/engine.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/runtime/errors.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/runtime/execution.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/runtime/graph.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/runtime/human.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/runtime/persistence.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/runtime/planner.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/runtime/task.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/runtime/workflow.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/security/__init__.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/security/cors.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/security/csrf.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/security/headers.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/security/passwords.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/security/rate_limit.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/seo.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/static/client.js +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/status.py +0 -0
- /voodoo_framework-1.2.0/src/voodoo/storage.py → /voodoo_framework-1.3.0/src/voodoo/storage/manager.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/telemetry/__init__.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/telemetry/api.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/telemetry/middleware.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/telemetry/store.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/theme.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/tools/__init__.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/tools/registry.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ui/__init__.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ui/component.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ui/events.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ui/library.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ui/rendering.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ui/state.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ui/styles/__init__.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ui/styles/theme.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/workers/__init__.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/workers/queue.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo_framework.egg-info/dependency_links.txt +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo_framework.egg-info/entry_points.txt +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo_framework.egg-info/requires.txt +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo_framework.egg-info/top_level.txt +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_agent.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_agent_runtime.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_app.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_auth.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_cli.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_cli_inspect.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_components.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_contract_api.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_data.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_events.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_http_runtime.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_human.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_i18n.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_integration.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_mcp.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_mcp_runtime.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_mesh.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_model.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_persistence.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_planner_adaptive.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_primitives.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_providers.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_queue.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_reactive.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_runtime.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_security.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_seo.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_state.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_theme.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_tools.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_ui.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_websocket.py +0 -0
- {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_workers.py +0 -0
|
@@ -6,10 +6,13 @@ This package re-exports the names that previously lived in the flat
|
|
|
6
6
|
|
|
7
7
|
Storage backend boundary
|
|
8
8
|
-------------------------
|
|
9
|
-
The default backend is SQLite (via :mod:`aiosqlite`)
|
|
10
|
-
``
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
The default backend is SQLite (via :mod:`aiosqlite`) managed by the
|
|
10
|
+
``VoodooDatabase`` adapter in :mod:`voodoo.storage.database` — it owns
|
|
11
|
+
connection lifecycle, WAL pragmas and the ``schema_migrations`` ledger.
|
|
12
|
+
``init_db`` publishes the raw aiosqlite connection as
|
|
13
|
+
``_db_connection`` / ``get_db()``; everything above it (``BaseModel``,
|
|
14
|
+
``Model`` and the hooks) is backend-agnostic. A future PostgreSQL
|
|
15
|
+
adapter replaces the adapter, not this package.
|
|
13
16
|
"""
|
|
14
17
|
|
|
15
18
|
from voodoo.data.base import (
|
|
@@ -1,30 +1,52 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import os
|
|
3
2
|
from collections.abc import Callable
|
|
4
3
|
from typing import Any, get_type_hints
|
|
5
4
|
|
|
6
|
-
import
|
|
5
|
+
from voodoo.storage.database import Migration, SQLiteDatabase, VoodooDatabase
|
|
7
6
|
|
|
8
7
|
_db_connection = None
|
|
9
8
|
_triggers: dict[str, dict[str, list[Callable]]] = {}
|
|
10
9
|
_rls_policies: dict[str, Callable] = {}
|
|
11
10
|
|
|
12
11
|
|
|
12
|
+
async def _ensure_user_tables(db: VoodooDatabase) -> None:
|
|
13
|
+
"""Create tables for every registered user model (idempotent DDL)."""
|
|
14
|
+
for model in _models:
|
|
15
|
+
await model._create_table()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Migration 0001: the user-model baseline. Runs once to enter the ledger,
|
|
19
|
+
# then re-runs its idempotent DDL on every ``init_db`` so models imported
|
|
20
|
+
# after the baseline still get their tables (preserves pre-migration
|
|
21
|
+
# behavior exactly).
|
|
22
|
+
USER_MODEL_BASELINE = Migration(
|
|
23
|
+
version=1,
|
|
24
|
+
name="user_model_baseline",
|
|
25
|
+
fn=_ensure_user_tables,
|
|
26
|
+
rerun=True,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
13
30
|
async def init_db(db_path: str = None):
|
|
31
|
+
"""Open (and migrate) the SQLite database backing the data layer.
|
|
32
|
+
|
|
33
|
+
The raw :class:`aiosqlite.Connection` remains the value of
|
|
34
|
+
``_db_connection`` / ``get_db()`` so all existing callers keep working;
|
|
35
|
+
schema management now flows through the ``SQLiteDatabase`` adapter and
|
|
36
|
+
its migration ledger.
|
|
37
|
+
"""
|
|
14
38
|
if db_path is None:
|
|
15
39
|
from voodoo.config import config
|
|
16
40
|
|
|
17
41
|
db_path = config.db_path
|
|
18
42
|
|
|
19
|
-
db_dir = os.path.dirname(db_path)
|
|
20
|
-
if db_dir:
|
|
21
|
-
os.makedirs(db_dir, exist_ok=True)
|
|
22
|
-
|
|
23
43
|
global _db_connection
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
44
|
+
database = SQLiteDatabase(db_path, migrations=(USER_MODEL_BASELINE,))
|
|
45
|
+
await database.connect()
|
|
46
|
+
_db_connection = database.connection
|
|
47
|
+
# Migrate after the connection is published so rerunnable migration
|
|
48
|
+
# functions can use ``get_db()``.
|
|
49
|
+
await database.migrate()
|
|
28
50
|
|
|
29
51
|
|
|
30
52
|
async def get_db():
|
|
@@ -8,13 +8,11 @@ storage layer and the ``on_insert`` / ``on_update`` hook system from
|
|
|
8
8
|
Storage backend boundary
|
|
9
9
|
-------------------------
|
|
10
10
|
Everything in this module talks to the database through ``get_db()``
|
|
11
|
-
and builds portable ANSI-SQL strings.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
future migration runner can hook into ``init_db`` without changing any
|
|
17
|
-
call-site here.
|
|
11
|
+
and builds portable ANSI-SQL strings. Schema management flows through
|
|
12
|
+
the ``VoodooDatabase`` adapter in :mod:`voodoo.storage.database`
|
|
13
|
+
(SQLite default); the user-model DDL is migration 0001 in its ledger.
|
|
14
|
+
Swapping backends means providing another adapter — no call-site here
|
|
15
|
+
changes.
|
|
18
16
|
"""
|
|
19
17
|
|
|
20
18
|
from typing import Any, get_type_hints
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Voodoo storage package.
|
|
2
|
+
|
|
3
|
+
Two concerns live here:
|
|
4
|
+
|
|
5
|
+
- ``voodoo.storage.manager`` — the legacy ``StorageManager`` facade over
|
|
6
|
+
local-filesystem / S3-compatible uploads (import-compatible re-export of
|
|
7
|
+
the former flat ``voodoo/storage.py`` module).
|
|
8
|
+
- ``voodoo.storage.database`` — the ``VoodooDatabase`` capability adapters
|
|
9
|
+
(SQLite default, PostgreSQL later) that own all durable schema through
|
|
10
|
+
the migration runner.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from voodoo.storage.manager import StorageManager, storage
|
|
14
|
+
|
|
15
|
+
__all__ = ["StorageManager", "storage"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Voodoo database adapters (spec §11).
|
|
2
|
+
|
|
3
|
+
SQLite is the embedded default; PostgreSQL arrives behind the same
|
|
4
|
+
``VoodooDatabase`` protocol as an optional extra. Migrations form one global
|
|
5
|
+
version namespace: 1 = user-model baseline (registered by ``voodoo.data``),
|
|
6
|
+
2+ reserved by the framework, 100+ available to applications.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from voodoo.storage.database.interfaces import (
|
|
10
|
+
DatabaseCapabilities,
|
|
11
|
+
Migration,
|
|
12
|
+
VoodooDatabase,
|
|
13
|
+
)
|
|
14
|
+
from voodoo.storage.database.sqlite import (
|
|
15
|
+
FRAMEWORK_MIGRATIONS,
|
|
16
|
+
LEDGER_TABLE,
|
|
17
|
+
SQLiteDatabase,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"DatabaseCapabilities",
|
|
22
|
+
"FRAMEWORK_MIGRATIONS",
|
|
23
|
+
"LEDGER_TABLE",
|
|
24
|
+
"Migration",
|
|
25
|
+
"SQLiteDatabase",
|
|
26
|
+
"VoodooDatabase",
|
|
27
|
+
]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Voodoo database capability interface.
|
|
2
|
+
|
|
3
|
+
Every durable Voodoo subsystem (state, executions, tasks, events, schedules)
|
|
4
|
+
persists through a ``VoodooDatabase`` implementation. SQLite is the default
|
|
5
|
+
embedded backend (spec §11); PostgreSQL arrives later as an optional adapter
|
|
6
|
+
behind the same protocol.
|
|
7
|
+
|
|
8
|
+
Application code must never import ``aiosqlite``/``psycopg`` directly — the
|
|
9
|
+
adapter boundary lives here (spec §2).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, Sequence
|
|
15
|
+
from contextlib import asynccontextmanager
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Any, Protocol
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class DatabaseCapabilities:
|
|
22
|
+
"""Declarative contract of what a database adapter guarantees (spec §9)."""
|
|
23
|
+
|
|
24
|
+
provider: str
|
|
25
|
+
transactions: bool = True
|
|
26
|
+
migrations: bool = True
|
|
27
|
+
native_json: bool = False
|
|
28
|
+
concurrent_writers: bool = False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class Migration:
|
|
33
|
+
"""One ordered, tracked schema change.
|
|
34
|
+
|
|
35
|
+
Version numbering is a single global namespace shared by the framework
|
|
36
|
+
and the application: version 1 is the user-model baseline, the framework
|
|
37
|
+
reserves 2+, applications may use 100+.
|
|
38
|
+
|
|
39
|
+
``fn`` receives the ``VoodooDatabase`` instance, keeping migrations
|
|
40
|
+
backend-neutral. ``rerun=True`` steps re-execute ``fn`` on every
|
|
41
|
+
``migrate()`` call (idempotent DDL only) so items registered after the
|
|
42
|
+
baseline — e.g. models imported later — still get their tables.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
version: int
|
|
46
|
+
name: str
|
|
47
|
+
statements: tuple[str, ...] = ()
|
|
48
|
+
fn: Callable[[VoodooDatabase], Awaitable[None]] | None = None
|
|
49
|
+
rerun: bool = False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class VoodooDatabase(Protocol):
|
|
53
|
+
"""Backend-neutral database capability.
|
|
54
|
+
|
|
55
|
+
Implementations provide: connection lifecycle, an ordered idempotent
|
|
56
|
+
migration runner backed by a ledger table, a transaction helper, and
|
|
57
|
+
query primitives. ``connect()`` intentionally does not migrate — callers
|
|
58
|
+
control when schema changes apply.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
provider: str
|
|
62
|
+
|
|
63
|
+
async def connect(self) -> None: ...
|
|
64
|
+
|
|
65
|
+
async def close(self) -> None: ...
|
|
66
|
+
|
|
67
|
+
async def migrate(self) -> None: ...
|
|
68
|
+
|
|
69
|
+
def current_version(self) -> int: ...
|
|
70
|
+
|
|
71
|
+
def capabilities(self) -> DatabaseCapabilities: ...
|
|
72
|
+
|
|
73
|
+
@asynccontextmanager
|
|
74
|
+
def transaction(self) -> AsyncIterator[Any]: ...
|
|
75
|
+
|
|
76
|
+
async def execute(self, query: str, params: Sequence[Any] = ()) -> Any: ...
|
|
77
|
+
|
|
78
|
+
async def fetch_all(self, query: str, params: Sequence[Any] = ()) -> list[Any]: ...
|
|
79
|
+
|
|
80
|
+
async def fetch_one(self, query: str, params: Sequence[Any] = ()) -> Any | None: ...
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""SQLite implementation of the Voodoo database capability.
|
|
2
|
+
|
|
3
|
+
The local source of truth (spec §11): WAL journaling for file-backed
|
|
4
|
+
databases, ordered idempotent migrations tracked in ``schema_migrations``,
|
|
5
|
+
and commit/rollback transaction semantics. Every SQLite-backed runtime
|
|
6
|
+
subsystem persists through this adapter — never through aiosqlite directly.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from collections.abc import AsyncIterator, Sequence
|
|
13
|
+
from contextlib import asynccontextmanager
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
|
+
|
|
16
|
+
import aiosqlite
|
|
17
|
+
|
|
18
|
+
from voodoo.storage.database.interfaces import (
|
|
19
|
+
DatabaseCapabilities,
|
|
20
|
+
Migration,
|
|
21
|
+
VoodooDatabase,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
LEDGER_TABLE = "schema_migrations"
|
|
25
|
+
|
|
26
|
+
# Framework-owned schema changes. Version 1 is the user-model baseline
|
|
27
|
+
# registered by ``voodoo.data``; the framework reserves 2+ (tasks table,
|
|
28
|
+
# executions journal, schedules, … arrive in later sprints).
|
|
29
|
+
FRAMEWORK_MIGRATIONS: tuple[Migration, ...] = ()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SQLiteDatabase:
|
|
33
|
+
"""Embedded SQLite backend — the default Voodoo database."""
|
|
34
|
+
|
|
35
|
+
provider = "sqlite"
|
|
36
|
+
|
|
37
|
+
def __init__(self, path: str, migrations: Sequence[Migration] = ()) -> None:
|
|
38
|
+
self.path = path
|
|
39
|
+
self._extra_migrations = tuple(migrations)
|
|
40
|
+
self._migrations = self._merge_migrations()
|
|
41
|
+
self._conn: aiosqlite.Connection | None = None
|
|
42
|
+
self._version = 0
|
|
43
|
+
|
|
44
|
+
def _merge_migrations(self) -> tuple[Migration, ...]:
|
|
45
|
+
by_version: dict[int, Migration] = {}
|
|
46
|
+
for migration in (*FRAMEWORK_MIGRATIONS, *self._extra_migrations):
|
|
47
|
+
if migration.version in by_version:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"duplicate migration version {migration.version} "
|
|
50
|
+
f"({by_version[migration.version].name!r} vs {migration.name!r})"
|
|
51
|
+
)
|
|
52
|
+
by_version[migration.version] = migration
|
|
53
|
+
return tuple(by_version[v] for v in sorted(by_version))
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def connection(self) -> aiosqlite.Connection:
|
|
57
|
+
if self._conn is None:
|
|
58
|
+
raise RuntimeError(
|
|
59
|
+
f"sqlite database at {self.path!r} is not connected; "
|
|
60
|
+
"call connect() first"
|
|
61
|
+
)
|
|
62
|
+
return self._conn
|
|
63
|
+
|
|
64
|
+
# -- lifecycle -----------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
async def connect(self) -> None:
|
|
67
|
+
if self._conn is not None:
|
|
68
|
+
return
|
|
69
|
+
db_dir = os.path.dirname(self.path)
|
|
70
|
+
if db_dir:
|
|
71
|
+
os.makedirs(db_dir, exist_ok=True)
|
|
72
|
+
self._conn = await aiosqlite.connect(self.path)
|
|
73
|
+
self._conn.row_factory = aiosqlite.Row
|
|
74
|
+
# Durability/concurrency pragmas. WAL is a no-op on :memory:.
|
|
75
|
+
await self._conn.execute("PRAGMA journal_mode=WAL")
|
|
76
|
+
await self._conn.execute("PRAGMA busy_timeout=5000")
|
|
77
|
+
await self._conn.commit()
|
|
78
|
+
|
|
79
|
+
async def close(self) -> None:
|
|
80
|
+
"""Close the connection if open. No-op otherwise (keeps startup lazy).
|
|
81
|
+
|
|
82
|
+
aiosqlite runs each connection on a dedicated non-daemon thread; an
|
|
83
|
+
unclosed connection would keep the interpreter alive at shutdown.
|
|
84
|
+
"""
|
|
85
|
+
if self._conn is not None:
|
|
86
|
+
try:
|
|
87
|
+
await self._conn.close()
|
|
88
|
+
finally:
|
|
89
|
+
self._conn = None
|
|
90
|
+
|
|
91
|
+
# -- migrations ----------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
async def migrate(self) -> None:
|
|
94
|
+
conn = self.connection
|
|
95
|
+
await conn.execute(
|
|
96
|
+
f"CREATE TABLE IF NOT EXISTS {LEDGER_TABLE} ("
|
|
97
|
+
"version INTEGER PRIMARY KEY,"
|
|
98
|
+
"name TEXT NOT NULL,"
|
|
99
|
+
"applied_at TEXT NOT NULL DEFAULT (datetime('now')))"
|
|
100
|
+
)
|
|
101
|
+
await conn.commit()
|
|
102
|
+
|
|
103
|
+
applied = {
|
|
104
|
+
row["version"]
|
|
105
|
+
for row in await self.fetch_all(f"SELECT version FROM {LEDGER_TABLE}")
|
|
106
|
+
}
|
|
107
|
+
for migration in self._migrations:
|
|
108
|
+
if migration.version not in applied:
|
|
109
|
+
async with self.transaction():
|
|
110
|
+
for statement in migration.statements:
|
|
111
|
+
await conn.execute(statement)
|
|
112
|
+
if migration.fn is not None:
|
|
113
|
+
await migration.fn(self)
|
|
114
|
+
await conn.execute(
|
|
115
|
+
f"INSERT INTO {LEDGER_TABLE} (version, name) VALUES (?, ?)",
|
|
116
|
+
(migration.version, migration.name),
|
|
117
|
+
)
|
|
118
|
+
self._version = migration.version
|
|
119
|
+
elif migration.rerun and migration.fn is not None:
|
|
120
|
+
# Idempotent re-runnable step: keep late-registered items
|
|
121
|
+
# (e.g. models imported after the baseline) working.
|
|
122
|
+
await migration.fn(self)
|
|
123
|
+
|
|
124
|
+
def current_version(self) -> int:
|
|
125
|
+
return self._version
|
|
126
|
+
|
|
127
|
+
# -- queries -------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
@asynccontextmanager
|
|
130
|
+
async def transaction(self) -> AsyncIterator[aiosqlite.Connection]:
|
|
131
|
+
"""Run a block atomically: commit on success, rollback on error.
|
|
132
|
+
|
|
133
|
+
Yields the raw connection so callers compose arbitrary statements;
|
|
134
|
+
aiosqlite opens the implicit transaction on first DML.
|
|
135
|
+
"""
|
|
136
|
+
conn = self.connection
|
|
137
|
+
try:
|
|
138
|
+
yield conn
|
|
139
|
+
except BaseException:
|
|
140
|
+
await conn.rollback()
|
|
141
|
+
raise
|
|
142
|
+
await conn.commit()
|
|
143
|
+
|
|
144
|
+
async def execute(self, query: str, params: Sequence[Any] = ()) -> aiosqlite.Cursor:
|
|
145
|
+
cursor = await self.connection.execute(query, params)
|
|
146
|
+
await self.connection.commit()
|
|
147
|
+
return cursor
|
|
148
|
+
|
|
149
|
+
async def fetch_all(
|
|
150
|
+
self, query: str, params: Sequence[Any] = ()
|
|
151
|
+
) -> list[aiosqlite.Row]:
|
|
152
|
+
async with self.connection.execute(query, params) as cursor:
|
|
153
|
+
return list(await cursor.fetchall())
|
|
154
|
+
|
|
155
|
+
async def fetch_one(
|
|
156
|
+
self, query: str, params: Sequence[Any] = ()
|
|
157
|
+
) -> aiosqlite.Row | None:
|
|
158
|
+
async with self.connection.execute(query, params) as cursor:
|
|
159
|
+
return await cursor.fetchone()
|
|
160
|
+
|
|
161
|
+
def capabilities(self) -> DatabaseCapabilities:
|
|
162
|
+
return DatabaseCapabilities(
|
|
163
|
+
provider=self.provider,
|
|
164
|
+
transactions=True,
|
|
165
|
+
migrations=True,
|
|
166
|
+
native_json=False, # JSON persisted as TEXT
|
|
167
|
+
concurrent_writers=False, # single writer at a time (WAL readers ok)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
if TYPE_CHECKING:
|
|
172
|
+
# Structural conformance assertion — mypy errors here if the adapter
|
|
173
|
+
# drifts from the VoodooDatabase protocol.
|
|
174
|
+
_protocol_check: VoodooDatabase = SQLiteDatabase("check")
|
|
@@ -10,7 +10,6 @@ src/voodoo/py.typed
|
|
|
10
10
|
src/voodoo/queue.py
|
|
11
11
|
src/voodoo/seo.py
|
|
12
12
|
src/voodoo/status.py
|
|
13
|
-
src/voodoo/storage.py
|
|
14
13
|
src/voodoo/theme.py
|
|
15
14
|
src/voodoo/adapters/__init__.py
|
|
16
15
|
src/voodoo/adapters/tailwind.py
|
|
@@ -93,6 +92,11 @@ src/voodoo/security/headers.py
|
|
|
93
92
|
src/voodoo/security/passwords.py
|
|
94
93
|
src/voodoo/security/rate_limit.py
|
|
95
94
|
src/voodoo/static/client.js
|
|
95
|
+
src/voodoo/storage/__init__.py
|
|
96
|
+
src/voodoo/storage/manager.py
|
|
97
|
+
src/voodoo/storage/database/__init__.py
|
|
98
|
+
src/voodoo/storage/database/interfaces.py
|
|
99
|
+
src/voodoo/storage/database/sqlite.py
|
|
96
100
|
src/voodoo/telemetry/__init__.py
|
|
97
101
|
src/voodoo/telemetry/api.py
|
|
98
102
|
src/voodoo/telemetry/middleware.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo_framework.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo_framework.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo_framework.egg-info/requires.txt
RENAMED
|
File without changes
|
{voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo_framework.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|