Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,6 @@ specs/
.playwright-mcp
.geminiignore
uv.toml
.beads/
.agent/
.geminiignore
.beads/
12 changes: 12 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,18 @@ def update_html_context(
context["generate_toctree_html"] = partial(context["generate_toctree_html"], startdepth=0)


def _ensure_static_dir(app: Sphinx, exception: Any) -> None:
"""Ensure _static directory exists for extensions that write to it."""
if exception is None and hasattr(app.builder, "outdir"):
from pathlib import Path

static_dir = Path(app.builder.outdir) / "_static"
static_dir.mkdir(parents=True, exist_ok=True)


def setup(app: Sphinx) -> dict[str, bool]:
app.setup_extension("shibuya")
# Ensure _static exists before sphinx_datatables tries to write to it
# Use priority < 500 to run before sphinx_datatables' finish handler
app.connect("build-finished", _ensure_static_dir, priority=100)
return {"parallel_read_safe": True, "parallel_write_safe": True}
11 changes: 8 additions & 3 deletions docs/examples/extensions/litestar/plugin_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
def test_litestar_plugin_setup() -> None:
pytest.importorskip("litestar")
# start-example
from litestar import Litestar
from litestar import Litestar, get

from sqlspec import SQLSpec
from sqlspec.adapters.sqlite import SqliteConfig
from sqlspec.adapters.sqlite import SqliteConfig, SqliteDriver
from sqlspec.extensions.litestar import SQLSpecPlugin

sqlspec = SQLSpec()
Expand All @@ -21,7 +21,12 @@ def test_litestar_plugin_setup() -> None:
)
)

app = Litestar(plugins=[SQLSpecPlugin(sqlspec=sqlspec)])
@get("/health")
def health_check(db_session: SqliteDriver) -> dict[str, str]:
result = db_session.execute("SELECT 'ok' as status")
return result.one()

app = Litestar(route_handlers=[health_check], plugins=[SQLSpecPlugin(sqlspec=sqlspec)])
# end-example

assert app is not None
6 changes: 2 additions & 4 deletions docs/examples/frameworks/fastapi/basic_setup.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from __future__ import annotations

from typing import Annotated, Any

import pytest
Expand All @@ -14,7 +12,7 @@ def test_fastapi_basic_setup() -> None:
from fastapi import Depends, FastAPI

from sqlspec import SQLSpec
from sqlspec.adapters.aiosqlite import AiosqliteConfig
from sqlspec.adapters.aiosqlite import AiosqliteConfig, AiosqliteDriver
from sqlspec.extensions.fastapi import SQLSpecPlugin

sqlspec = SQLSpec()
Expand All @@ -24,7 +22,7 @@ def test_fastapi_basic_setup() -> None:
db_ext = SQLSpecPlugin(sqlspec, app)

@app.get("/teams")
async def list_teams(db: Annotated[Any, Depends(db_ext.provide_session())]) -> Any:
async def list_teams(db: Annotated[AiosqliteDriver, Depends(db_ext.provide_session())]) -> dict[str, Any]:
result = await db.execute("select 1 as ok")
return result.one()

Expand Down
57 changes: 57 additions & 0 deletions docs/examples/frameworks/fastapi/multi_database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from typing import Annotated

import pytest

__all__ = ("test_fastapi_multi_database",)


def test_fastapi_multi_database() -> None:
pytest.importorskip("fastapi")
pytest.importorskip("aiosqlite")
# start-example
from fastapi import Depends, FastAPI

from sqlspec import SQLSpec
from sqlspec.adapters.aiosqlite import AiosqliteConfig, AiosqliteDriver
from sqlspec.adapters.sqlite import SqliteConfig, SqliteDriver
from sqlspec.extensions.fastapi import SQLSpecPlugin

sqlspec = SQLSpec()

# Primary async database
sqlspec.add_config(
AiosqliteConfig(
connection_config={"database": ":memory:"},
extension_config={
"starlette": {"session_key": "db", "connection_key": "db_connection", "pool_key": "db_pool"}
},
)
)

# ETL sync database (e.g., DuckDB pattern)
sqlspec.add_config(
SqliteConfig(
connection_config={"database": ":memory:"},
extension_config={
"starlette": {"session_key": "etl_db", "connection_key": "etl_connection", "pool_key": "etl_pool"}
},
)
)

app = FastAPI()
db_plugin = SQLSpecPlugin(sqlspec, app)

@app.get("/report")
async def report(
db: Annotated[AiosqliteDriver, Depends(db_plugin.provide_session("db"))],
etl_db: Annotated[SqliteDriver, Depends(db_plugin.provide_session("etl_db"))],
) -> dict[str, list]:
# Async query to primary database
users = await db.select("SELECT 1 as id, 'Alice' as name")
# Sync query to ETL database
metrics = etl_db.select("SELECT 'metric1' as name, 100 as value")
return {"users": users, "metrics": metrics}

# end-example

assert app is not None
26 changes: 16 additions & 10 deletions docs/examples/frameworks/flask/basic_setup.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from __future__ import annotations

from typing import Any

import pytest

__all__ = ("test_flask_basic_setup",)
Expand All @@ -13,21 +11,29 @@ def test_flask_basic_setup() -> None:
from flask import Flask

