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.
Files changed (156) hide show
  1. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/PKG-INFO +1 -1
  2. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/__init__.py +1 -1
  3. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/data/__init__.py +7 -4
  4. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/data/base.py +32 -10
  5. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/data/model.py +5 -7
  6. voodoo_framework-1.3.0/src/voodoo/storage/__init__.py +15 -0
  7. voodoo_framework-1.3.0/src/voodoo/storage/database/__init__.py +27 -0
  8. voodoo_framework-1.3.0/src/voodoo/storage/database/interfaces.py +80 -0
  9. voodoo_framework-1.3.0/src/voodoo/storage/database/sqlite.py +174 -0
  10. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo_framework.egg-info/PKG-INFO +1 -1
  11. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo_framework.egg-info/SOURCES.txt +5 -1
  12. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/README.md +0 -0
  13. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/pyproject.toml +0 -0
  14. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/setup.cfg +0 -0
  15. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/adapters/__init__.py +0 -0
  16. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/adapters/tailwind.py +0 -0
  17. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/adapters/voodoo_css.py +0 -0
  18. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/agent.py +0 -0
  19. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ai/__init__.py +0 -0
  20. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ai/agent.py +0 -0
  21. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ai/providers/__init__.py +0 -0
  22. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ai/providers/anthropic.py +0 -0
  23. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ai/providers/gemini.py +0 -0
  24. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ai/providers/mock.py +0 -0
  25. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ai/providers/ollama.py +0 -0
  26. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ai/providers/openai.py +0 -0
  27. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ai/tools/__init__.py +0 -0
  28. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ai/tools/registry.py +0 -0
  29. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/api.py +0 -0
  30. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/auth/__init__.py +0 -0
  31. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/auth/cookies.py +0 -0
  32. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/auth/guards.py +0 -0
  33. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/auth/jwt.py +0 -0
  34. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/auth/middleware.py +0 -0
  35. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/auth/passwords.py +0 -0
  36. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/auth/user.py +0 -0
  37. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/cli/__init__.py +0 -0
  38. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/cli/__main__.py +0 -0
  39. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/cli/ai.py +0 -0
  40. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/cli/auth.py +0 -0
  41. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/cli/dev.py +0 -0
  42. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/cli/doctor.py +0 -0
  43. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/cli/generate.py +0 -0
  44. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/cli/inspect.py +0 -0
  45. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/cli/new.py +0 -0
  46. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/cli/recover.py +0 -0
  47. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/cli/routes.py +0 -0
  48. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/cli/scaffolding.py +0 -0
  49. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/cli/terminal.py +0 -0
  50. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/cli/version.py +0 -0
  51. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/components.py +0 -0
  52. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/config.py +0 -0
  53. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/core/__init__.py +0 -0
  54. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/core/app.py +0 -0
  55. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/core/errors.py +0 -0
  56. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/core/events.py +0 -0
  57. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/core/render.py +0 -0
  58. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/core/routing.py +0 -0
  59. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/core/sitemap.py +0 -0
  60. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/core/state.py +0 -0
  61. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/i18n.py +0 -0
  62. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/mcp/__init__.py +0 -0
  63. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/mcp/client.py +0 -0
  64. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/mesh/__init__.py +0 -0
  65. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/mesh/client.py +0 -0
  66. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/primitives/__init__.py +0 -0
  67. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/primitives/capability.py +0 -0
  68. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/primitives/compute.py +0 -0
  69. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/primitives/constraint.py +0 -0
  70. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/primitives/effect.py +0 -0
  71. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/primitives/intent.py +0 -0
  72. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/primitives/resource.py +0 -0
  73. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/primitives/state.py +0 -0
  74. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/primitives/time.py +0 -0
  75. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/py.typed +0 -0
  76. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/queue.py +0 -0
  77. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/routing/api.py +0 -0
  78. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/routing/pages.py +0 -0
  79. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/runtime/__init__.py +0 -0
  80. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/runtime/adaptive.py +0 -0
  81. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/runtime/capability.py +0 -0
  82. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/runtime/constraint.py +0 -0
  83. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/runtime/context.py +0 -0
  84. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/runtime/engine.py +0 -0
  85. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/runtime/errors.py +0 -0
  86. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/runtime/execution.py +0 -0
  87. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/runtime/graph.py +0 -0
  88. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/runtime/human.py +0 -0
  89. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/runtime/persistence.py +0 -0
  90. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/runtime/planner.py +0 -0
  91. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/runtime/task.py +0 -0
  92. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/runtime/workflow.py +0 -0
  93. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/security/__init__.py +0 -0
  94. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/security/cors.py +0 -0
  95. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/security/csrf.py +0 -0
  96. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/security/headers.py +0 -0
  97. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/security/passwords.py +0 -0
  98. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/security/rate_limit.py +0 -0
  99. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/seo.py +0 -0
  100. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/static/client.js +0 -0
  101. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/status.py +0 -0
  102. /voodoo_framework-1.2.0/src/voodoo/storage.py → /voodoo_framework-1.3.0/src/voodoo/storage/manager.py +0 -0
  103. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/telemetry/__init__.py +0 -0
  104. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/telemetry/api.py +0 -0
  105. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/telemetry/middleware.py +0 -0
  106. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/telemetry/store.py +0 -0
  107. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/theme.py +0 -0
  108. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/tools/__init__.py +0 -0
  109. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/tools/registry.py +0 -0
  110. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ui/__init__.py +0 -0
  111. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ui/component.py +0 -0
  112. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ui/events.py +0 -0
  113. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ui/library.py +0 -0
  114. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ui/rendering.py +0 -0
  115. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ui/state.py +0 -0
  116. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ui/styles/__init__.py +0 -0
  117. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/ui/styles/theme.py +0 -0
  118. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/workers/__init__.py +0 -0
  119. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo/workers/queue.py +0 -0
  120. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo_framework.egg-info/dependency_links.txt +0 -0
  121. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo_framework.egg-info/entry_points.txt +0 -0
  122. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo_framework.egg-info/requires.txt +0 -0
  123. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/src/voodoo_framework.egg-info/top_level.txt +0 -0
  124. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_agent.py +0 -0
  125. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_agent_runtime.py +0 -0
  126. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_app.py +0 -0
  127. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_auth.py +0 -0
  128. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_cli.py +0 -0
  129. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_cli_inspect.py +0 -0
  130. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_components.py +0 -0
  131. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_contract_api.py +0 -0
  132. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_data.py +0 -0
  133. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_events.py +0 -0
  134. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_http_runtime.py +0 -0
  135. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_human.py +0 -0
  136. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_i18n.py +0 -0
  137. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_integration.py +0 -0
  138. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_mcp.py +0 -0
  139. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_mcp_runtime.py +0 -0
  140. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_mesh.py +0 -0
  141. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_model.py +0 -0
  142. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_persistence.py +0 -0
  143. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_planner_adaptive.py +0 -0
  144. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_primitives.py +0 -0
  145. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_providers.py +0 -0
  146. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_queue.py +0 -0
  147. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_reactive.py +0 -0
  148. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_runtime.py +0 -0
  149. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_security.py +0 -0
  150. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_seo.py +0 -0
  151. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_state.py +0 -0
  152. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_theme.py +0 -0
  153. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_tools.py +0 -0
  154. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_ui.py +0 -0
  155. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_websocket.py +0 -0
  156. {voodoo_framework-1.2.0 → voodoo_framework-1.3.0}/tests/test_workers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voodoo-framework
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: Voodoo Framework
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: starlette
@@ -76,7 +76,7 @@ from .ui.styles import (
76
76
  from .ui.styles.theme import Theme, ThemeColors, create_theme
77
77
  from .workers import task
78
78
 
79
- __version__ = "1.2.0"
79
+ __version__ = "1.3.0"
80
80
 
81
81
  __all__ = [
82
82
  # Core runtime
@@ -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`). The functions
10
- ``init_db`` and ``get_db`` together with the module-level ``_db_connection``
11
- form the seam a future PostgreSQL adapter would replace; everything above
12
- them (``BaseModel``, ``Model`` and the hooks) is backend-agnostic.
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 aiosqlite
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
- _db_connection = await aiosqlite.connect(db_path)
25
- _db_connection.row_factory = aiosqlite.Row
26
- for model in _models:
27
- await model._create_table()
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. Swapping SQLite for PostgreSQL
12
- (or another backend) only requires replacing the connection function
13
- and the DDL generated by ``BaseModel._create_table``; the facade stays
14
- the same. Migrations are an explicit extension point: today the only
15
- DDL emitted is ``CREATE TABLE IF NOT EXISTS`` (create-if-absent). A
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")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voodoo-framework
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: Voodoo Framework
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: starlette
@@ -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