from sqlspec import SQLSpec
from sqlspec.adapters.sqlite import SqliteConfig
from sqlspec.adapters.sqlite import SqliteConfig, SqliteDriver
from sqlspec.extensions.flask import SQLSpecPlugin

# Create SQLSpec and plugin at module level
sqlspec = SQLSpec()
sqlspec.add_config(SqliteConfig(connection_config={"database": ":memory:"}))
plugin = SQLSpecPlugin(sqlspec)

def create_app() -> Flask:
"""Application factory pattern."""
app = Flask(__name__)
plugin.init_app(app)

app = Flask(__name__)
plugin = SQLSpecPlugin(sqlspec, app)
@app.get("/health")
def health() -> dict[str, int]:
db: SqliteDriver = plugin.get_session()
result = db.execute("select 1 as ok")
return result.one()

@app.get("/health")
def health() -> Any:
session = plugin.get_session()
result = session.execute("select 1 as ok")
return result.one()
return app

app = create_app()
# end-example

assert plugin is not None
assert app is not None
57 changes: 57 additions & 0 deletions docs/examples/frameworks/flask/multi_database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from __future__ import annotations

import pytest

__all__ = ("test_flask_multi_database",)


def test_flask_multi_database() -> None:
pytest.importorskip("flask")
# start-example
from flask import Flask

from sqlspec import SQLSpec
from sqlspec.adapters.sqlite import SqliteConfig, SqliteDriver
from sqlspec.extensions.flask import SQLSpecPlugin

sqlspec = SQLSpec()

# Primary database
sqlspec.add_config(
SqliteConfig(
connection_config={"database": ":memory:"},
extension_config={"flask": {"session_key": "db", "connection_key": "db_connection", "pool_key": "db_pool"}},
)
)

# ETL database with custom keys
sqlspec.add_config(
SqliteConfig(
connection_config={"database": ":memory:"},
extension_config={
"flask": {"session_key": "etl_db", "connection_key": "etl_connection", "pool_key": "etl_pool"}
},
)
)

plugin = SQLSpecPlugin(sqlspec)

def create_app() -> Flask:
app = Flask(__name__)
plugin.init_app(app)

@app.get("/report")
def report() -> dict[str, list]:
db: SqliteDriver = plugin.get_session("db")
etl_db: SqliteDriver = plugin.get_session("etl_db")

users = db.select("SELECT 1 as id, 'Alice' as name")
metrics = etl_db.select("SELECT 'metric1' as name, 100 as value")
return {"users": users, "metrics": metrics}

return app

app = create_app()
# end-example

assert app is not None
9 changes: 6 additions & 3 deletions docs/examples/frameworks/starlette/basic_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,22 @@ def test_starlette_basic_setup() -> None:
from starlette.routing import Route

from sqlspec import SQLSpec
from sqlspec.adapters.aiosqlite import AiosqliteConfig
from sqlspec.adapters.aiosqlite import AiosqliteConfig, AiosqliteDriver
from sqlspec.extensions.starlette import SQLSpecPlugin

sqlspec = SQLSpec()
sqlspec.add_config(AiosqliteConfig(connection_config={"database": ":memory:"}))

# Create plugin at module level
db_plugin = SQLSpecPlugin(sqlspec)

async def health(request: Request) -> JSONResponse:
db = request.app.state.sqlspec.get_session(request)
db: AiosqliteDriver = db_plugin.get_session(request)
result = await db.execute("select 1 as ok")
return JSONResponse(result.one())

app = Starlette(routes=[Route("/health", health)])
app.state.sqlspec = SQLSpecPlugin(sqlspec, app)
db_plugin.init_app(app) # Initialize plugin with app
# end-example

assert app is not None
60 changes: 60 additions & 0 deletions docs/examples/frameworks/starlette/multi_database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from __future__ import annotations

import pytest

__all__ = ("test_starlette_multi_database",)


def test_starlette_multi_database() -> None:
pytest.importorskip("starlette")
pytest.importorskip("aiosqlite")
# start-example
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.routing import Route

from sqlspec import SQLSpec
from sqlspec.adapters.aiosqlite import AiosqliteConfig, AiosqliteDriver
from sqlspec.adapters.sqlite import SqliteConfig, SqliteDriver
from sqlspec.extensions.starlette import SQLSpecPlugin

sqlspec = SQLSpec()

# Primary async database
sqlspec.add_config(
AiosqliteConfig(
connection_config={"database": ":memory:"},
extension_config={
"starlette": {"session_key": "db", "connection_key": "db_connection", "pool_key": "db_pool"}
},
)
)

# ETL sync database
sqlspec.add_config(
SqliteConfig(
connection_config={"database": ":memory:"},
extension_config={
"starlette": {"session_key": "etl_db", "connection_key": "etl_connection", "pool_key": "etl_pool"}
},
)
)

db_plugin = SQLSpecPlugin(sqlspec)

async def report(request: Request) -> JSONResponse:
db: AiosqliteDriver = db_plugin.get_session(request, "db")
etl_db: SqliteDriver = db_plugin.get_session(request, "etl_db")

# Async query to primary database
users = await db.select("SELECT 1 as id, 'Alice' as name")
# Sync query to ETL database
metrics = etl_db.select("SELECT 'metric1' as name, 100 as value")
return JSONResponse({"users": users, "metrics": metrics})

app = Starlette(routes=[Route("/report", report)])
db_plugin.init_app(app)
# end-example

assert app is not None
Loading
Loading