`_ for detailed fixture conventions.
- **Integration Resources**:
- - Integration tests use ``pytest-databases`` fixtures.
- - Run database containers locally before running integration tests if testing locally (e.g. via ``make infra-up``).
+ - Integration tests use ``pytest-databases`` fixtures to provision and manage database containers automatically.
- **Coverage Requirements**:
- The repository-wide coverage floor is temporarily 76%.
- Every new commit must keep changed-code coverage at or above **90%**.
diff --git a/Makefile b/Makefile
index 0a851c8eb..091e9df77 100644
--- a/Makefile
+++ b/Makefile
@@ -169,7 +169,7 @@ clean: ## Cleanup temporary build a
.PHONY: test
test: ## Run the tests
@echo "${INFO} Running test cases... 🧪"
- @uv run pytest -n 4 --dist=loadgroup tests/unit tests/typing docs/examples
+ @uv run pytest -n 4 --dist=loadgroup tests/unit
@uv run pytest -n 1 --dist=loadgroup tests/integration --ignore=tests/integration/adapters
@for family in $(ADAPTER_FAMILIES); do
uv run pytest -n 1 --dist=loadgroup "tests/integration/adapters/$${family}"
@@ -185,7 +185,7 @@ coverage: ## Run tests with coverage r
@echo "${INFO} Running tests with coverage... 📊"
@uv run coverage erase
@uv run pytest --cov --cov-report= --cov-fail-under=0 -n 4 --dist=loadgroup --quiet \
- tests/unit tests/typing docs/examples
+ tests/unit
@uv run pytest --cov --cov-append --cov-report= --cov-fail-under=0 -n 1 --dist=loadgroup --quiet \
tests/integration --ignore=tests/integration/adapters
@for family in $(ADAPTER_FAMILIES); do
@@ -259,7 +259,7 @@ lint: fix prek type-check slotscheck zizmor ## Run all linting checks
@echo "${OK} All linting checks passed ✨"
.PHONY: check-all
-check-all: lint test-all coverage ## Run all checks (lint, test, coverage)
+check-all: lint coverage ## Run all checks (lint, test with coverage)
@echo "${OK} All checks passed successfully ✨"
# =============================================================================
@@ -405,49 +405,6 @@ pgo-local: ## Run full three-stage PGO
@uv pip install dist/*.whl --force-reinstall --no-deps >/dev/null 2>&1
@echo "${OK} PGO build complete 🚀"
-# =============================================================================
-# Development Infrastructure
-# =============================================================================
-
-.PHONY: infra-up
-infra-up: ## Start development infrastructure (databases, storage)
- @echo "${INFO} Starting development infrastructure..."
- @./tools/local-infra.sh up
- @echo "${OK} Development infrastructure ready ✨"
-
-.PHONY: infra-down
-infra-down: ## Stop development infrastructure
- @echo "${INFO} Stopping development infrastructure..."
- @./tools/local-infra.sh down --quiet
- @echo "${OK} Development infrastructure stopped"
-
-.PHONY: infra-status
-infra-status: ## Show development infrastructure status
- @./tools/local-infra.sh status
-
-.PHONY: infra-cleanup
-infra-cleanup: ## Clean up development infrastructure
- @echo "${WARN} This will remove all development containers and volumes"
- @./tools/local-infra.sh cleanup
-
-.PHONY: infra-postgres
-infra-postgres: ## Start only PostgreSQL
- @echo "${INFO} Starting PostgreSQL..."
- @./tools/local-infra.sh up postgres --quiet
- @echo "${OK} PostgreSQL ready on port 5433"
-
-.PHONY: infra-oracle
-infra-oracle: ## Start only Oracle
- @echo "${INFO} Starting Oracle..."
- @./tools/local-infra.sh up oracle --quiet
- @echo "${OK} Oracle ready on port 1522"
-
-.PHONY: infra-mysql
-infra-mysql: ## Start only MySQL
- @echo "${INFO} Starting MySQL..."
- @./tools/local-infra.sh up mysql --quiet
- @echo "${OK} MySQL ready on port 3307"
-
# =============================================================================
# End of Makefile
# =============================================================================
diff --git a/docs/PYPI_README.md b/docs/PYPI_README.md
deleted file mode 100644
index 1f79d1512..000000000
--- a/docs/PYPI_README.md
+++ /dev/null
@@ -1,88 +0,0 @@
-
-
-# SQLSpec
-
-**Type-safe SQL execution for Python, without an ORM.**
-
-[](https://pypi.org/project/sqlspec/)
-[](https://pypi.org/project/sqlspec/)
-[](https://github.com/litestar-org/sqlspec/blob/main/LICENSE)
-[](https://sqlspec.dev/)
-
-
-
-SQLSpec is a SQL execution layer for Python. You write the SQL -- as strings, through a builder API, or loaded from files. SQLSpec handles connections, parameter binding, and dialect translation. It maps results back to typed Python objects. It uses [sqlglot](https://github.com/tobymao/sqlglot) under the hood. Queries are parsed, validated, and optimized before they hit the database.
-
-| Area | Support |
-| --- | --- |
-| **One API** | The same session and result APIs with sync or async drivers. |
-| **Databases** | PostgreSQL, SQLite, DuckDB, MySQL, SQL Server, Oracle, CockroachDB, BigQuery, Spanner, and supported Arrow Database Connectivity backends such as Snowflake, Flight SQL, and GizmoSQL. |
-| **Data tools** | Typed result mapping, Arrow export, built-in storage, and native bulk ingest where the adapter supports it. |
-| **Frameworks** | Litestar, FastAPI, Flask, Sanic, and Starlette. |
-
-## Quick Start
-
-```bash
-uv add sqlspec
-# or
-pip install sqlspec
-```
-
-```python
-from dataclasses import dataclass
-
-from sqlspec import SQLSpec
-from sqlspec.adapters.sqlite import SqliteConfig
-
-
-@dataclass
-class Greeting:
- message: str
-
-
-spec = SQLSpec()
-db = spec.add_config(SqliteConfig(connection_config={"database": ":memory:"}))
-
-with spec.provide_session(db) as session:
- greeting = session.select_one("SELECT 'Hello, SQLSpec!' AS message", schema_type=Greeting)
- print(greeting.message)
-```
-
-Write SQL, define a schema, get typed objects back. The [getting started guide](https://sqlspec.dev/getting_started/) covers adapter installation and the query builder.
-
-## Features
-
-- **Session lifecycle** -- sync and async sessions with pooling where the adapter supports it
-- **Parameter binding and dialect translation** -- powered by sqlglot, with a fluent query builder and `.sql` file loader
-- **Result mapping** -- map rows to Pydantic, msgspec, attrs, or dataclass models, or export to Arrow tables for pandas and Polars
-- **Storage layer** -- read and write Arrow tables to local files, fsspec, or object stores
-- **Framework integrations** -- Litestar plugin with DI, Starlette/FastAPI/Sanic middleware, Flask extension
-- **Google ADK** -- SQLSpec-backed session, event, memory, and artifact services
-- **Observability** -- OpenTelemetry and Prometheus instrumentation, structured logging with correlation IDs
-- **Event channels** -- LISTEN/NOTIFY, Oracle AQ/TxEventQ, and durable table-backed queues with polling fallback
-- **Migrations** -- native schema migration CLI backed by SQLSpec's SQL file loader
-
-## Documentation
-
-- [Getting Started](https://sqlspec.dev/getting_started/) -- installation, adapter selection, first steps
-- [Usage Guides](https://sqlspec.dev/usage/) -- adapters, configuration, SQL file loader, and more
-- [Recipes](https://sqlspec.dev/recipes/) -- production patterns (DI, service layers, multi-tenancy)
-- [API Reference](https://sqlspec.dev/reference/) -- full API docs
-- [CLI Reference](https://sqlspec.dev/usage/cli.html) -- migration and management commands
-
-## Playground
-
-Want to try it without installing anything? The [interactive playground](https://sqlspec.dev/playground) runs SQLSpec in your browser with a sandboxed Python runtime.
-
-## Reference Applications
-
-- **[PostgreSQL + Vertex AI Demo](https://github.com/cofin/postgres-vertexai-demo)** -- Vector search with pgvector and real-time chat using Litestar and Google ADK. Shows connection pooling, migrations, type-safe result mapping, vector embeddings, and response caching.
-- **[Oracle + Vertex AI Demo](https://github.com/cofin/oracledb-vertexai-demo)** -- Oracle 23ai vector search with semantic similarity using HNSW indexes. Demonstrates NumPy array conversion, large object handling, and real-time performance metrics.
-
-## Contributing
-
-Contributions are welcome -- whether that's bug reports, new adapter ideas, or pull requests. Take a look at the [contributor guide](https://sqlspec.dev/contributing/) to get started.
-
-## License
-
-MIT
diff --git a/docs/_static/.gitkeep b/docs/_static/.gitkeep
deleted file mode 100644
index e69de29bb..000000000
diff --git a/docs/_static/demos/.gitkeep b/docs/_static/demos/.gitkeep
deleted file mode 100644
index e69de29bb..000000000
diff --git a/docs/_static/theme.js b/docs/_static/theme.js
deleted file mode 100644
index 38dfc73eb..000000000
--- a/docs/_static/theme.js
+++ /dev/null
@@ -1,49 +0,0 @@
-function initDropdowns() {
- const dropdownToggles = document.querySelectorAll(".st-dropdown-toggle")
-
- const dropdowns = [...dropdownToggles].map(toggleEl => ({
- toggleEl,
- contentEL: toggleEl.parentElement.querySelector(".st-dropdown-menu")
- }))
-
- const close = (dropdown) => {
- const {toggleEl, contentEL} = dropdown
- toggleEl.setAttribute("aria-expanded", "false")
- contentEL.classList.toggle("hidden", true)
- }
-
- const closeAll = () => dropdowns.forEach(close)
-
- const open = (dropdown) => {
- closeAll()
- dropdown.toggleEl.setAttribute("aria-expanded", "true")
- dropdown.contentEL.classList.toggle("hidden", false)
- const boundaries = [dropdown.contentEL, ...dropdownToggles]
- const clickOutsideListener = (event) => {
- const target = event.target
- if (!target) return
-
- if (!boundaries.some(b => b.contains(target))) {
- closeAll()
- document.removeEventListener("click", clickOutsideListener)
- }
-
- }
- document.addEventListener("click", clickOutsideListener)
- }
-
-
- dropdowns.forEach(dropdown => {
- dropdown.toggleEl.addEventListener("click", () => {
- if (dropdown.toggleEl.getAttribute("aria-expanded") === "true") {
- close(dropdown)
- } else {
- open(dropdown)
- }
- })
- })
-}
-
-window.addEventListener("DOMContentLoaded", () => {
- initDropdowns()
-})
diff --git a/docs/_tapes/.gitkeep b/docs/_tapes/.gitkeep
deleted file mode 100644
index e69de29bb..000000000
diff --git a/docs/conf.py b/docs/conf.py
index 96bbcb1e0..de604b8b8 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -61,25 +61,18 @@
"sphinx.ext.githubpages",
"sphinx.ext.viewcode",
"tools.sphinx_ext.missing_references",
- "tools.sphinx_ext.changelog",
"tools.sphinx_ext.guarded_imports",
"sphinx_autodoc_typehints",
"myst_parser",
"auto_pytabs.sphinx_ext",
"sphinx_copybutton",
- "sphinx.ext.todo",
"sphinx_click",
"click_extra.sphinx",
"sphinx_design",
- "sphinx_tabs.tabs",
- "sphinx_togglebutton",
"sphinx_paramlinks",
"sphinxcontrib.mermaid",
"numpydoc",
"sphinx_iconify",
- "sphinx_datatables",
- "jupyter_sphinx",
- "nbsphinx",
"tools.sphinx_ext.playground",
]
intersphinx_mapping = {
@@ -162,14 +155,10 @@
# https://sphinx-copybutton.readthedocs.io/en/latest/use.html#strip-and-configure-input-prompts-for-code-cells
copybutton_prompt_text = "$ "
-nbsphinx_requirejs_path = ""
-jupyter_sphinx_require_url = ""
-
# -- Style configuration -----------------------------------------------------
html_theme = "shibuya"
html_title = "SQLSpec"
html_short_title = "SQLSpec"
-todo_include_todos = True
html_static_path = ["_static"]
html_favicon = "_static/favicon.png"
@@ -180,7 +169,6 @@
"_build",
"Thumbs.db",
".DS_Store",
- "PYPI_README.md",
"STYLE_GUIDE.md",
"VOICE_AUDIT_REPORT.md",
"autoapi/sqlspec/index.rst",
@@ -307,18 +295,7 @@ 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]:
+ """Configure Sphinx application plugins."""
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}
diff --git a/docs/examples/README.md b/docs/examples/README.md
index 5ace77029..93936e762 100644
--- a/docs/examples/README.md
+++ b/docs/examples/README.md
@@ -1,10 +1,21 @@
# SQLSpec Examples
-This directory contains the runnable, pytest-friendly example catalog used by the docs.
-Each file is scoped to a single concept and uses `# start-example` / `# end-example`
+This directory contains runnable examples and code snippets referenced throughout the documentation.
+Each file illustrates a single concept or workflow, often using `# start-example` and `# end-example`
markers for Sphinx `literalinclude` directives.
-Structure overview:
+## Examples vs. Library Tests
+
+The scripts and snippets in this directory serve as reader-facing demonstrations and documentation
+inclusions. They are designed to be read, adapted, or executed directly as stand-alone demonstrations
+(e.g., via `uv run python docs/examples/...`).
+
+Formal integration and regression testing for all SQLSpec behavior—including CLI configuration discovery,
+migration execution, and framework integrations—belongs exclusively in the library test suite under
+`tests/` (such as `tests/integration/cli/test_migration_quickstart.py`). Documentation examples do not
+serve as test suites and should not be collected as pytest test paths.
+
+## Structure Overview
- `quickstart/`: First-time setup and configuration.
- `frameworks/`: Litestar, FastAPI, Flask, Sanic, and Starlette integration examples.
@@ -17,8 +28,10 @@ Structure overview:
- `reference/`: API-level snippets for reference docs.
- `contributing/`: Adapter skeletons.
-Run the full example suite:
+## Running Examples
+
+Individual examples can be executed directly with Python:
```bash
-uv run pytest docs/examples/ -q
+uv run python docs/examples/quickstart_migrations.py
```
diff --git a/docs/examples/quickstart_migrations.py b/docs/examples/quickstart_migrations.py
index a9aaaf927..ab6985205 100644
--- a/docs/examples/quickstart_migrations.py
+++ b/docs/examples/quickstart_migrations.py
@@ -1,52 +1,47 @@
-import sqlite3
+"""Quickstart example demonstrating the SQLSpec migration CLI workflow."""
+
+import os
+import sys
+import tempfile
from pathlib import Path
-import pytest
from click.testing import CliRunner
from sqlspec.cli import add_migration_commands
-__all__ = ("test_sqlite_migration_quickstart",)
+__all__ = ("run_migration_quickstart",)
-def test_sqlite_migration_quickstart(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
- """Exercise the documented config discovery and migration workflow."""
+def run_migration_quickstart() -> None:
+ """Execute the standard migration workflow commands against a temporary SQLite database."""
config_example = Path(__file__).with_name("migration_quickstart_config.py")
- (tmp_path / "database.py").write_text(config_example.read_text())
- monkeypatch.chdir(tmp_path)
- monkeypatch.delenv("SQLSPEC_CONFIG", raising=False)
runner = CliRunner()
- config_path = "database:database_config"
-
- cli_result = runner.invoke(add_migration_commands(), ["--config", config_path, "show-config"])
- assert cli_result.exit_code == 0, cli_result.output
- assert "app" in cli_result.output
-
- env_result = runner.invoke(
- add_migration_commands(), ["show-config"], env={"SQLSPEC_CONFIG": "database.database_config"}
- )
- assert env_result.exit_code == 0, env_result.output
- assert "app" in env_result.output
-
- (tmp_path / "pyproject.toml").write_text('[tool.sqlspec]\nconfig = "database:database_config"\n')
- pyproject_result = runner.invoke(add_migration_commands(), ["show-config"])
- assert pyproject_result.exit_code == 0, pyproject_result.output
- assert "Using config from pyproject.toml" in pyproject_result.output
-
- commands = (
- ["init", "--no-prompt"],
- ["create-migration", "-m", "create users table", "--no-prompt"],
- ["upgrade", "--no-prompt"],
- ["show-current-revision"],
- )
- for command in commands:
- result = runner.invoke(add_migration_commands(), ["--config", config_path, *command])
- assert result.exit_code == 0, result.output
-
- assert (tmp_path / "app.db").is_file()
- assert len(list((tmp_path / "migrations").glob("*.sql"))) == 1
- with sqlite3.connect(tmp_path / "app.db") as connection:
- tracker_name = connection.execute(
- "SELECT name FROM sqlite_master WHERE type = ? AND name = ?", ("table", "schema_versions")
- ).fetchone()
- assert tracker_name == ("schema_versions",)
+ with tempfile.TemporaryDirectory() as temp_dir:
+ temp_path = Path(temp_dir)
+ (temp_path / "database.py").write_text(config_example.read_text())
+ old_cwd = Path.cwd()
+ sys.path.insert(0, str(temp_path))
+ os.chdir(temp_path)
+ try:
+ config_path = "database:database_config"
+ commands = (
+ ["show-config"],
+ ["init", "--no-prompt"],
+ ["create-migration", "-m", "create users table", "--no-prompt"],
+ ["upgrade", "--no-prompt"],
+ ["show-current-revision"],
+ )
+ for command in commands:
+ result = runner.invoke(add_migration_commands(), ["--config", config_path, *command])
+ print(result.output, end="")
+ if result.exit_code:
+ message = f"Migration command failed: {' '.join(command)}"
+ raise RuntimeError(message) from result.exception
+ finally:
+ os.chdir(old_cwd)
+ if str(temp_path) in sys.path:
+ sys.path.remove(str(temp_path))
+
+
+if __name__ == "__main__":
+ run_migration_quickstart()
diff --git a/pyproject.toml b/pyproject.toml
index 1f0939245..d16da3702 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -95,18 +95,13 @@ doc = [
"sphinx-click>=6.0.0",
"click-extra[sphinx]",
"sphinx-design>=0.5.0",
- "sphinx-tabs",
- "sphinx-datatables",
"sphinxcontrib-jquery",
"sphinxcontrib-mermaid>=0.9.2",
"sphinx-paramlinks>=0.6.0",
- "sphinx-togglebutton>=0.3.2",
"myst-parser",
"sphinx-autodoc-typehints",
"numpydoc",
"sphinx-iconify",
- "jupyter-sphinx",
- "nbsphinx",
]
extras = [
"adbc_driver_manager",
@@ -484,8 +479,8 @@ markers = [
"pymysql: marks tests using pymysql",
"psqlpy: marks tests using psqlpy",
]
-python_files = ["test_*.py", "quickstart_*.py", "usage_*.py"]
-testpaths = ["tests", "docs/examples"]
+python_files = ["test_*.py"]
+testpaths = ["tests"]
timeout = 300
timeout_method = "signal"
@@ -663,7 +658,7 @@ known-first-party = ["sqlspec", "tests"]
split-on-trailing-comma = false
[tool.ruff.lint.per-file-ignores]
-"docs/**/*.*" = ["S", "B", "DTZ", "A", "TC", "ERA", "D", "RET", "PLW0127", "PLR2004"]
+"docs/**/*.*" = ["S", "B", "DTZ", "A", "TC", "ERA", "D", "RET", "PLW0127", "PLR2004", "TID251"]
"docs/examples/**" = ["T201"]
"sqlspec/adapters/spanner/config.py" = ["PLC2801"]
"sqlspec/extensions/adk/converters.py" = ["S403"]
@@ -700,10 +695,14 @@ split-on-trailing-comma = false
"PT",
"PERF203",
"ANN",
+ "TID251",
]
-"tools/**/*.*" = ["D", "ARG", "EM", "TRY", "G", "FBT", "S603", "F811", "PLW0127", "PLR0911"]
+"tools/**/*.*" = ["D", "ARG", "EM", "TRY", "G", "FBT", "S603", "F811", "PLW0127", "PLR0911", "TID251"]
"tools/prepare_release.py" = ["S603", "S607"]
[tool.ruff.lint.flake8-tidy-imports]
# Disallow all relative imports.
ban-relative-imports = "all"
+
+[tool.ruff.lint.flake8-tidy-imports.banned-api]
+"__future__.annotations" = { msg = "Use Python 3.10+ PEP 604 annotations instead of from __future__ import annotations" }
diff --git a/sqlspec/extensions/events/_channel.py b/sqlspec/extensions/events/_channel.py
index 047cfb54f..32d2a7e1c 100644
--- a/sqlspec/extensions/events/_channel.py
+++ b/sqlspec/extensions/events/_channel.py
@@ -17,7 +17,7 @@
from sqlspec.extensions.events._models import EventMessage
from sqlspec.extensions.events._names import normalize_event_channel_name
from sqlspec.extensions.events._protocols import AsyncEventBackendProtocol, SyncEventBackendProtocol
-from sqlspec.extensions.events._queue import build_queue_backend
+from sqlspec.extensions.events._queue import SyncTableEventQueue, build_queue_backend
from sqlspec.utils.logging import get_logger, log_with_context
from sqlspec.utils.type_guards import has_span_attribute
from sqlspec.utils.uuids import uuid4
@@ -419,14 +419,21 @@ def _run_listener(
poll_interval: float,
auto_ack: bool,
) -> None:
- """Internal listener loop."""
+ """Internal listener loop.
+
+ Table queue waits preserve the polling interval and wake on stop.
+ Native backends retain their configured notification wait interval.
+ """
try:
while not stop_event.is_set():
span = _start_event_span(
self._runtime, "dequeue", self._backend_name, self._adapter_name, channel, mode="sync"
)
try:
- event = self._backend.dequeue(channel, poll_interval)
+ if isinstance(self._backend, SyncTableEventQueue):
+ event = self._backend.dequeue(channel, poll_interval, stop_event=stop_event)
+ else:
+ event = self._backend.dequeue(channel, poll_interval)
except Exception as error:
_end_event_span(self._runtime, span, error=error)
raise
diff --git a/sqlspec/extensions/events/_queue.py b/sqlspec/extensions/events/_queue.py
index 5a807e1d5..6c76eb4bd 100644
--- a/sqlspec/extensions/events/_queue.py
+++ b/sqlspec/extensions/events/_queue.py
@@ -21,6 +21,7 @@
if TYPE_CHECKING:
from collections.abc import Sequence
from contextlib import AbstractAsyncContextManager, AbstractContextManager
+ from threading import Event
from sqlspec.config import DatabaseConfigProtocol
from sqlspec.driver import AsyncDriverAdapterBase, SyncDriverAdapterBase
@@ -205,6 +206,7 @@ def _claim_verified(row: "dict[str, Any] | None", leased_until: "datetime") -> b
def _hydrate_event(row: "dict[str, Any]", lease_expires_at: "datetime | None") -> EventMessage:
payload_raw = row.get("payload_json")
metadata_raw = row.get("metadata_json")
+ payload_obj: object
if isinstance(payload_raw, dict):
payload_obj = payload_raw
elif payload_raw is not None:
@@ -281,7 +283,9 @@ def publish_many(self, events: "Sequence[tuple[str, dict[str, Any], dict[str, An
self._runtime.increment_metric("events.publish", len(records))
return event_ids
- def dequeue(self, channel: str, poll_interval: float | None = None) -> "EventMessage | None":
+ def dequeue(
+ self, channel: str, poll_interval: float | None = None, *, stop_event: "Event | None" = None
+ ) -> "EventMessage | None":
attempt = 0
while attempt < self._max_claim_attempts:
attempt += 1
@@ -293,14 +297,20 @@ def dequeue(self, channel: str, poll_interval: float | None = None) -> "EventMes
self._runtime.increment_metric("events.poll.empty")
delay = self._next_empty_poll_delay(channel, poll_interval)
if delay > 0:
- time.sleep(delay)
+ if stop_event is None:
+ time.sleep(delay)
+ else:
+ stop_event.wait(delay)
return None
row = self._fetch_candidate(channel)
if row is None:
self._runtime.increment_metric("events.poll.empty")
delay = self._next_empty_poll_delay(channel, poll_interval)
if delay > 0:
- time.sleep(delay)
+ if stop_event is None:
+ time.sleep(delay)
+ else:
+ stop_event.wait(delay)
return None
now = self._utcnow()
leased_until = now + timedelta(seconds=self._lease_seconds)
diff --git a/tests/conftest.py b/tests/conftest.py
index fa9ff8e7f..d772dae60 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,3 +1,4 @@
+import importlib.machinery
import logging
import os
import warnings
@@ -50,6 +51,11 @@
here = Path(__file__).parent
+COMPILED_EXTENSION_SUFFIXES: tuple[str, ...] = tuple(
+ dict.fromkeys((*importlib.machinery.EXTENSION_SUFFIXES, ".so", ".pyd"))
+)
+
+
def is_compiled() -> bool:
"""Detect if sqlspec driver modules are mypyc-compiled.
@@ -59,11 +65,25 @@ def is_compiled() -> bool:
try:
from sqlspec.driver import _sync
- return hasattr(_sync, "__file__") and (_sync.__file__ or "").endswith(".so")
+ module_file = getattr(_sync, "__file__", None) or ""
+ return bool(module_file and module_file.endswith(COMPILED_EXTENSION_SUFFIXES))
except ImportError:
return False
+def pytest_report_header(config: pytest.Config) -> list[str]:
+ """Report driver module origin and compilation status in pytest header."""
+ _ = config
+ try:
+ from sqlspec.driver import _sync
+
+ module_file = getattr(_sync, "__file__", None) or "unknown"
+ status = "compiled" if is_compiled() else "interpreted"
+ return [f"sqlspec driver: {status} ({module_file})"]
+ except ImportError as err:
+ return [f"sqlspec driver: unavailable ({err})"]
+
+
requires_interpreted = pytest.mark.skipif(
is_compiled(), reason="Test uses interpreted subclass of compiled base (mypyc GC conflict)"
)
@@ -139,23 +159,9 @@ def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item
return
skip_adbc = pytest.mark.skip(reason="Skip ADBC tests when running against mypyc-compiled modules.")
- skip_compiled = pytest.mark.skip(
- reason="Skip tests that rely on interpreted subclasses or mocks of compiled driver bases."
- )
for item in items:
- item_path = str(getattr(item, "path", getattr(item, "fspath", "")))
if item.get_closest_marker("adbc") is not None:
item.add_marker(skip_adbc)
- continue
- if (
- "tests/unit/adapters/" in item_path
- or "tests/unit/driver/" in item_path
- or "tests/unit/extensions/" in item_path
- or item_path.endswith("tests/unit/config/test_storage_capabilities.py")
- or "tests/unit/observability/" in item_path
- ):
- item.add_marker(skip_compiled)
- continue
@pytest.fixture(scope="session", autouse=True)
diff --git a/tests/integration/adapters/cockroach/test_native_storage_benchmark.py b/tests/integration/adapters/cockroach/test_native_storage_benchmark.py
deleted file mode 100644
index f90beac54..000000000
--- a/tests/integration/adapters/cockroach/test_native_storage_benchmark.py
+++ /dev/null
@@ -1,40 +0,0 @@
-"""Local paired native/inherited Parquet benchmark acceptance run."""
-
-import json
-from typing import TYPE_CHECKING
-from urllib.parse import urlsplit, urlunsplit
-
-import pytest
-from tools.scripts.bench_cockroach_storage import run_benchmark
-
-from tests.fixtures.rustfs import rustfs_fsspec_kwargs
-from tests.integration.adapters.cockroach.test_native_storage_capabilities import (
- native_storage_uri as native_storage_uri,
-)
-
-if TYPE_CHECKING:
- from collections.abc import Callable
-
- from pytest_databases.docker.cockroachdb import CockroachDBService
- from pytest_databases.docker.rustfs import RustfsService
-
-
-def test_native_storage_paired_benchmark(
- cockroachdb_service: "CockroachDBService",
- rustfs_service: "RustfsService",
- native_storage_uri: str,
- monkeypatch: pytest.MonkeyPatch,
- record_property: "Callable[[str, object], None]",
-) -> None:
- monkeypatch.setenv(
- "SQLSPEC_COCKROACH_DSN",
- f"host={cockroachdb_service.host} port={cockroachdb_service.port} dbname={cockroachdb_service.database} user=root sslmode=disable",
- )
- monkeypatch.setenv("SQLSPEC_COCKROACH_NATIVE_URI", native_storage_uri)
- monkeypatch.setenv("SQLSPEC_COCKROACH_CLIENT_URI", urlunsplit(urlsplit(native_storage_uri)._replace(query="")))
- monkeypatch.setenv("SQLSPEC_COCKROACH_STORAGE_OPTIONS", json.dumps(rustfs_fsspec_kwargs(rustfs_service)))
- result = run_benchmark([100, 1000, 10000], warmup=1, iterations=3, poll_interval=0.02)
- assert len(result["samples"]) == 18
- assert len(result["summaries"]) == 12
- assert all(item["samples"] == 3 for item in result["summaries"])
- record_property("cockroach_native_benchmark", json.dumps(result))
diff --git a/tests/integration/adapters/duckdb/duckdb/test_native_storage.py b/tests/integration/adapters/duckdb/duckdb/test_native_storage.py
index 60ac5b181..e4f13119d 100644
--- a/tests/integration/adapters/duckdb/duckdb/test_native_storage.py
+++ b/tests/integration/adapters/duckdb/duckdb/test_native_storage.py
@@ -1,7 +1,5 @@
"""Local extension and S3-compatible behavior behind native storage eligibility."""
-import json
-import os
from functools import partial
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
@@ -87,22 +85,6 @@ def reject_arrow(*args: Any, **kwargs: Any) -> Any:
thread.join()
-@pytest.mark.skipif(not os.getenv("SQLSPEC_DUCKDB_STORAGE_BENCHMARK"), reason="Opt-in local storage benchmark")
-def test_native_storage_benchmark(
- rustfs_service: RustfsService, rustfs_bucket_name: str, monkeypatch: pytest.MonkeyPatch
-) -> None:
- from tools.scripts.bench_duckdb_storage import run_benchmark
-
- bucket = ensure_rustfs_bucket(rustfs_service, rustfs_bucket_name)
- monkeypatch.setenv("SQLSPEC_STORAGE_ENDPOINT", f"http://{rustfs_service.endpoint}")
- monkeypatch.setenv("SQLSPEC_STORAGE_BUCKET", bucket)
- monkeypatch.setenv("AWS_ACCESS_KEY_ID", rustfs_service.access_key)
- monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", rustfs_service.secret_key)
- results = run_benchmark()
- Path(os.environ["SQLSPEC_DUCKDB_STORAGE_BENCHMARK"]).write_text(json.dumps(results, indent=2) + "\n")
- assert len(results["results"]) == 3
-
-
@pytest.mark.parametrize("autoload", [False, True])
def test_unavailable_extension_is_distinct_from_object_failure(tmp_path: Path, autoload: bool) -> None:
with duckdb.connect(
diff --git a/tests/integration/adapters/postgres/psqlpy/extensions/adk/test_owner_id_column.py b/tests/integration/adapters/postgres/psqlpy/extensions/adk/test_owner_id_column.py
index 9a83ac7f1..828018112 100644
--- a/tests/integration/adapters/postgres/psqlpy/extensions/adk/test_owner_id_column.py
+++ b/tests/integration/adapters/postgres/psqlpy/extensions/adk/test_owner_id_column.py
@@ -1,6 +1,7 @@
"""Integration tests for Psqlpy ADK store owner_id_column feature."""
-from collections.abc import AsyncGenerator
+import copy
+from collections.abc import AsyncGenerator, Generator
import pytest
@@ -11,22 +12,33 @@
@pytest.fixture
-async def psqlpy_store_with_fk(psqlpy_config: PsqlpyConfig) -> "AsyncGenerator[PsqlpyADKStore, None]":
+def isolated_psqlpy_config(psqlpy_config: PsqlpyConfig) -> Generator[PsqlpyConfig, None, None]:
+ """Provide a test-scoped isolated psqlpy configuration that restores previous state on cleanup."""
+ previous_config = copy.deepcopy(psqlpy_config.extension_config)
+ try:
+ yield psqlpy_config
+ finally:
+ psqlpy_config.extension_config = previous_config
+
+
+@pytest.fixture
+async def psqlpy_store_with_fk(isolated_psqlpy_config: PsqlpyConfig) -> AsyncGenerator[PsqlpyADKStore, None]:
"""Create Psqlpy ADK store with owner_id_column configured."""
- psqlpy_config.extension_config = {
+ isolated_psqlpy_config.extension_config = {
"adk": {
"session_table": "test_sessions_fk_psqlpy",
"events_table": "test_events_fk_psqlpy",
"owner_id_column": "tenant_id INTEGER NOT NULL",
}
}
- store = PsqlpyADKStore(psqlpy_config)
+ store = PsqlpyADKStore(isolated_psqlpy_config)
await store.create_tables()
- yield store
-
- async with psqlpy_config.provide_connection() as conn:
- await conn.execute("DROP TABLE IF EXISTS test_events_fk_psqlpy CASCADE", [])
- await conn.execute("DROP TABLE IF EXISTS test_sessions_fk_psqlpy CASCADE", [])
+ try:
+ yield store
+ finally:
+ async with isolated_psqlpy_config.provide_connection() as conn:
+ await conn.execute("DROP TABLE IF EXISTS test_events_fk_psqlpy CASCADE", [])
+ await conn.execute("DROP TABLE IF EXISTS test_sessions_fk_psqlpy CASCADE", [])
async def test_store_owner_id_column_initialization(psqlpy_store_with_fk: PsqlpyADKStore) -> None:
@@ -35,16 +47,16 @@ async def test_store_owner_id_column_initialization(psqlpy_store_with_fk: Psqlpy
assert psqlpy_store_with_fk.owner_id_column_name == "tenant_id"
-async def test_store_inherits_owner_id_column(psqlpy_config: PsqlpyConfig) -> None:
+async def test_store_inherits_owner_id_column(isolated_psqlpy_config: PsqlpyConfig) -> None:
"""Test that store correctly inherits owner_id_column from base class."""
- psqlpy_config.extension_config = {
+ isolated_psqlpy_config.extension_config = {
"adk": {
"session_table": "test_inherit_psqlpy",
"events_table": "test_events_inherit_psqlpy",
"owner_id_column": "org_id UUID",
}
}
- store = PsqlpyADKStore(psqlpy_config)
+ store = PsqlpyADKStore(isolated_psqlpy_config)
assert hasattr(store, "_owner_id_column_ddl")
assert hasattr(store, "_owner_id_column_name")
@@ -52,12 +64,12 @@ async def test_store_inherits_owner_id_column(psqlpy_config: PsqlpyConfig) -> No
assert store.owner_id_column_name == "org_id"
-async def test_store_without_owner_id_column(psqlpy_config: PsqlpyConfig) -> None:
+async def test_store_without_owner_id_column(isolated_psqlpy_config: PsqlpyConfig) -> None:
"""Test that store works without owner_id_column (default behavior)."""
- psqlpy_config.extension_config = {
+ isolated_psqlpy_config.extension_config = {
"adk": {"session_table": "test_no_fk_psqlpy", "events_table": "test_events_no_fk_psqlpy"}
}
- store = PsqlpyADKStore(psqlpy_config)
+ store = PsqlpyADKStore(isolated_psqlpy_config)
assert store.owner_id_column_ddl is None
assert store.owner_id_column_name is None
diff --git a/tests/integration/adapters/spanner/spanner/test_bytes_direct.py b/tests/integration/adapters/spanner/spanner/test_bytes_direct.py
deleted file mode 100644
index 5d9ba024a..000000000
--- a/tests/integration/adapters/spanner/spanner/test_bytes_direct.py
+++ /dev/null
@@ -1,81 +0,0 @@
-"""Spanner BYTES residual outside the SQLSpec driver contracts.
-
-This test exercises the Google Spanner SDK's base64 parameter convention
-directly. It stays local because the shared contracts assert SQLSpec driver
-materialization, not raw SDK BYTES parameter encoding.
-"""
-
-import base64
-from typing import TYPE_CHECKING
-
-import pytest
-from google.cloud.spanner_v1 import param_types
-
-if TYPE_CHECKING:
- from google.cloud.spanner_v1.database import Database
-
-pytestmark = [pytest.mark.spanner, pytest.mark.integration]
-
-
-def test_bytes_direct_write_read(spanner_database: "Database") -> None:
- """Test bytes roundtrip directly with Spanner using base64 encoding.
-
- The Spanner Python client requires base64-encoded bytes for BYTES parameters.
- This test verifies the correct pattern:
- 1. Write: base64.b64encode(raw_bytes)
- 2. Read: base64.b64decode(stored_bytes)
- """
- database = spanner_database
-
- # Create a simple test table
- try:
- database.update_ddl([ # type: ignore[no-untyped-call]
- """
- CREATE TABLE test_bytes_direct (
- id STRING(128) NOT NULL,
- data BYTES(MAX)
- ) PRIMARY KEY (id)
- """
- ]).result(60)
- except Exception:
- pass
-
- # Clean
- def clean_txn(txn):
- txn.execute_update("DELETE FROM test_bytes_direct WHERE TRUE")
-
- database.run_in_transaction(clean_txn) # type: ignore[no-untyped-call]
-
- # Write using base64-encoded bytes - the correct pattern for Spanner
- test_bytes = b"payload"
- encoded = base64.b64encode(test_bytes)
- write_params = {"id": "test1", "data": encoded}
- write_types = {"id": param_types.STRING, "data": param_types.BYTES}
-
- def insert_txn(txn):
- txn.execute_update(
- "INSERT INTO test_bytes_direct (id, data) VALUES (@id, @data)", params=write_params, param_types=write_types
- )
-
- database.run_in_transaction(insert_txn) # type: ignore[no-untyped-call]
-
- # Read directly - returns base64-encoded bytes
- with database.snapshot() as snap: # type: ignore[no-untyped-call]
- result = list(
- snap.execute_sql(
- "SELECT data FROM test_bytes_direct WHERE id = @id",
- params={"id": "test1"},
- param_types={"id": param_types.STRING},
- )
- )
-
- assert len(result) == 1
- stored_bytes = result[0][0]
-
- # Decode the base64-encoded bytes back to raw bytes
- if isinstance(stored_bytes, (bytes, str)):
- retrieved = base64.b64decode(stored_bytes)
- else:
- retrieved = stored_bytes
-
- assert retrieved == test_bytes, f"Expected {test_bytes!r}, got {retrieved!r}"
diff --git a/tests/integration/cli/__init__.py b/tests/integration/cli/__init__.py
new file mode 100644
index 000000000..94742f55e
--- /dev/null
+++ b/tests/integration/cli/__init__.py
@@ -0,0 +1 @@
+"""Integration tests for CLI commands."""
diff --git a/tests/integration/cli/test_migration_quickstart.py b/tests/integration/cli/test_migration_quickstart.py
new file mode 100644
index 000000000..f3a2933ea
--- /dev/null
+++ b/tests/integration/cli/test_migration_quickstart.py
@@ -0,0 +1,150 @@
+"""Integration tests for SQLSpec migration CLI configuration and workflows."""
+
+import sqlite3
+import sys
+from collections.abc import Iterator
+from pathlib import Path
+
+import pytest
+from click.testing import CliRunner
+
+from sqlspec.cli import add_migration_commands
+
+SQLITE_MIGRATION_CONFIG = """
+from sqlspec.adapters.sqlite import SqliteConfig
+
+database_config = SqliteConfig(
+ bind_key="app",
+ connection_config={"database": "app.db"},
+ migration_config={"script_location": "migrations", "version_table_name": "schema_versions"},
+)
+"""
+
+
+@pytest.fixture
+def cli_workspace(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[Path]:
+ """Provide an isolated workspace with a test-owned SQLite configuration module."""
+ (tmp_path / "database.py").write_text(SQLITE_MIGRATION_CONFIG)
+ monkeypatch.chdir(tmp_path)
+ monkeypatch.syspath_prepend(str(tmp_path))
+ monkeypatch.delenv("SQLSPEC_CONFIG", raising=False)
+ yield tmp_path
+ if "database" in sys.modules:
+ del sys.modules["database"]
+
+
+def test_migration_cli_explicit_config_flag(cli_workspace: Path) -> None:
+ """Verify show-config discovers configuration specified via the --config flag."""
+ runner = CliRunner()
+ result = runner.invoke(add_migration_commands(), ["--config", "database:database_config", "show-config"])
+ assert result.exit_code == 0, result.output
+ assert "app" in result.output
+
+
+def test_migration_cli_env_var_config(cli_workspace: Path) -> None:
+ """Verify show-config discovers configuration specified via the SQLSPEC_CONFIG environment variable."""
+ runner = CliRunner()
+ result = runner.invoke(
+ add_migration_commands(), ["show-config"], env={"SQLSPEC_CONFIG": "database.database_config"}
+ )
+ assert result.exit_code == 0, result.output
+ assert "app" in result.output
+
+
+def test_migration_cli_pyproject_discovery(cli_workspace: Path) -> None:
+ """Verify show-config discovers configuration defined in pyproject.toml."""
+ (cli_workspace / "pyproject.toml").write_text('[tool.sqlspec]\nconfig = "database:database_config"\n')
+ runner = CliRunner()
+ result = runner.invoke(add_migration_commands(), ["show-config"])
+ assert result.exit_code == 0, result.output
+ assert "Using config from pyproject.toml" in result.output
+
+
+def test_migration_cli_init(cli_workspace: Path) -> None:
+ """Verify init creates the migrations directory."""
+ runner = CliRunner()
+ result = runner.invoke(add_migration_commands(), ["--config", "database:database_config", "init", "--no-prompt"])
+ assert result.exit_code == 0, result.output
+ assert (cli_workspace / "migrations").is_dir()
+
+
+def test_migration_cli_create_migration(cli_workspace: Path) -> None:
+ """Verify create-migration generates a new migration SQL file."""
+ runner = CliRunner()
+ runner.invoke(add_migration_commands(), ["--config", "database:database_config", "init", "--no-prompt"])
+ result = runner.invoke(
+ add_migration_commands(),
+ ["--config", "database:database_config", "create-migration", "-m", "create users table", "--no-prompt"],
+ )
+ assert result.exit_code == 0, result.output
+ assert len(list((cli_workspace / "migrations").glob("*.sql"))) == 1
+
+
+def test_migration_cli_upgrade(cli_workspace: Path) -> None:
+ """Verify upgrade creates the database file and applies migrations."""
+ runner = CliRunner()
+ runner.invoke(add_migration_commands(), ["--config", "database:database_config", "init", "--no-prompt"])
+ runner.invoke(
+ add_migration_commands(),
+ ["--config", "database:database_config", "create-migration", "-m", "create users table", "--no-prompt"],
+ )
+ result = runner.invoke(add_migration_commands(), ["--config", "database:database_config", "upgrade", "--no-prompt"])
+ assert result.exit_code == 0, result.output
+ assert (cli_workspace / "app.db").is_file()
+
+
+def test_migration_cli_show_current_revision_and_schema_tracking(cli_workspace: Path) -> None:
+ """Verify show-current-revision succeeds and the tracking table exists in SQLite."""
+ runner = CliRunner()
+ runner.invoke(add_migration_commands(), ["--config", "database:database_config", "init", "--no-prompt"])
+ runner.invoke(
+ add_migration_commands(),
+ ["--config", "database:database_config", "create-migration", "-m", "create users table", "--no-prompt"],
+ )
+ runner.invoke(add_migration_commands(), ["--config", "database:database_config", "upgrade", "--no-prompt"])
+ result = runner.invoke(add_migration_commands(), ["--config", "database:database_config", "show-current-revision"])
+ assert result.exit_code == 0, result.output
+ with sqlite3.connect(cli_workspace / "app.db") as connection:
+ tracker_row = connection.execute(
+ "SELECT name FROM sqlite_master WHERE type = ? AND name = ?", ("table", "schema_versions")
+ ).fetchone()
+ assert tracker_row == ("schema_versions",)
+
+
+def test_migration_cli_quickstart_workflow(cli_workspace: Path) -> None:
+ """Verify the complete sequential quickstart workflow as documented."""
+ runner = CliRunner()
+ config_path = "database:database_config"
+
+ cli_result = runner.invoke(add_migration_commands(), ["--config", config_path, "show-config"])
+ assert cli_result.exit_code == 0, cli_result.output
+ assert "app" in cli_result.output
+
+ env_result = runner.invoke(
+ add_migration_commands(), ["show-config"], env={"SQLSPEC_CONFIG": "database.database_config"}
+ )
+ assert env_result.exit_code == 0, env_result.output
+ assert "app" in env_result.output
+
+ (cli_workspace / "pyproject.toml").write_text('[tool.sqlspec]\nconfig = "database:database_config"\n')
+ pyproject_result = runner.invoke(add_migration_commands(), ["show-config"])
+ assert pyproject_result.exit_code == 0, pyproject_result.output
+ assert "Using config from pyproject.toml" in pyproject_result.output
+
+ commands = (
+ ["init", "--no-prompt"],
+ ["create-migration", "-m", "create users table", "--no-prompt"],
+ ["upgrade", "--no-prompt"],
+ ["show-current-revision"],
+ )
+ for command in commands:
+ cmd_result = runner.invoke(add_migration_commands(), ["--config", config_path, *command])
+ assert cmd_result.exit_code == 0, cmd_result.output
+
+ assert (cli_workspace / "app.db").is_file()
+ assert len(list((cli_workspace / "migrations").glob("*.sql"))) == 1
+ with sqlite3.connect(cli_workspace / "app.db") as connection:
+ tracker_name = connection.execute(
+ "SELECT name FROM sqlite_master WHERE type = ? AND name = ?", ("table", "schema_versions")
+ ).fetchone()
+ assert tracker_name == ("schema_versions",)
diff --git a/tests/unit/adapters/test_adbc/test_core.py b/tests/unit/adapters/test_adbc/test_core.py
index e69eeda00..5a6aeef78 100644
--- a/tests/unit/adapters/test_adbc/test_core.py
+++ b/tests/unit/adapters/test_adbc/test_core.py
@@ -3,6 +3,7 @@
from collections.abc import Sequence
from types import SimpleNamespace
from typing import Any, cast
+from uuid import UUID
from adbc_driver_manager import AdbcStatusCode, DatabaseError
@@ -14,6 +15,7 @@
get_statement_config,
prepare_parameters_with_casts,
prepare_postgres_parameters,
+ prepare_postgres_uuid_bindings,
resolve_column_names,
resolve_many_rowcount,
)
@@ -357,3 +359,13 @@ def test_pg_textsearch_class_and_instance_resolve_as_postgres_family() -> None:
for dialect in (PGTextSearch, PGTextSearch()):
name = adbc_core.resolve_dialect_name(dialect)
assert adbc_core.is_postgres_dialect(name)
+
+
+def test_adbc_postgres_uuid_binding_runs_across_mypyc_boundary() -> None:
+ """The compiled ADBC core should retain UUID rewrite behavior."""
+ value = UUID("550e8400-e29b-41d4-a716-446655440000")
+
+ sql, parameters = prepare_postgres_uuid_bindings("SELECT $1", [value], is_many=False, dialect="postgres")
+
+ assert sql == "SELECT CAST($1 AS UUID)"
+ assert parameters == [str(value)]
diff --git a/tests/unit/adapters/test_bigquery/test_storage_benchmark.py b/tests/unit/adapters/test_bigquery/test_storage_benchmark.py
deleted file mode 100644
index 0f662003a..000000000
--- a/tests/unit/adapters/test_bigquery/test_storage_benchmark.py
+++ /dev/null
@@ -1,107 +0,0 @@
-"""Local harness contracts; fake timings do not measure provider performance."""
-
-from contextlib import contextmanager
-from pathlib import Path
-from types import SimpleNamespace
-from typing import Any, cast
-from unittest.mock import Mock
-
-import pytest
-from tools.scripts.bench_bigquery_storage import QUERY, characterize_ingest, run_benchmark
-
-
-def test_benchmark_controls_sizes_accounting_and_closes_resources() -> None:
- calls: list[tuple[bool, int]] = []
- closed: list[bool] = []
-
- @contextmanager
- def factory(native: bool) -> Any:
- def export(query: str, destination: str, parameters: dict[str, int], **kwargs: Any) -> Any:
- assert query == QUERY
- assert kwargs["format_hint"] == "parquet"
- calls.append((native, parameters["row_count"]))
- telemetry: dict[str, Any] = {"destination": destination, "extra": {"native_export": native}}
- if not native:
- telemetry["bytes_processed"] = 42
- return SimpleNamespace(telemetry=telemetry)
-
- try:
- yield cast(Any, SimpleNamespace(select_to_storage=export))
- finally:
- closed.append(native)
-
- records = run_benchmark(factory, destination="gs://bucket/bench", environment="fake-contract", repetitions=2)
- assert len(records) == 12
- assert len(calls) == 18
- assert closed == [True, False]
- assert {record["input_rows"] for record in records} == {100, 1000, 10000}
- for record in records:
- assert record["status"] == "ok"
- assert record["scenario"] == "bigquery_storage_export"
- assert record["environment"] == "fake-contract"
- assert record["total_s"] >= 0
- assert record["cloud_performance_verified"] is False
- if record["route"] == "native":
- assert "output_bytes" not in record
- else:
- assert record["output_bytes"] == 42
- assert record["bytes_source"] == "storage_telemetry"
-
-
-@pytest.mark.parametrize("mode", ["exception", "fallback", "missing", "bad_bytes"])
-def test_benchmark_reports_failure_or_unsupported_without_successful_native_timing(mode: str) -> None:
- closed: list[bool] = []
-
- @contextmanager
- def factory(native: bool) -> Any:
- telemetry: dict[str, Any] = {"destination": "gs://bucket/out", "extra": {"native_export": native}}
- if mode == "fallback":
- telemetry["extra"]["native_export"] = False
- elif mode == "missing":
- telemetry.pop("destination")
- elif mode == "bad_bytes":
- telemetry["bytes_processed"] = "unknown"
- export = Mock(return_value=SimpleNamespace(telemetry=telemetry))
- if mode == "exception":
- export.side_effect = RuntimeError("native job failed")
- try:
- yield cast(Any, SimpleNamespace(select_to_storage=export))
- finally:
- closed.append(native)
-
- records = run_benchmark(factory, destination="gs://bucket/bench", environment="fake-contract", warmup=0)
- assert closed == [True, False]
- native_records = [record for record in records if record["route"] == "native"]
- assert all(record["status"] == ("unsupported" if mode == "fallback" else "failed") for record in native_records)
- if mode == "fallback":
- assert all("total_s" not in record for record in native_records)
-
-
-@pytest.mark.parametrize("fail", [False, True])
-def test_ingest_characterization_counts_upload_payloads_and_cleans_landing(fail: bool) -> None:
- paths: list[Path] = []
- uploads: list[tuple[str, int]] = []
-
- def consume(route: str, source: bytes | Path) -> None:
- if isinstance(source, Path):
- paths.append(source)
- uploads.append((route, len(source.read_bytes())))
- if fail:
- raise RuntimeError("simulated URI load failure")
- else:
- uploads.append((route, len(source)))
-
- if fail:
- with pytest.raises(RuntimeError, match="simulated URI load failure"):
- characterize_ingest(consume=consume)
- else:
- records = characterize_ingest(consume=consume)
- assert [record["input_rows"] for record in records] == [100, 1000, 10000]
- assert len(uploads) == 6
- for index, record in enumerate(records):
- assert record["encoded_bytes"] == uploads[index * 2][1] == uploads[index * 2 + 1][1]
- assert record["direct_client_upload_bytes"] == record["landing_client_upload_bytes"]
- assert record["encoding_s"] >= 0
- assert record["cloud_performance_verified"] is False
- assert paths
- assert all(not path.exists() and not path.parent.exists() for path in paths)
diff --git a/tests/unit/adapters/test_cockroach_native_storage.py b/tests/unit/adapters/test_cockroach_native_storage.py
index 17c22d0d5..c4eeb66e4 100644
--- a/tests/unit/adapters/test_cockroach_native_storage.py
+++ b/tests/unit/adapters/test_cockroach_native_storage.py
@@ -277,23 +277,6 @@ async def test_native_export_resolves_real_remote_and_local_aliases(
native_driver.fallback.assert_called_once()
-def test_native_benchmark_cli_omits_failure_credentials(
- monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
-) -> None:
- from unittest.mock import Mock
-
- from tools.scripts import bench_cockroach_storage
-
- monkeypatch.setattr("sys.argv", ["bench_cockroach_storage"])
- monkeypatch.setattr(bench_cockroach_storage, "run_benchmark", Mock(side_effect=RuntimeError("secret-access-key")))
- with pytest.raises(SystemExit) as exc:
- bench_cockroach_storage.main()
- assert exc.value.code == 1
- output = capsys.readouterr()
- assert "RuntimeError" in output.err
- assert "secret-access-key" not in output.err
-
-
@pytest.mark.parametrize("query", ["SELECT 1 -- trailing comment", "SELECT ';' AS value -- trailing comment"])
async def test_native_export_preserves_comments_and_literal_delimiters(native_driver: Any, query: str) -> None:
native_driver.execute.return_value = [{"filename": "out.parquet", "rows": 1, "bytes": 10}]
diff --git a/tests/unit/adapters/test_psqlpy/test_core.py b/tests/unit/adapters/test_psqlpy/test_core.py
index 41f281318..cd4628b29 100644
--- a/tests/unit/adapters/test_psqlpy/test_core.py
+++ b/tests/unit/adapters/test_psqlpy/test_core.py
@@ -212,16 +212,6 @@ def test_format_table_identifier_preserves_quoted_dots() -> None:
)
-def test_no_lazy_optional_dependency_getter_functions_in_psqlpy_core() -> None:
- assert not hasattr(psqlpy_core, "_get_jsonb_type")
- assert not hasattr(psqlpy_core, "_librt_string_writer_type")
-
-
-def test_no_optional_dependency_resolved_sentinel_flags_in_psqlpy_core() -> None:
- assert not hasattr(psqlpy_core, "_JSONB_RESOLVED")
- assert not hasattr(psqlpy_core, "_STRING_WRITER_RESOLVED")
-
-
def test_optional_dependency_globals_are_resolved_at_import_time() -> None:
assert hasattr(psqlpy_core, "_JSONB_TYPE")
assert hasattr(psqlpy_core, "_STRING_WRITER_TYPE")
@@ -272,16 +262,6 @@ class MyInt(int):
assert prepared == [5]
-def test_psqlpy_driver_no_longer_caches_output_converter() -> None:
- """The driver should no longer construct the dead psqlpy output converter."""
- import sqlspec.adapters.psqlpy.driver as psqlpy_driver
- import sqlspec.adapters.psqlpy.type_converter as psqlpy_type_converter
-
- assert not hasattr(psqlpy_driver, "_type_converter")
- assert not hasattr(psqlpy_type_converter, "PostgreSQLOutputConverter")
- assert "PostgreSQLOutputConverter" not in psqlpy_type_converter.__all__
-
-
def test_prepare_parameters_with_casts_supports_virtual_abc_dispatch() -> None:
statement_config = build_statement_config()
statement_config = statement_config.replace(
diff --git a/tests/unit/builder/test_dialect_override.py b/tests/unit/builder/test_dialect_override.py
index 0c512211c..25e609fd7 100644
--- a/tests/unit/builder/test_dialect_override.py
+++ b/tests/unit/builder/test_dialect_override.py
@@ -1,7 +1,5 @@
"""Unit tests for build() and to_sql() dialect override parameter."""
-from pathlib import Path
-
from sqlspec import sql
from sqlspec.builder import Column
from sqlspec.core import StatementConfig
@@ -159,7 +157,6 @@ def test_oracle_lock_target_rendering_does_not_use_builder_string_cleanup() -> N
query = sql.select("id", dialect="oracle").from_("job", alias="j").for_update(of="j")
assert "FOR UPDATE OF j" in query.build().sql
- assert "_strip_lock_identifier_quotes" not in Path("sqlspec/builder/_base.py").read_text()
def test_to_sql_dialect_override_with_complex_query() -> None:
diff --git a/tests/unit/builder/test_lateral_joins.py b/tests/unit/builder/test_lateral_joins.py
index d037366e2..3cc92ad52 100644
--- a/tests/unit/builder/test_lateral_joins.py
+++ b/tests/unit/builder/test_lateral_joins.py
@@ -179,7 +179,6 @@ def test_lateral_join_error_conditions() -> None:
def test_lateral_join_parameter_binding() -> None:
"""Test parameter binding in LATERAL joins."""
- # Use simple parameter binding via builder methods instead of sql.raw()
query = sql.select("u.name", "s.value").from_("users u")
subquery = sql.select("value").from_("stats").where_eq("user_id", 123)
query = query.lateral_join(subquery, alias="s")
@@ -188,8 +187,7 @@ def test_lateral_join_parameter_binding() -> None:
assert "LATERAL" in stmt.sql
assert "stats" in stmt.sql.lower()
- # Check for parameter from the where clause
- assert stmt.parameters or True # Parameters may be handled differently
+ assert stmt.parameters == {"user_id": 123}
def test_lateral_join_types_coverage() -> None:
diff --git a/tests/unit/builder/test_sqlglot_arg_contracts.py b/tests/unit/builder/test_sqlglot_arg_contracts.py
deleted file mode 100644
index 1365c0c5e..000000000
--- a/tests/unit/builder/test_sqlglot_arg_contracts.py
+++ /dev/null
@@ -1,196 +0,0 @@
-"""Static contract between sqlspec and sqlglot expression argument names.
-
-sqlglot's ``Expression.set()`` and constructor kwargs accept any key, but the
-generator only visits keys present in the expression class's ``arg_types`` —
-an unknown key is silently dropped from rendered SQL. This suite scans the
-package source so that every argument sqlspec passes to a sqlglot expression
-is provably visible to the generator.
-
-Constructor kwargs are validated automatically. ``.set()`` receivers assigned
-from an ``exp.(...)`` constructor in the same function are validated
-against that class; every other ``.set()`` site must be registered in
-``KNOWN_SET_SITES`` after manually verifying the key against the receiver's
-runtime class. Adding an unregistered ``.set()`` call fails this suite by
-design.
-"""
-
-import ast
-from collections.abc import Iterator
-from pathlib import Path
-
-import sqlglot.expressions as sge
-
-import sqlspec
-
-PACKAGE_ROOT = Path(sqlspec.__file__).parent
-
-EXP_MODULE_ALIASES = frozenset({"exp", "expressions", "sge"})
-
-KNOWN_SET_SITES: frozenset[tuple[str, str, str]] = frozenset({
- ("sqlspec/adapters/bigquery/core.py", "statement_values", "expressions"),
- ("sqlspec/adapters/duckdb/core.py", "part", "quoted"),
- ("sqlspec/builder/_base.py", "cte_duck_expression", "alias"),
- ("sqlspec/builder/_base.py", "cte_expression", "alias"),
- ("sqlspec/builder/_base.py", "cte_select_expression", "alias"),
- ("sqlspec/builder/_base.py", "expression", "conflict"),
- ("sqlspec/builder/_base.py", "final_expression", "with_"),
- ("sqlspec/builder/_base.py", "final_expression.args['with_']", "recursive"),
- ("sqlspec/builder/_base.py", "lock", "sqlspec_share_mode"),
- ("sqlspec/builder/_base.py", "node", "quoted"),
- ("sqlspec/builder/_base.py", "optimized", "conflict"),
- ("sqlspec/builder/_dml.py", "current_expr", "expression"),
- ("sqlspec/builder/_dml.py", "current_expr", "expressions"),
- ("sqlspec/builder/_dml.py", "current_expr", "from_"),
- ("sqlspec/builder/_dml.py", "current_expr", "this"),
- ("sqlspec/builder/_insert.py", "insert_expr", "conflict"),
- ("sqlspec/builder/_join.py", "inner_table", "alias"),
- ("sqlspec/builder/_join.py", "inner_table", "version"),
- ("sqlspec/builder/_join.py", "join_expr", "kind"),
- ("sqlspec/builder/_join.py", "join_expr", "side"),
- ("sqlspec/builder/_join.py", "join_expr", "this"),
- ("sqlspec/builder/_merge.py", "current_expr", "on"),
- ("sqlspec/builder/_merge.py", "current_expr", "this"),
- ("sqlspec/builder/_merge.py", "current_expr", "using"),
- ("sqlspec/builder/_merge.py", "current_expr", "whens"),
- ("sqlspec/builder/_merge.py", "source", "alias"),
- ("sqlspec/builder/_merge.py", "table_expr", "this"),
- ("sqlspec/builder/_merge.py", "then_expr", "where"),
- ("sqlspec/builder/_merge.py", "when_expr", "condition"),
- ("sqlspec/builder/_select.py", "modified_expr", "hint"),
- ("sqlspec/builder/_select.py", "select_expr", "distinct"),
- ("sqlspec/builder/_select.py", "select_expr", "expressions"),
- ("sqlspec/builder/_select.py", "select_expr", "locks"),
- ("sqlspec/builder/_select.py", "self._expression", "returning"),
- ("sqlspec/builder/_select.py", "table", "pivots"),
- ("sqlspec/builder/_select.py", "where_clause", "this"),
- ("sqlspec/builder/_temporal.py", "table_expr", "version"),
- ("sqlspec/builder/_values.py", "expression", "with_"),
- ("sqlspec/core/query_modifiers.py", "existing_where", "this"),
- ("sqlspec/core/query_modifiers.py", "expression", "expressions"),
- ("sqlspec/core/query_modifiers.py", "result", "with_"),
- ("sqlspec/core/query_modifiers.py", "working_expr", "with_"),
- ("sqlspec/dialects/spanner/_generators.py", "properties", "expressions"),
- ("sqlspec/dialects/spanner/_parsers.py", "create", "properties"),
- ("sqlspec/dialects/spanner/_parsers.py", "properties", "expressions"),
- ("sqlspec/driver/_common.py", "count_expr", "from_"),
- ("sqlspec/driver/_common.py", "count_expr", "joins"),
- ("sqlspec/driver/_common.py", "count_expr", "with_"),
- ("sqlspec/driver/_common.py", "count_source", "limit"),
- ("sqlspec/driver/_common.py", "count_source", "offset"),
- ("sqlspec/driver/_common.py", "count_source", "order"),
- ("sqlspec/driver/_common.py", "expr", "with_"),
- ("sqlspec/driver/_common.py", "expr_copy", "with_"),
- ("sqlspec/driver/_common.py", "modified_expr", "with_"),
- ("sqlspec/driver/_common.py", "subquery_expr", "limit"),
- ("sqlspec/driver/_common.py", "subquery_expr", "offset"),
- ("sqlspec/driver/_common.py", "subquery_expr", "order"),
- ("sqlspec/utils/fixtures.py", "insert_expression", "conflict"),
-})
-
-
-def _iter_module_trees() -> "Iterator[tuple[str, ast.Module]]":
- for path in sorted(PACKAGE_ROOT.rglob("*.py")):
- yield path.relative_to(PACKAGE_ROOT.parent).as_posix(), ast.parse(path.read_text(), filename=str(path))
-
-
-def _constructor_class(node: ast.Call) -> "type | None":
- func = node.func
- if (
- isinstance(func, ast.Attribute)
- and isinstance(func.value, ast.Name)
- and func.value.id in EXP_MODULE_ALIASES
- and func.attr[:1].isupper()
- ):
- cls = getattr(sge, func.attr, None)
- if isinstance(cls, type) and issubclass(cls, sge.Expression):
- return cls
- return None
-
-
-def test_constructor_kwargs_exist_in_arg_types() -> None:
- violations: list[str] = []
- for rel, tree in _iter_module_trees():
- for node in ast.walk(tree):
- if not isinstance(node, ast.Call):
- continue
- cls = _constructor_class(node)
- if cls is None:
- continue
- violations.extend(
- f"{rel}:{node.lineno} exp.{cls.__name__}({kw.arg}=...) — valid: {sorted(cls.arg_types)}"
- for kw in node.keywords
- if kw.arg is not None and kw.arg not in cls.arg_types
- )
- assert not violations, "constructor kwargs unknown to sqlglot (silently dropped):\n" + "\n".join(violations)
-
-
-def test_set_calls_use_known_arg_keys() -> None:
- hard_violations: list[str] = []
- unregistered: list[str] = []
- for rel, tree in _iter_module_trees():
- for func_node in ast.walk(tree):
- if not isinstance(func_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
- continue
- local_classes: dict[str, type] = {}
- for stmt in ast.walk(func_node):
- if isinstance(stmt, ast.Assign) and isinstance(stmt.value, ast.Call):
- cls = _constructor_class(stmt.value)
- if cls is not None:
- for tgt in stmt.targets:
- if isinstance(tgt, ast.Name):
- local_classes[tgt.id] = cls
- if not (
- isinstance(stmt, ast.Call)
- and isinstance(stmt.func, ast.Attribute)
- and stmt.func.attr == "set"
- and stmt.args
- and isinstance(stmt.args[0], ast.Constant)
- and isinstance(stmt.args[0].value, str)
- ):
- continue
- key = stmt.args[0].value
- recv_src = ast.unparse(stmt.func.value)
- if recv_src.startswith("sql."):
- continue
- recv_cls = local_classes.get(recv_src)
- if recv_cls is not None:
- if key not in recv_cls.arg_types:
- hard_violations.append(
- f"{rel}:{stmt.lineno} {recv_src}.set({key!r}) on exp.{recv_cls.__name__} — "
- f"valid: {sorted(recv_cls.arg_types)}"
- )
- elif _looks_like_sqlglot_receiver(key) and (rel, recv_src, key) not in KNOWN_SET_SITES:
- unregistered.append(f'("{rel}", "{recv_src}", "{key}"),')
- assert not hard_violations, "set() keys unknown to the receiver's arg_types:\n" + "\n".join(hard_violations)
- assert not unregistered, (
- "unregistered .set() sites — verify each key against the receiver's runtime sqlglot class "
- "(the generator silently drops unknown keys), then add the tuple to KNOWN_SET_SITES:\n"
- + "\n".join(sorted(unregistered))
- )
-
-
-def _looks_like_sqlglot_receiver(key: str) -> bool:
- return any(key in cls.arg_types for cls in _all_expression_classes()) or not key.startswith("_")
-
-
-def _all_expression_classes() -> "list[type]":
- return [cls for cls in vars(sge).values() if isinstance(cls, type) and issubclass(cls, sge.Expression)]
-
-
-def test_no_direct_args_subscript_writes() -> None:
- violations: list[str] = []
- for rel, tree in _iter_module_trees():
- for node in ast.walk(tree):
- if not isinstance(node, ast.Assign):
- continue
- violations.extend(
- f"{rel}:{node.lineno} {ast.unparse(tgt)} = ..."
- for tgt in node.targets
- if (
- isinstance(tgt, ast.Subscript)
- and isinstance(tgt.value, ast.Attribute)
- and tgt.value.attr == "args"
- and isinstance(tgt.slice, ast.Constant)
- )
- )
- assert not violations, "direct .args[...] writes bypass arg_types validation:\n" + "\n".join(violations)
diff --git a/tests/unit/docs/test_conf.py b/tests/unit/docs/test_conf.py
deleted file mode 100644
index bb66cb2fc..000000000
--- a/tests/unit/docs/test_conf.py
+++ /dev/null
@@ -1,21 +0,0 @@
-"""Regression tests for Sphinx docs configuration."""
-
-import importlib.util
-from pathlib import Path
-
-
-def _load_docs_conf() -> object:
- docs_conf_path = Path(__file__).resolve().parents[3] / "docs" / "conf.py"
- spec = importlib.util.spec_from_file_location("sqlspec_docs_conf", docs_conf_path)
- assert spec is not None
- assert spec.loader is not None
- module = importlib.util.module_from_spec(spec)
- spec.loader.exec_module(module)
- return module
-
-
-def test_docs_conf_disables_smartquotes() -> None:
- """Rendered examples should preserve straight ASCII quotes."""
- conf = _load_docs_conf()
-
- assert getattr(conf, "smartquotes", None) is False
diff --git a/tests/unit/driver/test_query_cache.py b/tests/unit/driver/test_query_cache.py
index 220850c40..558e84ffe 100644
--- a/tests/unit/driver/test_query_cache.py
+++ b/tests/unit/driver/test_query_cache.py
@@ -868,19 +868,6 @@ def fail_on_worker(*_args: object, **_kwargs: object) -> object:
await aiosqlite_async_driver._execute_cache_hit("INSERT INTO t (id) VALUES (?)", (1,), cached)
-def test_cached_query_and_query_cache_are_final() -> None:
- """@final markers are present for mypyc devirtualization."""
- assert getattr(CachedQuery, "__final__", False) is True
- assert getattr(QueryCache, "__final__", False) is True
-
- cache = QueryCache()
- assert isinstance(cache, QueryCache)
- assert len(cache) == 0
-
- cached = CachedQuery.__new__(CachedQuery)
- assert isinstance(cached, CachedQuery)
-
-
def test_query_cache_lru_eviction_after_final() -> None:
"""QueryCache LRU eviction still works after final/native annotations."""
cache = QueryCache(max_size=2)
diff --git a/tests/unit/extensions/test_events/test_channel_extended.py b/tests/unit/extensions/test_events/test_channel_extended.py
index f1954b152..c8387306e 100644
--- a/tests/unit/extensions/test_events/test_channel_extended.py
+++ b/tests/unit/extensions/test_events/test_channel_extended.py
@@ -3,6 +3,8 @@
import asyncio
import threading
+import time
+from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any, cast
import pytest
@@ -10,7 +12,13 @@
from sqlspec import ObservabilityRuntime
from sqlspec.adapters.sqlite import SqliteConfig
from sqlspec.exceptions import EventChannelError, ImproperConfigurationError
-from sqlspec.extensions.events import AsyncEventChannel, AsyncEventListener, SyncEventChannel, SyncEventListener
+from sqlspec.extensions.events import (
+ AsyncEventChannel,
+ AsyncEventListener,
+ EventMessage,
+ SyncEventChannel,
+ SyncEventListener,
+)
if TYPE_CHECKING:
from sqlspec.config import AsyncDatabaseConfig, SyncDatabaseConfig
@@ -129,22 +137,23 @@ def test_event_channel_rejects_retired_backend_with_migration_guidance(
SyncEventChannel(config)
-def test_event_channel_honors_driver_feature_backend(monkeypatch: pytest.MonkeyPatch, tmp_path) -> None:
- """Adapter driver features select the backend when extension config does not."""
- selected: list[str | None] = []
-
- def capture_backend(config: Any, backend_name: str | None, settings: dict[str, Any], adapter_name: str) -> None:
- selected.append(backend_name)
-
- monkeypatch.setattr("sqlspec.extensions.events._channel.load_native_backend", capture_backend)
+def test_event_channel_honors_driver_feature_backend(caplog: pytest.LogCaptureFixture, tmp_path) -> None:
+ """An unavailable driver-feature backend reports the requested transport."""
config = SqliteConfig(
connection_config={"database": str(tmp_path / "test.db")}, driver_features={"events_backend": "notify"}
)
-
channel = SyncEventChannel(config)
assert channel._backend_name == "poll_queue"
- assert selected == ["notify"]
+ warnings = [record.__dict__["extra_fields"] for record in caplog.records if record.message == "event.listen"]
+ assert warnings == [
+ {
+ "adapter_name": "sqlite",
+ "backend_name": "notify",
+ "fallback_backend": "poll_queue",
+ "status": "backend_unavailable",
+ }
+ ]
def test_event_extension_backend_takes_precedence_over_driver_feature(tmp_path) -> None:
@@ -379,3 +388,111 @@ def test_event_channel_custom_retention_seconds_via_extension(tmp_path) -> None:
backend = channel._backend
assert backend._retention_seconds == 7200
+
+
+class _ControllableSyncBackend:
+ """Mock sync backend for testing listener dequeue interruption and lifecycle."""
+
+ supports_sync = True
+ supports_async = False
+ backend_name = "controllable-sync-test"
+
+ def __init__(self) -> None:
+ self.entered = threading.Event()
+ self.observed_poll_intervals: list[float] = []
+ self.queued_events: list[EventMessage] = []
+ self.acked_ids: list[str] = []
+
+ def dequeue(self, channel: str, poll_interval: float) -> EventMessage | None:
+ self.observed_poll_intervals.append(poll_interval)
+ self.entered.set()
+ if self.queued_events:
+ return self.queued_events.pop(0)
+ time.sleep(poll_interval)
+ return None
+
+ def ack(self, event_id: str) -> None:
+ self.acked_ids.append(event_id)
+
+ def nack(self, event_id: str) -> None:
+ pass
+
+ def shutdown(self) -> None:
+ pass
+
+
+def test_sync_listener_delivers_and_acknowledges(tmp_path) -> None:
+ """Sync listener processes delivered events and acknowledges them before joining."""
+ config = SqliteConfig(connection_config={"database": str(tmp_path / "test.db")})
+ channel = SyncEventChannel(config)
+ backend = _ControllableSyncBackend()
+ now = datetime.now(timezone.utc)
+ event = EventMessage(
+ event_id="evt-101",
+ channel="test_channel",
+ payload={"key": "value"},
+ metadata=None,
+ attempts=0,
+ available_at=now,
+ lease_expires_at=None,
+ created_at=now,
+ )
+ backend.queued_events.append(event)
+ channel._backend = backend
+
+ received: list[EventMessage] = []
+ delivered = threading.Event()
+
+ def handle_event(message: EventMessage) -> None:
+ received.append(message)
+ delivered.set()
+
+ listener = channel.listen("test_channel", handle_event, poll_interval=0.02)
+ assert delivered.wait(timeout=1.0)
+ channel.stop_listener(listener.id)
+
+ assert len(received) == 1
+ assert received[0].event_id == "evt-101"
+ assert backend.acked_ids == ["evt-101"]
+ assert backend.observed_poll_intervals[0] == 0.02
+ assert not listener.thread.is_alive()
+ assert listener.id not in channel._listeners
+
+
+@pytest.mark.parametrize("select_for_update", [False, True])
+def test_sync_table_listener_preserves_idle_poll_interval(
+ monkeypatch: pytest.MonkeyPatch, tmp_path, select_for_update: bool
+) -> None:
+ """Stopping a long idle table poll wakes the listener without extra queries."""
+ from sqlspec.migrations.commands import SyncMigrationCommands
+
+ polled = threading.Event()
+ polls: list[str] = []
+
+ def trace(statement: str) -> None:
+ if statement.startswith("SELECT") and "sqlspec_event_queue" in statement:
+ polls.append(statement)
+ polled.set()
+
+ migrations_dir = tmp_path / "migrations"
+ migrations_dir.mkdir()
+ config = SqliteConfig(
+ connection_config={"database": str(tmp_path / "test.db")},
+ migration_config={"script_location": str(migrations_dir), "include_extensions": ["events"]},
+ driver_features={"on_connection_create": lambda connection: connection.set_trace_callback(trace)},
+ )
+ SyncMigrationCommands(config).upgrade()
+ polls.clear()
+ polled.clear()
+ channel = SyncEventChannel(config)
+ monkeypatch.setattr(channel._backend, "_select_for_update", select_for_update)
+ listener = channel.listen("test_channel", lambda _: None, poll_interval=10.0)
+ try:
+ assert polled.wait(timeout=1.0)
+ # Several old 0.1-second polling windows must not trigger more queries.
+ time.sleep(0.3)
+ assert len(polls) == 1
+ finally:
+ channel.stop_listener(listener.id)
+ config.close_pool()
+ assert not listener.thread.is_alive()
diff --git a/tests/unit/extensions/test_events/test_events_config.py b/tests/unit/extensions/test_events/test_events_config.py
index a9b1809bd..776a2969d 100644
--- a/tests/unit/extensions/test_events/test_events_config.py
+++ b/tests/unit/extensions/test_events/test_events_config.py
@@ -50,7 +50,6 @@ def test_listener_queue_capacity_is_accepted_by_poll_queue_store(tmp_path: Path)
("sqlspec.adapters.psqlpy.config", "PsqlpyDriverFeatures"),
)
_POSTGRES_EVENT_BACKENDS = {"notify", "notify_queue", "poll_queue"}
-_RETIRED_EVENT_BACKENDS = ("listen_notify", "listen_notify_durable", "table_queue")
@pytest.mark.parametrize(("module_name", "features_name"), _POSTGRES_DRIVER_FEATURES)
@@ -64,21 +63,6 @@ def test_postgres_driver_feature_event_backends_are_canonical_literals(module_na
assert set(get_args(literal)) == _POSTGRES_EVENT_BACKENDS
-def test_active_event_config_prose_uses_canonical_transport_names() -> None:
- """Active adapter config and event reference docs do not advertise retired names."""
- project_root = Path(__file__).parents[4]
- paths = [*project_root.glob("sqlspec/adapters/*/config.py"), project_root / "docs/reference/extensions/events.rst"]
-
- violations = {
- str(path.relative_to(project_root)): retired
- for path in paths
- for retired in _RETIRED_EVENT_BACKENDS
- if retired in path.read_text(encoding="utf-8")
- }
-
- assert violations == {}
-
-
def test_events_extension_auto_includes_migrations(tmp_path) -> None:
"""Configs with events settings auto-include extension migrations."""
diff --git a/tests/unit/extensions/test_events/test_models.py b/tests/unit/extensions/test_events/test_models.py
index 780c4efd0..24db232c3 100644
--- a/tests/unit/extensions/test_events/test_models.py
+++ b/tests/unit/extensions/test_events/test_models.py
@@ -153,14 +153,6 @@ def test_event_message_different_timestamps() -> None:
assert message.available_at < message.lease_expires_at
-def test_event_message_slots_used() -> None:
- """EventMessage uses __slots__ for memory efficiency."""
- assert hasattr(EventMessage, "__slots__")
- assert "event_id" in EventMessage.__slots__
- assert "channel" in EventMessage.__slots__
- assert "payload" in EventMessage.__slots__
-
-
def test_event_message_dataclass_fields() -> None:
"""EventMessage has correct dataclass fields."""
import dataclasses
diff --git a/tests/unit/extensions/test_events/test_mypyc_boundary.py b/tests/unit/extensions/test_events/test_mypyc_boundary.py
deleted file mode 100644
index a55124fa0..000000000
--- a/tests/unit/extensions/test_events/test_mypyc_boundary.py
+++ /dev/null
@@ -1,48 +0,0 @@
-"""Tests for events mypyc boundary decisions."""
-
-import ast
-from pathlib import Path
-
-try:
- import tomllib # type: ignore[import-not-found]
-except ModuleNotFoundError: # pragma: no cover
- import tomli as tomllib
-
-PROJECT_ROOT = Path(__file__).resolve().parents[4]
-EVENTS_PACKAGE = "sqlspec.extensions.events"
-EVENTS_ROOT = PROJECT_ROOT / "sqlspec" / "extensions" / "events"
-
-
-def _events_mypyc_config() -> tuple[set[str], set[str]]:
- pyproject = tomllib.loads((PROJECT_ROOT / "pyproject.toml").read_text())
- config = pyproject["tool"]["hatch"]["build"]["targets"]["wheel"]["hooks"]["mypyc"]
- includes = {path for path in config["include"] if path.startswith("sqlspec/extensions/events/")}
- excludes = {path for path in config["exclude"] if path.startswith("sqlspec/extensions/events/")}
- return includes, excludes
-
-
-def _imported_events_modules(path: Path) -> set[str]:
- tree = ast.parse(path.read_text())
- modules: set[str] = set()
- for node in ast.walk(tree):
- if isinstance(node, ast.ImportFrom) and node.module and node.module.startswith(EVENTS_PACKAGE):
- modules.add(node.module)
- elif isinstance(node, ast.Import):
- modules.update(alias.name for alias in node.names if alias.name.startswith(EVENTS_PACKAGE))
- return modules
-
-
-def test_compiled_events_modules_do_not_import_interpreted_events_modules() -> None:
- """Compiled events helpers should only depend on compiled events siblings."""
- includes, excludes = _events_mypyc_config()
-
- assert "sqlspec/extensions/events/_channel.py" in includes
- assert "sqlspec/extensions/events/_models.py" in includes
- assert "sqlspec/extensions/events/_queue.py" in includes
- assert "sqlspec/extensions/events/primitives.py" in includes
-
- excluded_modules = {f"{EVENTS_PACKAGE}.{Path(path).stem}" for path in excludes if Path(path).name != "__init__.py"}
- allowed_interpreted_imports: set[str] = set()
- for include in includes:
- imported_modules = _imported_events_modules(PROJECT_ROOT / include)
- assert imported_modules.isdisjoint(excluded_modules - allowed_interpreted_imports)
diff --git a/tests/unit/extensions/test_events/test_queue.py b/tests/unit/extensions/test_events/test_queue.py
index 03bb5f910..376f6be4d 100644
--- a/tests/unit/extensions/test_events/test_queue.py
+++ b/tests/unit/extensions/test_events/test_queue.py
@@ -11,6 +11,7 @@
from sqlspec.core.parameters import structural_fingerprint
from sqlspec.exceptions import EventChannelError
from sqlspec.extensions.events import AsyncTableEventQueue, EventMessage, SyncTableEventQueue, parse_event_timestamp
+from tests.conftest import is_compiled
def _event_row(event_id: str = "event-1") -> dict[str, Any]:
@@ -27,9 +28,7 @@ def _event_row(event_id: str = "event-1") -> dict[str, Any]:
}
-def test_table_event_queue_classes_are_final_with_classvar_flags() -> None:
- assert getattr(SyncTableEventQueue, "__final__", False) is True
- assert getattr(AsyncTableEventQueue, "__final__", False) is True
+def test_table_event_queue_backend_capabilities() -> None:
assert SyncTableEventQueue.supports_sync is True
assert SyncTableEventQueue.supports_async is False
assert AsyncTableEventQueue.supports_sync is False
@@ -38,6 +37,7 @@ def test_table_event_queue_classes_are_final_with_classvar_flags() -> None:
assert AsyncTableEventQueue.backend_name == "poll_queue"
+@pytest.mark.skipif(is_compiled(), reason="mypyc direct method calls bypass queue method monkeypatches")
def test_sync_table_queue_empty_poll_backoff_is_bounded_and_resets(
monkeypatch: pytest.MonkeyPatch, tmp_path: Any
) -> None:
@@ -59,6 +59,8 @@ def test_sync_table_queue_empty_poll_backoff_is_bounded_and_resets(
assert sleeps == [0.08, 0.08, 0.01, 0.08]
+@pytest.mark.skipif(is_compiled(), reason="mypyc direct method calls bypass queue method monkeypatches")
+@pytest.mark.anyio
async def test_async_table_queue_empty_poll_backoff_is_bounded_and_resets(
monkeypatch: pytest.MonkeyPatch, tmp_path: Any
) -> None:
diff --git a/tests/unit/test_bench_oracle_scenarios.py b/tests/unit/test_bench_oracle_scenarios.py
deleted file mode 100644
index 640b76230..000000000
--- a/tests/unit/test_bench_oracle_scenarios.py
+++ /dev/null
@@ -1,50 +0,0 @@
-"""Unit coverage for Oracle JSON benchmark scenario registration."""
-
-from collections.abc import Callable
-
-import pytest
-from tools.scripts import bench
-
-
-def test_oracle_json_scenarios_are_registered() -> None:
- """Every Oracle JSON benchmark resolves to its public callable."""
- expected: dict[tuple[str, str, str], Callable[[], None]] = {
- ("sqlspec_native_json", "oracle", "json_write"): bench.sqlspec_oracle_native_json_write,
- ("sqlspec_serialized_json", "oracle", "json_write"): bench.sqlspec_oracle_serialized_json_write,
- ("sqlspec", "oracle", "json_read"): bench.sqlspec_oracle_json_read,
- }
-
- for key, scenario in expected.items():
- assert bench.SCENARIO_REGISTRY[key] is scenario
- assert (key[0], key[2]) in bench.ORACLE_EXTENDED_SCENARIOS
-
-
-def test_oracle_json_rows_distinguish_native_and_serialized_payloads() -> None:
- """Native and serialized writes use equivalent payloads with distinct bind types."""
- native_rows = bench._oracle_json_rows(serialized=False)
- serialized_rows = bench._oracle_json_rows(serialized=True)
-
- assert len(native_rows) == len(serialized_rows) == bench.ORACLE_JSON_ROWS
- assert isinstance(native_rows[0][1], dict)
- assert isinstance(serialized_rows[0][1], str)
- assert native_rows[0][1] == bench.ORACLE_JSON_PAYLOAD
-
-
-def test_oracle_json_public_wrappers_delegate(monkeypatch: "pytest.MonkeyPatch") -> None:
- """Public scenarios select the intended write/read runner mode."""
- calls: list[tuple[str, bool | None]] = []
-
- def fake_write(*, serialized: bool) -> None:
- calls.append(("write", serialized))
-
- def fake_read() -> None:
- calls.append(("read", None))
-
- monkeypatch.setattr(bench, "_run_sqlspec_oracle_json_write", fake_write)
- monkeypatch.setattr(bench, "_run_sqlspec_oracle_json_read", fake_read)
-
- bench.sqlspec_oracle_native_json_write()
- bench.sqlspec_oracle_serialized_json_write()
- bench.sqlspec_oracle_json_read()
-
- assert calls == [("write", False), ("write", True), ("read", None)]
diff --git a/tests/unit/test_docs_extras_parity.py b/tests/unit/test_docs_extras_parity.py
deleted file mode 100644
index edd16e0d7..000000000
--- a/tests/unit/test_docs_extras_parity.py
+++ /dev/null
@@ -1,63 +0,0 @@
-"""Installation docs parity tests for optional extras."""
-
-import re
-import sys
-from pathlib import Path
-
-if sys.version_info >= (3, 11):
- import tomllib
-else: # pragma: no cover
- import tomli as tomllib
-
-
-PROJECT_ROOT = Path(__file__).resolve().parents[2]
-
-
-def _normalize_dependency_name(requirement: str) -> str:
- requirement = requirement.split(";", 1)[0].strip()
- return re.split(r"[<>=!~]", requirement, maxsplit=1)[0].strip()
-
-
-def _package_groups_table() -> str:
- docs = (PROJECT_ROOT / "docs/getting_started/installation.rst").read_text()
- _, table = docs.split("Package groups\n--------------", maxsplit=1)
- return table.split("Multiple extras\n---------------", maxsplit=1)[0]
-
-
-def _documented_extras() -> dict[str, set[str]]:
- table = _package_groups_table()
- rows: dict[str, set[str]] = {}
- matches = list(re.finditer(r"^\s+\* - ``([^`]+)``\s*$", table, flags=re.MULTILINE))
-
- for index, match in enumerate(matches):
- extra_name = match.group(1)
- next_start = matches[index + 1].start() if index + 1 < len(matches) else len(table)
- row = table[match.end() : next_start]
- includes_match = re.search(r"^\s+- (.+)$", row, flags=re.MULTILINE)
- if includes_match is None:
- rows[extra_name] = set()
- continue
- rows[extra_name] = {
- _normalize_dependency_name(dependency) for dependency in re.findall(r"``([^`]+)``", includes_match.group(1))
- }
-
- return rows
-
-
-def _documented_extra_names() -> list[str]:
- return re.findall(r"^\s+\* - ``([^`]+)``\s*$", _package_groups_table(), flags=re.MULTILINE)
-
-
-def _pyproject_extras() -> dict[str, set[str]]:
- pyproject = tomllib.loads((PROJECT_ROOT / "pyproject.toml").read_text())
- return {
- extra_name: {_normalize_dependency_name(dependency) for dependency in dependencies}
- for extra_name, dependencies in pyproject["project"]["optional-dependencies"].items()
- }
-
-
-def test_installation_docs_extras_match_pyproject_optional_dependencies() -> None:
- """Every public optional extra should appear exactly once in installation docs."""
- documented_names = _documented_extra_names()
- assert len(documented_names) == len(set(documented_names))
- assert _documented_extras() == _pyproject_extras()
diff --git a/tests/unit/tools/test_no_future_annotations.py b/tests/unit/tools/test_no_future_annotations.py
deleted file mode 100644
index 0ff58e636..000000000
--- a/tests/unit/tools/test_no_future_annotations.py
+++ /dev/null
@@ -1,52 +0,0 @@
-from pathlib import Path
-
-import pytest
-from tools.hooks.no_future_annotations import main
-
-
-@pytest.mark.parametrize("source", ["value: int = 1\n", "from __future__ import generator_stop\n"])
-def test_clean_python_files_pass(tmp_path: Path, source: str) -> None:
- path = tmp_path / "clean.py"
- path.write_text(source, encoding="utf-8")
-
- assert main([str(path)]) == 0
-
-
-@pytest.mark.parametrize(
- "source", ["from __future__ import annotations\n", "from __future__ import annotations, generator_stop\n"]
-)
-def test_future_annotations_imports_fail(tmp_path: Path, capsys: pytest.CaptureFixture[str], source: str) -> None:
- path = tmp_path / "future.py"
- path.write_text(source, encoding="utf-8")
-
- assert main([str(path)]) == 1
- assert str(path) in capsys.readouterr().err
-
-
-def test_syntax_error_falls_back_to_text_detection(tmp_path: Path) -> None:
- path = tmp_path / "invalid.py"
- path.write_text("from __future__ import annotations\nthis is not valid python !!!\n", encoding="utf-8")
-
- assert main([str(path)]) == 1
-
-
-def test_non_python_files_are_ignored(tmp_path: Path) -> None:
- path = tmp_path / "example.txt"
- path.write_text("from __future__ import annotations\n", encoding="utf-8")
-
- assert main([str(path)]) == 0
-
-
-def test_all_offending_files_are_reported(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
- first = tmp_path / "first.py"
- second = tmp_path / "second.py"
- clean = tmp_path / "clean.py"
- first.write_text("from __future__ import annotations\n", encoding="utf-8")
- second.write_text("from __future__ import annotations, generator_stop\n", encoding="utf-8")
- clean.write_text("value = 1\n", encoding="utf-8")
-
- assert main([str(first), str(clean), str(second)]) == 1
- error = capsys.readouterr().err
- assert str(first) in error
- assert str(second) in error
- assert str(clean) not in error
diff --git a/tests/unit/utils/test_correlation.py b/tests/unit/utils/test_correlation.py
index 54c764454..3545a1cd1 100644
--- a/tests/unit/utils/test_correlation.py
+++ b/tests/unit/utils/test_correlation.py
@@ -12,7 +12,7 @@
import pytest
-from sqlspec.utils.correlation import CorrelationContext, get_correlation_adapter
+from sqlspec.utils.correlation import CorrelationContext, correlation_context, get_correlation_adapter
def setup_function() -> None:
@@ -462,3 +462,25 @@ def test_generate_produces_valid_uuids() -> None:
parsed_uuid = uuid.UUID(correlation_id)
assert str(parsed_uuid) == correlation_id
+
+
+def test_correlation_context_function_is_public() -> None:
+ """correlation_context is a public helper that manages correlation IDs."""
+ import sqlspec.utils.correlation as correlation_module
+
+ assert "correlation_context" in correlation_module.__all__
+ assert hasattr(correlation_module, "correlation_context")
+ with correlation_context("request-id") as correlation_id:
+ assert correlation_id == "request-id"
+ assert CorrelationContext.get() == "request-id"
+ assert CorrelationContext.get() is None
+
+
+def test_correlation_context_with_generated_id() -> None:
+ """correlation_context generates a UUID when called with no ID."""
+ with correlation_context() as correlation_id:
+ assert isinstance(correlation_id, str)
+ assert len(correlation_id) > 0
+ assert CorrelationContext.get() == correlation_id
+ uuid.UUID(correlation_id)
+ assert CorrelationContext.get() is None
diff --git a/tests/unit/utils/test_makefile_quality.py b/tests/unit/utils/test_makefile_quality.py
deleted file mode 100644
index e3803b5a3..000000000
--- a/tests/unit/utils/test_makefile_quality.py
+++ /dev/null
@@ -1,32 +0,0 @@
-"""Tests for Makefile quality-gate safety."""
-
-import re
-from pathlib import Path
-
-PROJECT_ROOT = Path(__file__).resolve().parents[3]
-
-
-def test_oneshell_recipes_fail_fast() -> None:
- """Makefile recipes should stop when an inner command fails."""
- makefile = (PROJECT_ROOT / "Makefile").read_text()
-
- assert ".ONESHELL:" in makefile
- shellflags_match = re.search(r"^\.SHELLFLAGS\s*:?=\s*(?P.+)$", makefile, flags=re.MULTILINE)
- assert shellflags_match is not None
- flags = shellflags_match.group("flags")
- assert "-e" in flags or "-eu" in flags or "-euo" in flags
- assert "-o pipefail" in flags
-
-
-def test_default_mypy_target_uses_parallel_checking() -> None:
- """The default mypy gate should use the faster mypy 2 parallel checker."""
- makefile = (PROJECT_ROOT / "Makefile").read_text()
-
- mypy_target_match = re.search(r"^mypy:.*?(?=^\S)", makefile, flags=re.MULTILINE | re.DOTALL)
- assert mypy_target_match is not None
- mypy_target = mypy_target_match.group(0)
- assert "uv run mypy" in mypy_target
- assert "-n $(MYPY_WORKERS)" in mypy_target
- assert "uv run dmypy" not in mypy_target
-
- assert re.search(r"^dmypy:.*?## Run mypy daemon", makefile, flags=re.MULTILINE) is not None
diff --git a/tests/unit/utils/test_mypyc_inventory.py b/tests/unit/utils/test_mypyc_inventory.py
deleted file mode 100644
index 351f9585e..000000000
--- a/tests/unit/utils/test_mypyc_inventory.py
+++ /dev/null
@@ -1,318 +0,0 @@
-"""Tests for mypyc inventory and smoke-gate tooling."""
-
-import ast
-import importlib.util
-import json
-import re
-import subprocess
-import sys
-from pathlib import Path
-from types import ModuleType
-from uuid import UUID
-
-import sqlspec.utils.correlation as correlation_module
-from sqlspec.adapters.adbc.core import prepare_postgres_uuid_bindings
-from sqlspec.utils.correlation import CorrelationContext
-
-if sys.version_info >= (3, 11):
- import tomllib
-else:
- import tomli as tomllib
-PROJECT_ROOT = Path(__file__).resolve().parents[3]
-
-
-def test_adbc_postgres_uuid_binding_runs_across_mypyc_boundary() -> None:
- """The compiled ADBC core should retain UUID rewrite behavior."""
- value = UUID("550e8400-e29b-41d4-a716-446655440000")
-
- sql, parameters = prepare_postgres_uuid_bindings("SELECT $1", [value], is_many=False, dialect="postgres")
-
- assert sql == "SELECT CAST($1 AS UUID)"
- assert parameters == [str(value)]
-
-
-def _load_mypyc_boundary_map_module() -> ModuleType:
- module_path = PROJECT_ROOT / "tools" / "scripts" / "mypyc_boundary_map.py"
- spec = importlib.util.spec_from_file_location("mypyc_boundary_map_for_tests", module_path)
- assert spec is not None
- assert spec.loader is not None
- module = importlib.util.module_from_spec(spec)
- spec.loader.exec_module(module)
- return module
-
-
-def test_inventory_cli_default_json_summary_names_live_surfaces() -> None:
- """The inventory CLI should emit stable machine-readable output."""
- script_path = PROJECT_ROOT / "tools" / "scripts" / "mypyc_inventory.py"
- completed = subprocess.run(
- [sys.executable, str(script_path)], check=True, cwd=PROJECT_ROOT, capture_output=True, text=True
- )
- payload = json.loads(completed.stdout)
- assert payload["summary"]["compiled_count"] > 0
- assert payload["summary"]["interpreted_count"] > 0
- assert (
- payload["summary"]["total_modules"]
- == payload["summary"]["compiled_count"] + payload["summary"]["interpreted_count"]
- )
- assert set(payload["surface_counts"]) == {"candidate", "compiled", "hard_block", "interpreted", "keep_interpreted"}
- assert "sqlspec/utils/serializers.py" not in payload["hot_surfaces"]
- assert all((PROJECT_ROOT / module_path).is_file() for module_path in payload["hot_surfaces"])
-
-
-def test_inventory_cli_markdown_summary_includes_surface_column() -> None:
- """The markdown mode should produce a citation-friendly hot-surface table."""
- script_path = PROJECT_ROOT / "tools" / "scripts" / "mypyc_inventory.py"
- completed = subprocess.run(
- [sys.executable, str(script_path), "--format", "markdown"],
- check=True,
- cwd=PROJECT_ROOT,
- capture_output=True,
- text=True,
- )
- assert "Compiled modules:" in completed.stdout
- assert "| Module | Surface | Status | Classification | Reason |" in completed.stdout
- assert "sqlspec/utils/serializers.py" not in completed.stdout
-
-
-def test_pyproject_mypyc_include_patterns_cover_smoke_critical_modules() -> None:
- """The hatch-mypyc include patterns should keep compiling the smoke-critical modules."""
- script_path = PROJECT_ROOT / "tools" / "scripts" / "mypyc_inventory.py"
- completed = subprocess.run(
- [sys.executable, str(script_path)], check=True, cwd=PROJECT_ROOT, capture_output=True, text=True
- )
- payload = json.loads(completed.stdout)
- smoke_critical_modules = [
- "sqlspec/base.py",
- "sqlspec/utils/text.py",
- "sqlspec/utils/sync_tools.py",
- "sqlspec/utils/env.py",
- "sqlspec/utils/module_loader.py",
- "sqlspec/core/cache.py",
- "sqlspec/core/hashing.py",
- "sqlspec/core/parameters/_processor.py",
- "sqlspec/core/result/_base.py",
- "sqlspec/core/splitter.py",
- "sqlspec/driver/_query_cache.py",
- "sqlspec/adapters/adbc/core.py",
- "sqlspec/adapters/sqlite/core.py",
- "sqlspec/adapters/psqlpy/core.py",
- "sqlspec/adapters/sqlite/pool.py",
- "sqlspec/storage/_paths.py",
- "sqlspec/storage/_utils.py",
- "sqlspec/storage/backends/local.py",
- "sqlspec/storage/backends/fsspec.py",
- "sqlspec/storage/backends/obstore.py",
- "sqlspec/data_dictionary/_loader.py",
- "sqlspec/data_dictionary/dialects/bigquery/config.py",
- "sqlspec/data_dictionary/dialects/cockroachdb/config.py",
- "sqlspec/data_dictionary/dialects/duckdb/config.py",
- "sqlspec/data_dictionary/dialects/mysql/config.py",
- "sqlspec/data_dictionary/dialects/oracle/config.py",
- "sqlspec/data_dictionary/dialects/postgres/config.py",
- "sqlspec/data_dictionary/dialects/spanner/config.py",
- "sqlspec/data_dictionary/dialects/sqlite/config.py",
- "sqlspec/dialects/postgres/_generators.py",
- "sqlspec/dialects/postgres/_operators.py",
- "sqlspec/dialects/spanner/_generators.py",
- "sqlspec/extensions/prometheus/_observer.py",
- "sqlspec/extensions/fastapi/providers.py",
- "sqlspec/extensions/litestar/providers.py",
- "sqlspec/extensions/events/_hints.py",
- "sqlspec/extensions/events/_models.py",
- "sqlspec/extensions/events/_names.py",
- "sqlspec/extensions/events/_payload.py",
- "sqlspec/extensions/events/_channel.py",
- "sqlspec/extensions/events/_queue.py",
- "sqlspec/extensions/adk/_types.py",
- "sqlspec/extensions/adk/memory/_types.py",
- "sqlspec/extensions/adk/artifact/_types.py",
- "sqlspec/migrations/version.py",
- "sqlspec/observability/_formatting.py",
- ]
- assert all((PROJECT_ROOT / path).is_file() for path in smoke_critical_modules)
- assert all(path in payload["compiled_modules"] for path in smoke_critical_modules)
-
-
-def test_inventory_records_rest_of_mypyc_boundary_decisions() -> None:
- """Inventory output should show admitted modules and retained dynamic boundaries."""
- script_path = PROJECT_ROOT / "tools" / "scripts" / "mypyc_inventory.py"
- completed = subprocess.run(
- [sys.executable, str(script_path)], check=True, cwd=PROJECT_ROOT, capture_output=True, text=True
- )
- payload = json.loads(completed.stdout)
- assert "sqlspec/storage/pipeline.py" in payload["compiled_modules"]
- assert "sqlspec/storage/_paths.py" in payload["compiled_modules"]
- assert "sqlspec/storage/_utils.py" in payload["compiled_modules"]
- assert "sqlspec/base.py" in payload["compiled_modules"]
- assert "sqlspec/extensions/prometheus/_observer.py" in payload["compiled_modules"]
- assert "sqlspec/extensions/fastapi/providers.py" in payload["compiled_modules"]
- assert "sqlspec/extensions/litestar/providers.py" in payload["compiled_modules"]
- assert "sqlspec/storage/backends/fsspec.py" in payload["compiled_modules"]
- assert "sqlspec/storage/backends/local.py" in payload["compiled_modules"]
- assert "sqlspec/storage/backends/obstore.py" in payload["compiled_modules"]
- assert "sqlspec/data_dictionary/_loader.py" in payload["compiled_modules"]
- assert "sqlspec/data_dictionary/dialects/bigquery/config.py" in payload["compiled_modules"]
- assert "sqlspec/data_dictionary/dialects/cockroachdb/config.py" in payload["compiled_modules"]
- assert "sqlspec/data_dictionary/dialects/duckdb/config.py" in payload["compiled_modules"]
- assert "sqlspec/data_dictionary/dialects/mysql/config.py" in payload["compiled_modules"]
- assert "sqlspec/data_dictionary/dialects/oracle/config.py" in payload["compiled_modules"]
- assert "sqlspec/data_dictionary/dialects/postgres/config.py" in payload["compiled_modules"]
- assert "sqlspec/data_dictionary/dialects/spanner/config.py" in payload["compiled_modules"]
- assert "sqlspec/data_dictionary/dialects/sqlite/config.py" in payload["compiled_modules"]
- assert "sqlspec/dialects/postgres/_generators.py" in payload["compiled_modules"]
- assert "sqlspec/dialects/postgres/_operators.py" in payload["compiled_modules"]
- assert "sqlspec/dialects/spanner/_generators.py" in payload["compiled_modules"]
- assert "sqlspec/extensions/events/_hints.py" in payload["compiled_modules"]
- assert "sqlspec/extensions/events/_models.py" in payload["compiled_modules"]
- assert "sqlspec/extensions/events/_names.py" in payload["compiled_modules"]
- assert "sqlspec/extensions/events/_payload.py" in payload["compiled_modules"]
- assert "sqlspec/extensions/events/_channel.py" in payload["compiled_modules"]
- assert "sqlspec/extensions/events/_queue.py" in payload["compiled_modules"]
- assert "sqlspec/extensions/adk/_types.py" in payload["compiled_modules"]
- assert "sqlspec/extensions/adk/memory/_types.py" in payload["compiled_modules"]
- assert "sqlspec/extensions/adk/artifact/_types.py" in payload["compiled_modules"]
- assert "sqlspec/migrations/runner.py" in payload["compiled_modules"]
- assert "sqlspec/observability/_formatting.py" in payload["compiled_modules"]
- assert "sqlspec/utils/env.py" in payload["compiled_modules"]
- assert "sqlspec/adapters/asyncpg/driver.py" in payload["interpreted_modules"]
- assert "sqlspec/adapters/psycopg/driver.py" in payload["interpreted_modules"]
- assert "sqlspec/adapters/cockroach_asyncpg/driver.py" in payload["interpreted_modules"]
- assert "sqlspec/adapters/cockroach_psycopg/driver.py" in payload["interpreted_modules"]
- assert "sqlspec/adapters/sqlite/driver.py" in payload["interpreted_modules"]
- assert "sqlspec/adapters/aiosqlite/driver.py" in payload["interpreted_modules"]
- assert "sqlspec/dialects/postgres/_paradedb.py" in payload["interpreted_modules"]
- assert "sqlspec/dialects/postgres/_pgvector.py" in payload["interpreted_modules"]
- assert "sqlspec/dialects/spanner/_spangres.py" in payload["interpreted_modules"]
- assert "sqlspec/dialects/spanner/_spanner.py" in payload["interpreted_modules"]
- assert "sqlspec/extensions/adk/converters.py" in payload["interpreted_modules"]
- assert "sqlspec/storage/_arrow_payload.py" in payload["interpreted_modules"]
- assert "sqlspec/extensions/adk/converters.py" in payload["preserved_exclusions"]
- assert payload["adapter_pool_runtimes"]["status"] == "compiled"
- assert payload["adapter_driver_shells"]["classification"] == "prove_separately"
- assert payload["adapter_driver_shells"]["status"] == "blocked"
- assert "sqlspec/adapters/asyncpg/driver.py" in payload["adapter_driver_shells"]["modules"]
- assert "sqlspec/adapters/psycopg/driver.py" in payload["adapter_driver_shells"]["modules"]
- assert "sqlspec/adapters/cockroach_asyncpg/driver.py" in payload["adapter_driver_shells"]["modules"]
- assert "sqlspec/adapters/cockroach_psycopg/driver.py" in payload["adapter_driver_shells"]["modules"]
-
-
-def test_inventory_records_wave4_candidate_and_hard_block_buckets() -> None:
- """Wave 4 planning surfaces should distinguish promotable candidates from hard blocks."""
- script_path = PROJECT_ROOT / "tools" / "scripts" / "mypyc_inventory.py"
- completed = subprocess.run(
- [sys.executable, str(script_path)], check=True, cwd=PROJECT_ROOT, capture_output=True, text=True
- )
- payload = json.loads(completed.stdout)
- hot_surfaces = payload["hot_surfaces"]
-
- expected_candidates: set[str] = set()
- expected_hard_blocks = {
- "sqlspec/dialects/postgres/_paradedb.py",
- "sqlspec/dialects/postgres/_pgvector.py",
- "sqlspec/dialects/spanner/_spangres.py",
- "sqlspec/dialects/spanner/_spanner.py",
- "sqlspec/storage/_arrow_payload.py",
- "sqlspec/utils/arrow_helpers.py",
- }
-
- for module_path in expected_candidates:
- details = hot_surfaces[module_path]
- assert details["surface"] == "candidate"
- assert details["status"] == "interpreted"
- assert details["reason"]
-
- assert hot_surfaces["sqlspec/base.py"]["surface"] == "compiled"
- assert hot_surfaces["sqlspec/base.py"]["status"] == "compiled"
- assert hot_surfaces["sqlspec/base.py"]["reason"]
- assert hot_surfaces["sqlspec/extensions/prometheus/_observer.py"]["surface"] == "compiled"
- assert hot_surfaces["sqlspec/extensions/prometheus/_observer.py"]["status"] == "compiled"
- assert hot_surfaces["sqlspec/extensions/prometheus/_observer.py"]["reason"]
- assert hot_surfaces["sqlspec/extensions/prometheus/__init__.py"]["surface"] == "keep_interpreted"
- assert hot_surfaces["sqlspec/extensions/prometheus/__init__.py"]["status"] == "interpreted"
- assert hot_surfaces["sqlspec/extensions/prometheus/__init__.py"]["reason"]
- for module_path in {
- "sqlspec/extensions/fastapi/providers.py",
- "sqlspec/extensions/litestar/providers.py",
- "sqlspec/extensions/events/_channel.py",
- "sqlspec/storage/backends/fsspec.py",
- "sqlspec/storage/backends/local.py",
- "sqlspec/storage/backends/obstore.py",
- }:
- assert hot_surfaces[module_path]["surface"] == "compiled"
- assert hot_surfaces[module_path]["status"] == "compiled"
- assert hot_surfaces[module_path]["reason"]
-
- for module_path in expected_hard_blocks:
- details = hot_surfaces[module_path]
- assert details["surface"] == "hard_block"
- assert details["status"] == "interpreted"
- assert details["classification"] == "hard_block"
- assert details["reason"]
-
- assert "sqlspec/_serialization.py" not in hot_surfaces
-
-
-def test_boundary_map_uses_live_wave4_module_edges() -> None:
- """Boundary-map output should not preserve stale pre-Wave-4 module edges."""
- module = _load_mypyc_boundary_map_module()
-
- boundary_map = module.build_boundary_map(PROJECT_ROOT)
- serialized = json.dumps(boundary_map, sort_keys=True)
-
- assert "sqlspec/_serialization.py" not in serialized
- assert any(
- boundary["from_module"] == "sqlspec/storage/pipeline.py"
- and boundary["from_status"] == "compiled"
- and boundary["to_module"] == "sqlspec/storage/_arrow_payload.py"
- and boundary["to_status"] == "interpreted"
- and boundary["classification"] == "compiled_to_interpreted_arrow_boundary"
- for boundary in boundary_map["storage_arrow_boundaries"]
- )
- assert all(
- details["bucket"] != "hard_block"
- for module_path, details in boundary_map["exclusion_revalidation_seed"].items()
- if module_path in {"sqlspec/extensions/events/_models.py", "sqlspec/extensions/events/_queue.py"}
- )
-
-
-def test_event_channel_module_has_no_async_generators() -> None:
- """Event channel iteration should stay compatible with compiled-wheel builds."""
- source_path = PROJECT_ROOT / "sqlspec" / "extensions" / "events" / "_channel.py"
- module_ast = ast.parse(source_path.read_text(), filename=str(source_path))
- async_generators = [
- node.name
- for node in ast.walk(module_ast)
- if isinstance(node, ast.AsyncFunctionDef)
- and any(isinstance(child, (ast.Yield, ast.YieldFrom)) for child in ast.walk(node))
- ]
-
- assert async_generators == []
-
-
-def test_mypy_2_toolchain_policy_is_explicit_and_parallel_gate_is_default() -> None:
- """The mypy 2.0 cutover should keep parallel checking in the default type gate."""
- pyproject = tomllib.loads((PROJECT_ROOT / "pyproject.toml").read_text())
- build_dependencies = pyproject["dependency-groups"]["build"]
- lint_dependencies = pyproject["dependency-groups"]["lint"]
- mypyc_dependencies = pyproject["tool"]["hatch"]["build"]["targets"]["wheel"]["hooks"]["mypyc"]["dependencies"]
- mypy_config = pyproject["tool"]["mypy"]
- assert "mypy>=2.0.0" in build_dependencies
- assert "mypy>=2.0.0" in lint_dependencies
- assert "mypy>=2.0.0" in mypyc_dependencies
- assert mypy_config["local_partial_types"] is True
- assert mypy_config["strict_bytes"] is True
- assert mypy_config["allow_redefinition"] is False
- makefile = (PROJECT_ROOT / "Makefile").read_text()
- assert re.search("^mypy:.*?uv run mypy -n \\$\\(MYPY_WORKERS\\)", makefile, flags=re.MULTILINE | re.DOTALL)
- assert re.search("^dmypy:.*?## Run mypy daemon", makefile, flags=re.MULTILINE) is not None
- assert re.search("^mypy-parallel:.*?##", makefile, flags=re.MULTILINE) is not None
- assert re.search("^type-check:\\s+mypy pyright\\s+##", makefile, flags=re.MULTILINE) is not None
-
-
-def test_correlation_context_function_is_public() -> None:
- """correlation_context is a public helper (imported by downstream consumers)."""
- assert "correlation_context" in correlation_module.__all__
- assert hasattr(correlation_module, "correlation_context")
- with correlation_module.correlation_context("request-id") as correlation_id:
- assert correlation_id == "request-id"
- assert CorrelationContext.get() == "request-id"
diff --git a/tests/unit/utils/test_mypyc_smoke.py b/tests/unit/utils/test_mypyc_smoke.py
deleted file mode 100644
index 0de7eccc5..000000000
--- a/tests/unit/utils/test_mypyc_smoke.py
+++ /dev/null
@@ -1,288 +0,0 @@
-"""Tests for the compiled-wheel smoke matrix."""
-
-import importlib.util
-from collections.abc import Sequence
-from pathlib import Path
-from types import ModuleType
-
-import pytest
-from pytest import MonkeyPatch
-
-try:
- import tomllib # type: ignore[import-not-found]
-except ModuleNotFoundError: # pragma: no cover
- import tomli as tomllib
-
-PROJECT_ROOT = Path(__file__).resolve().parents[3]
-
-
-def _load_mypyc_smoke_module() -> ModuleType:
- module_path = PROJECT_ROOT / "tools" / "scripts" / "mypyc_smoke.py"
- spec = importlib.util.spec_from_file_location("mypyc_smoke_for_tests", module_path)
- assert spec is not None
- assert spec.loader is not None
- module = importlib.util.module_from_spec(spec)
- spec.loader.exec_module(module)
- return module
-
-
-def _load_mypyc_paths(config_key: str) -> set[str]:
- pyproject = tomllib.loads((PROJECT_ROOT / "pyproject.toml").read_text())
- patterns: Sequence[str] = pyproject["tool"]["hatch"]["build"]["targets"]["wheel"]["hooks"]["mypyc"][config_key]
- paths: set[str] = set()
- for pattern in patterns:
- if any(marker in pattern for marker in "*?["):
- paths.update(path.relative_to(PROJECT_ROOT).as_posix() for path in PROJECT_ROOT.glob(pattern))
- else:
- paths.add(pattern)
- return paths
-
-
-def _load_mypyc_include_paths() -> set[str]:
- return _load_mypyc_paths("include")
-
-
-def _load_mypyc_exclude_paths() -> set[str]:
- return _load_mypyc_paths("exclude")
-
-
-def test_smoke_matrix_covers_compiled_wheel_import_surfaces() -> None:
- module = _load_mypyc_smoke_module()
-
- smoke_names = {entry.name for entry in module.SMOKE_IMPORTS}
-
- assert {
- "package",
- "base_sqlspec",
- "prometheus_observer",
- "async_bridge",
- "core_statement",
- "builder_select",
- "env_utils",
- "sync_driver",
- "async_driver",
- "storage_registry",
- "storage_backend_local",
- "storage_backend_fsspec",
- "storage_backend_obstore",
- "data_dictionary_registry",
- "sqlite_type_converter",
- }.issubset(smoke_names)
-
- compiled_required = {entry.name for entry in module.SMOKE_IMPORTS if entry.require_compiled}
- assert compiled_required == {
- "async_driver",
- "adk_record_types",
- "async_bridge",
- "base_sqlspec",
- "builder_select",
- "core_statement",
- "data_dictionary_loader",
- "data_dictionary_registry",
- "env_utils",
- "event_channel",
- "event_payload",
- "event_queue",
- "fastapi_providers",
- "litestar_providers",
- "migration_runner",
- "prometheus_observer",
- "sqlite_pool",
- "sqlite_type_converter",
- "storage_backend_fsspec",
- "storage_backend_local",
- "storage_backend_obstore",
- "storage_registry",
- "storage_pipeline",
- "sync_driver",
- }
-
-
-def test_compiled_smoke_requirements_are_in_mypyc_include_list() -> None:
- module = _load_mypyc_smoke_module()
- included_paths = _load_mypyc_include_paths()
-
- missing = [entry.module.replace(".", "/") + ".py" for entry in module.SMOKE_IMPORTS if entry.require_compiled]
- missing = [path for path in missing if path not in included_paths]
-
- assert missing == []
-
-
-def test_compiled_smoke_requirements_are_not_in_mypyc_exclude_list() -> None:
- module = _load_mypyc_smoke_module()
- excluded_paths = _load_mypyc_exclude_paths()
-
- excluded = [entry.module.replace(".", "/") + ".py" for entry in module.SMOKE_IMPORTS if entry.require_compiled]
- excluded = [path for path in excluded if path in excluded_paths]
-
- assert excluded == []
-
-
-def test_smoke_runner_imports_matrix_without_requiring_compilation() -> None:
- module = _load_mypyc_smoke_module()
-
- results = module.run_smoke(require_compiled=False)
-
- assert all(result["imported"] or result["skipped"] for result in results)
- assert any(result["module"] == "sqlspec.driver._sync" for result in results)
- assert any(result["module"] == "sqlspec.adapters.sqlite.type_converter" for result in results)
- assert any(result["module"] == "sqlspec.storage.pipeline" for result in results)
- assert any(result["module"] == "sqlspec.storage.backends.local" for result in results)
- assert any(result["module"] == "sqlspec.storage.backends.fsspec" for result in results)
- assert any(result["module"] == "sqlspec.storage.backends.obstore" for result in results)
- assert any(result["module"] == "sqlspec.migrations.runner" for result in results)
- assert any(result["module"] == "sqlspec.utils.env" for result in results)
- assert any(result["module"] == "sqlspec.utils.sync_tools" for result in results)
-
-
-def test_construction_checks_build_provider_signatures_without_requiring_compilation() -> None:
- module = _load_mypyc_smoke_module()
-
- results = module.run_construction_checks(require_compiled=False)
-
- assert all(result["imported"] or result["skipped"] for result in results)
- assert {result["name"] for result in results} == {
- "adapter_config_construction",
- "aiosqlite_ambient_exception",
- "aiosqlite_exception_mapping",
- "fastapi_filter_construction",
- "litestar_filter_construction",
- "statement_cache_rebind",
- "statement_sentinel_identity",
- "sqlspec_construction",
- }
- sqlspec_result = next(result for result in results if result["name"] == "sqlspec_construction")
- assert sqlspec_result["error"] is None
- adapter_result = next(result for result in results if result["name"] == "adapter_config_construction")
- assert adapter_result["error"] is None
-
-
-def test_adapter_config_construction_check_covers_every_database_config() -> None:
- module = _load_mypyc_smoke_module()
-
- discovered = {qualified_name for qualified_name, _ in module._discover_adapter_config_classes()}
-
- assert discovered == {
- "sqlspec.adapters.adbc.config.AdbcConfig",
- "sqlspec.adapters.aiomysql.config.AiomysqlConfig",
- "sqlspec.adapters.aiosqlite.config.AiosqliteConfig",
- "sqlspec.adapters.arrow_odbc.config.ArrowOdbcConfig",
- "sqlspec.adapters.asyncmy.config.AsyncmyConfig",
- "sqlspec.adapters.asyncpg.config.AsyncpgConfig",
- "sqlspec.adapters.bigquery.config.BigQueryConfig",
- "sqlspec.adapters.cockroach_asyncpg.config.CockroachAsyncpgConfig",
- "sqlspec.adapters.cockroach_psycopg.config.CockroachPsycopgAsyncConfig",
- "sqlspec.adapters.cockroach_psycopg.config.CockroachPsycopgSyncConfig",
- "sqlspec.adapters.duckdb.config.DuckDBConfig",
- "sqlspec.adapters.mssql_python.config.MssqlPythonConfig",
- "sqlspec.adapters.mysqlconnector.config.MysqlConnectorAsyncConfig",
- "sqlspec.adapters.mysqlconnector.config.MysqlConnectorSyncConfig",
- "sqlspec.adapters.oracledb.config.OracleAsyncConfig",
- "sqlspec.adapters.oracledb.config.OracleSyncConfig",
- "sqlspec.adapters.psqlpy.config.PsqlpyConfig",
- "sqlspec.adapters.psycopg.config.PsycopgAsyncConfig",
- "sqlspec.adapters.psycopg.config.PsycopgSyncConfig",
- "sqlspec.adapters.pymssql.config.PymssqlConfig",
- "sqlspec.adapters.pymysql.config.PyMysqlConfig",
- "sqlspec.adapters.spanner.config.SpannerSyncConfig",
- "sqlspec.adapters.sqlite.config.SqliteConfig",
- }
-
-
-def test_statement_construction_checks_pass_without_requiring_compilation() -> None:
- module = _load_mypyc_smoke_module()
-
- results = module.run_construction_checks(require_compiled=False)
- result_by_name = {result["name"]: result for result in results}
-
- for name in ("statement_cache_rebind", "statement_sentinel_identity"):
- result = result_by_name[name]
- assert result["imported"] is True
- assert result["error"] is None
-
-
-def test_smoke_runner_skips_optional_adk_dependency(monkeypatch: MonkeyPatch) -> None:
- module = _load_mypyc_smoke_module()
- monkeypatch.setattr(
- module,
- "SMOKE_IMPORTS",
- (module.SmokeImport("adk_record_types", "sqlspec.extensions.adk._types", "StoredSession", True, "google.adk"),),
- )
-
- def import_missing_optional_dependency(name: str) -> ModuleType:
- raise ModuleNotFoundError("No module named 'google.adk'", name="google.adk")
-
- monkeypatch.setattr(module.importlib, "import_module", import_missing_optional_dependency)
-
- results = module.run_smoke(require_compiled=True)
-
- assert results == [
- {
- "name": "adk_record_types",
- "module": "sqlspec.extensions.adk._types",
- "attribute": "StoredSession",
- "imported": False,
- "compiled": False,
- "compiled_required": True,
- "error": None,
- "skipped": True,
- "skip_reason": "optional dependency missing: google.adk",
- }
- ]
- assert module._failed_results(results) == []
-
-
-def test_smoke_runner_skips_missing_optional_parent_package(monkeypatch: MonkeyPatch) -> None:
- module = _load_mypyc_smoke_module()
- monkeypatch.setattr(
- module,
- "SMOKE_IMPORTS",
- (module.SmokeImport("adk_record_types", "sqlspec.extensions.adk._types", "StoredSession", True, "google.adk"),),
- )
-
- def import_missing_optional_parent(name: str) -> ModuleType:
- raise ModuleNotFoundError("No module named 'google'", name="google")
-
- monkeypatch.setattr(module.importlib, "import_module", import_missing_optional_parent)
-
- results = module.run_smoke(require_compiled=True)
-
- assert results[0]["skipped"] is True
- assert results[0]["error"] is None
- assert results[0]["skip_reason"] == "optional dependency missing: google.adk"
- assert module._failed_results(results) == []
-
-
-@pytest.mark.parametrize("adapter, dependency", [("adbc", "adbc_driver_manager"), ("aiosqlite", "aiosqlite")])
-def test_adapter_discovery_reports_missing_optional_driver(
- monkeypatch: MonkeyPatch, adapter: str, dependency: str
-) -> None:
- module = _load_mypyc_smoke_module()
- original_import = importlib.import_module
-
- def import_without_driver(name: str) -> ModuleType:
- if name == f"sqlspec.adapters.{adapter}.config":
- raise ModuleNotFoundError(f"No module named {dependency!r}", name=dependency)
- return original_import(name)
-
- monkeypatch.setattr(module.importlib, "import_module", import_without_driver)
- result = module._check_adapter_config_construction()
- assert result["error"] is None
- assert result["skipped_adapters"] == [
- f"sqlspec.adapters.{adapter}.config: optional dependency missing: {dependency}"
- ]
- assert f"- SKIP sqlspec.adapters.{adapter}.config" in module._format_text([result])
-
-
-def test_adapter_discovery_does_not_hide_internal_import_errors(monkeypatch: MonkeyPatch) -> None:
- module = _load_mypyc_smoke_module()
- original_import = importlib.import_module
-
- def import_broken_adapter(name: str) -> ModuleType:
- if name == "sqlspec.adapters.adbc.config":
- raise ModuleNotFoundError("broken internal import", name="sqlspec.missing")
- return original_import(name)
-
- monkeypatch.setattr(module.importlib, "import_module", import_broken_adapter)
- with pytest.raises(ModuleNotFoundError, match="broken internal import"):
- module._discover_adapter_config_classes(skipped=[])
diff --git a/tests/unit/utils/test_to_value_type.py b/tests/unit/utils/test_to_value_type.py
index a5b71dcb5..a65727911 100644
--- a/tests/unit/utils/test_to_value_type.py
+++ b/tests/unit/utils/test_to_value_type.py
@@ -4,7 +4,7 @@
from dataclasses import dataclass
from decimal import Decimal
from pathlib import Path, PurePosixPath
-from typing import TypedDict
+from typing import Any, TypedDict
from unittest.mock import patch
from uuid import UUID
@@ -76,122 +76,61 @@ def test_foreign_key_metadata_list_conversion() -> None:
assert result[1].table_name == "items"
-def test_identity_conversions_int_identity() -> None:
- """Integer with exact type match returns the same object."""
- value = 42
- result = to_value_type(value, int)
+@pytest.mark.parametrize(
+ ("value", "target_type"),
+ [
+ pytest.param(42, int, id="int"),
+ pytest.param(3.14, float, id="float"),
+ pytest.param("hello", str, id="str"),
+ pytest.param(True, bool, id="bool"),
+ pytest.param(datetime.datetime(2024, 1, 15, 12, 30, 45), datetime.datetime, id="datetime"),
+ pytest.param(datetime.date(2024, 1, 15), datetime.date, id="date"),
+ pytest.param(datetime.time(12, 30, 45), datetime.time, id="time"),
+ pytest.param(Decimal("123.45"), Decimal, id="decimal"),
+ pytest.param(UUID("550e8400-e29b-41d4-a716-446655440000"), UUID, id="uuid"),
+ pytest.param(Path("/tmp/test.txt"), Path, id="path"),
+ pytest.param({"key": "value"}, dict, id="dict"),
+ pytest.param({}, dict, id="empty_dict"),
+ pytest.param([1, 2, 3], list, id="list"),
+ pytest.param([], list, id="empty_list"),
+ ],
+)
+def test_identity_conversions(value: Any, target_type: type[object]) -> None:
+ """Exact type match returns the same object instance."""
+ result = to_value_type(value, target_type)
assert result is value
- assert result == 42
-def test_identity_conversions_float_identity() -> None:
- """Float with exact type match returns the same object."""
- value = 3.14
- result = to_value_type(value, float)
- assert result is value
-
-
-def test_identity_conversions_str_identity() -> None:
- """String with exact type match returns the same object."""
- value = "hello"
- result = to_value_type(value, str)
- assert result is value
-
-
-def test_identity_conversions_bool_identity() -> None:
- """Boolean with exact type match returns the same object."""
- value = True
- result = to_value_type(value, bool)
- assert result is value
-
-
-def test_identity_conversions_datetime_identity() -> None:
- """Datetime with exact type match returns the same object."""
- value = datetime.datetime(2024, 1, 15, 12, 30, 45)
- result = to_value_type(value, datetime.datetime)
- assert result is value
-
-
-def test_identity_conversions_date_identity() -> None:
- """Date with exact type match returns the same object."""
- value = datetime.date(2024, 1, 15)
- result = to_value_type(value, datetime.date)
- assert result is value
-
-
-def test_identity_conversions_time_identity() -> None:
- """Time with exact type match returns the same object."""
- value = datetime.time(12, 30, 45)
- result = to_value_type(value, datetime.time)
- assert result is value
-
-
-def test_identity_conversions_decimal_identity() -> None:
- """Decimal with exact type match returns the same object."""
- value = Decimal("123.45")
- result = to_value_type(value, Decimal)
- assert result is value
-
-
-def test_identity_conversions_uuid_identity() -> None:
- """UUID with exact type match returns the same object."""
- value = UUID("550e8400-e29b-41d4-a716-446655440000")
- result = to_value_type(value, UUID)
- assert result is value
-
-
-def test_identity_conversions_path_identity() -> None:
- """Path with exact type match returns the same object."""
- value = Path("/tmp/test.txt")
- result = to_value_type(value, Path)
- assert result is value
-
-
-def test_identity_conversions_dict_identity() -> None:
- """Dict with exact type match returns the same object."""
- value = {"key": "value"}
- result = to_value_type(value, dict)
- assert result is value
-
-
-def test_identity_conversions_list_identity() -> None:
- """List with exact type match returns the same object."""
- value = [1, 2, 3]
- result = to_value_type(value, list)
- assert result is value
-
-
-def test_subclass_bug_fixes_bool_to_int_converts_true() -> None:
- """True should convert to 1 (not return True)."""
- result = to_value_type(True, int)
- assert result == 1
+@pytest.mark.parametrize(
+ ("val", "expected"), [pytest.param(True, 1, id="true_to_1"), pytest.param(False, 0, id="false_to_0")]
+)
+def test_subclass_bug_fixes_bool_to_int(val: bool, expected: int) -> None:
+ """Boolean values should convert to actual int instances, not return bool."""
+ result = to_value_type(val, int)
+ assert result == expected
assert type(result) is int
- assert result is not True
-
-
-def test_subclass_bug_fixes_bool_to_int_converts_false() -> None:
- """False should convert to 0 (not return False)."""
- result = to_value_type(False, int)
- assert result == 0
- assert type(result) is int
- assert result is not False
-
-
-def test_subclass_bug_fixes_datetime_to_date_converts() -> None:
- """Datetime should convert to date (not return datetime)."""
- dt = datetime.datetime(2024, 1, 15, 12, 30, 45)
- result = to_value_type(dt, datetime.date)
- assert result == datetime.date(2024, 1, 15)
- assert type(result) is datetime.date
- assert not isinstance(result, datetime.datetime)
-
-
-def test_subclass_bug_fixes_datetime_to_time_converts() -> None:
- """Datetime should convert to time."""
+ assert result is not val
+
+
+@pytest.mark.parametrize(
+ ("target_type", "expected_type", "expected_value"),
+ [
+ pytest.param(datetime.date, datetime.date, datetime.date(2024, 1, 15), id="datetime_to_date"),
+ pytest.param(datetime.time, datetime.time, datetime.time(12, 30, 45), id="datetime_to_time"),
+ ],
+)
+def test_subclass_bug_fixes_datetime_subtypes(
+ target_type: type[datetime.date] | type[datetime.time],
+ expected_type: type[datetime.date] | type[datetime.time],
+ expected_value: datetime.date | datetime.time,
+) -> None:
+ """Datetime instances should convert to strict date or time instances."""
dt = datetime.datetime(2024, 1, 15, 12, 30, 45)
- result = to_value_type(dt, datetime.time)
- assert result == datetime.time(12, 30, 45)
- assert type(result) is datetime.time
+ result = to_value_type(dt, target_type)
+ assert result == expected_value
+ assert type(result) is expected_type
+ if target_type is datetime.date:
+ assert not isinstance(result, datetime.datetime)
def test_convert_numpy_recursive_preserves_tuple_shape() -> None:
@@ -224,138 +163,106 @@ def fail_walk(_obj: object) -> object:
@pytest.mark.skipif(not schema_utils.NUMPY_INSTALLED, reason="numpy is not installed")
-def test_msgspec_conversion_falls_back_to_numpy_walk_for_ndarray_payload(monkeypatch: pytest.MonkeyPatch) -> None:
+def test_msgspec_conversion_falls_back_to_numpy_walk_for_ndarray_payload() -> None:
"""Ndarray payloads should still convert through the numpy fallback path."""
import numpy as np
class Measurement(msgspec.Struct):
values: list[float]
- original_walk = schema_utils._convert_numpy_recursive
- call_count = 0
-
- def count_walk(obj: object) -> object:
- nonlocal call_count
- if isinstance(obj, list):
- call_count += 1
- return original_walk(obj)
-
- monkeypatch.setattr(schema_utils, "_convert_numpy_recursive", count_walk)
result = schema_utils._convert_msgspec([{"values": np.array([1.0, 2.0])}], Measurement)
assert result == [Measurement(values=[1.0, 2.0])]
- assert call_count == 1
-
-
-def test_int_conversion_float_to_int() -> None:
- """Float truncates to int."""
- assert to_value_type(3.7, int) == 3
- assert to_value_type(3.2, int) == 3
- assert to_value_type(-3.7, int) == -3
-
-def test_int_conversion_str_to_int() -> None:
- """String with integer value converts to int."""
- assert to_value_type("42", int) == 42
- assert to_value_type("-123", int) == -123
-
-def test_int_conversion_str_float_to_int() -> None:
- """String with float value converts to int (truncated)."""
- assert to_value_type("42.7", int) == 42
- assert to_value_type("-3.9", int) == -3
-
-
-def test_int_conversion_decimal_to_int() -> None:
- """Decimal converts to int (truncated)."""
- assert to_value_type(Decimal("42.7"), int) == 42
-
-
-def test_int_conversion_invalid_str_to_int_raises() -> None:
- """Invalid string raises TypeError."""
- with pytest.raises(TypeError, match="Cannot convert str to int"):
- to_value_type("not a number", int)
-
-
-def test_float_conversion_int_to_float() -> None:
- """Integer converts to float."""
- assert to_value_type(42, float) == 42.0
-
-
-def test_float_conversion_str_to_float() -> None:
- """String with numeric value converts to float."""
- assert to_value_type("3.14", float) == 3.14
- assert to_value_type("-2.5", float) == -2.5
-
-
-def test_float_conversion_decimal_to_float() -> None:
- """Decimal converts to float."""
- assert to_value_type(Decimal("3.14159"), float) == pytest.approx(3.14159)
-
-
-def test_float_conversion_bool_to_float() -> None:
- """Boolean converts to float."""
- assert to_value_type(True, float) == 1.0
- assert to_value_type(False, float) == 0.0
-
-
-def test_float_conversion_invalid_str_to_float_raises() -> None:
- """Invalid string raises TypeError."""
- with pytest.raises(TypeError, match="Cannot convert str to float"):
- to_value_type("not a number", float)
-
-
-def test_str_conversion_int_to_str() -> None:
- """Integer converts to string."""
- assert to_value_type(42, str) == "42"
-
-
-def test_str_conversion_float_to_str() -> None:
- """Float converts to string."""
- assert to_value_type(3.14, str) == "3.14"
-
-
-def test_str_conversion_bool_to_str() -> None:
- """Boolean converts to string."""
- assert to_value_type(True, str) == "True"
- assert to_value_type(False, str) == "False"
-
-
-def test_str_conversion_uuid_to_str() -> None:
- """UUID converts to string."""
- uuid = UUID("550e8400-e29b-41d4-a716-446655440000")
- assert to_value_type(uuid, str) == "550e8400-e29b-41d4-a716-446655440000"
-
-
-def test_bool_conversion_int_to_bool() -> None:
- """Integer converts to bool."""
- assert to_value_type(1, bool) is True
- assert to_value_type(0, bool) is False
- assert to_value_type(42, bool) is True
-
-
-def test_bool_conversion_str_true_values_to_bool() -> None:
- """String true values convert to True."""
- for val in ["true", "True", "TRUE", "1", "yes", "Yes", "y", "Y", "t", "T", "on", "ON"]:
- assert to_value_type(val, bool) is True, f"Expected '{val}' to be True"
-
-
-def test_bool_conversion_str_false_values_to_bool() -> None:
- """String false values convert to False."""
- for val in ["false", "False", "FALSE", "0", "no", "No", "n", "N", "f", "F", "off", "OFF", "", "anything"]:
- assert to_value_type(val, bool) is False, f"Expected '{val}' to be False"
-
-
-def test_bool_conversion_float_to_bool() -> None:
- """Float converts to bool."""
- assert to_value_type(1.0, bool) is True
- assert to_value_type(0.0, bool) is False
- assert to_value_type(0.1, bool) is True
-
-
-def test_datetime_conversion_str_iso_to_datetime() -> None:
- """ISO format string converts to datetime."""
- result = to_value_type("2024-01-15T12:30:45", datetime.datetime)
- assert result == datetime.datetime(2024, 1, 15, 12, 30, 45)
+@pytest.mark.parametrize(
+ ("value", "expected"),
+ [
+ pytest.param(3.7, 3, id="float_positive_round_down"),
+ pytest.param(3.2, 3, id="float_positive_fraction"),
+ pytest.param(-3.7, -3, id="float_negative"),
+ pytest.param("42", 42, id="str_positive"),
+ pytest.param("-123", -123, id="str_negative"),
+ pytest.param("42.7", 42, id="str_float_positive"),
+ pytest.param("-3.9", -3, id="str_float_negative"),
+ pytest.param(Decimal("42.7"), 42, id="decimal"),
+ ],
+)
+def test_int_conversions(value: Any, expected: int) -> None:
+ """Values convert to integer with truncation where applicable."""
+ assert to_value_type(value, int) == expected
+
+
+@pytest.mark.parametrize(
+ ("value", "expected"),
+ [
+ pytest.param(42, 42.0, id="int"),
+ pytest.param("3.14", 3.14, id="str_positive"),
+ pytest.param("-2.5", -2.5, id="str_negative"),
+ pytest.param(Decimal("3.14159"), 3.14159, id="decimal"),
+ pytest.param(True, 1.0, id="bool_true"),
+ pytest.param(False, 0.0, id="bool_false"),
+ ],
+)
+def test_float_conversions(value: Any, expected: float) -> None:
+ """Values convert to float with matching numeric precision."""
+ assert to_value_type(value, float) == pytest.approx(expected)
+
+
+@pytest.mark.parametrize(
+ ("value", "expected"),
+ [
+ pytest.param(42, "42", id="int"),
+ pytest.param(3.14, "3.14", id="float"),
+ pytest.param(True, "True", id="bool_true"),
+ pytest.param(False, "False", id="bool_false"),
+ pytest.param(UUID("550e8400-e29b-41d4-a716-446655440000"), "550e8400-e29b-41d4-a716-446655440000", id="uuid"),
+ ],
+)
+def test_str_conversions(value: Any, expected: str) -> None:
+ """Values convert to string representation."""
+ assert to_value_type(value, str) == expected
+
+
+@pytest.mark.parametrize(
+ ("value", "expected"),
+ [
+ pytest.param(1, True, id="int_one"),
+ pytest.param(0, False, id="int_zero"),
+ pytest.param(42, True, id="int_positive"),
+ pytest.param(1.0, True, id="float_one"),
+ pytest.param(0.0, False, id="float_zero"),
+ pytest.param(0.1, True, id="float_fraction"),
+ ],
+)
+def test_bool_numeric_conversions(value: Any, expected: bool) -> None:
+ """Numeric values convert to bool according to zero/non-zero rules."""
+ assert to_value_type(value, bool) is expected
+
+
+@pytest.mark.parametrize("val", ["true", "True", "TRUE", "1", "yes", "Yes", "y", "Y", "t", "T", "on", "ON"])
+def test_bool_conversion_str_true_values(val: str) -> None:
+ """String representations of truth convert to True."""
+ assert to_value_type(val, bool) is True
+
+
+@pytest.mark.parametrize(
+ "val", ["false", "False", "FALSE", "0", "no", "No", "n", "N", "f", "F", "off", "OFF", "", "anything"]
+)
+def test_bool_conversion_str_false_values(val: str) -> None:
+ """String representations of falsity or empty strings convert to False."""
+ assert to_value_type(val, bool) is False
+
+
+@pytest.mark.parametrize(
+ ("value", "expected"),
+ [
+ pytest.param("2024-01-15T12:30:45", datetime.datetime(2024, 1, 15, 12, 30, 45), id="iso_str"),
+ pytest.param(datetime.date(2024, 1, 15), datetime.datetime(2024, 1, 15, 0, 0, 0), id="date_to_datetime"),
+ ],
+)
+def test_datetime_conversions(value: Any, expected: datetime.datetime) -> None:
+ """Values convert to datetime instances."""
+ assert to_value_type(value, datetime.datetime) == expected
def test_datetime_conversion_str_iso_with_tz_to_datetime() -> None:
@@ -366,234 +273,122 @@ def test_datetime_conversion_str_iso_with_tz_to_datetime() -> None:
assert result.day == 15
-def test_datetime_conversion_date_to_datetime() -> None:
- """Date converts to datetime at midnight."""
- date = datetime.date(2024, 1, 15)
- result = to_value_type(date, datetime.datetime)
- assert result == datetime.datetime(2024, 1, 15, 0, 0, 0)
-
-
-def test_datetime_conversion_invalid_str_to_datetime_raises() -> None:
- """Invalid string raises TypeError."""
- with pytest.raises(TypeError, match="Cannot convert str to datetime"):
- to_value_type("not a date", datetime.datetime)
-
-
-def test_date_conversion_str_iso_to_date() -> None:
- """ISO format string converts to date."""
- result = to_value_type("2024-01-15", datetime.date)
- assert result == datetime.date(2024, 1, 15)
-
-
-def test_date_conversion_str_datetime_to_date() -> None:
- """Datetime string extracts date portion."""
- result = to_value_type("2024-01-15T12:30:45", datetime.date)
- assert result == datetime.date(2024, 1, 15)
-
-
-def test_date_conversion_datetime_to_date() -> None:
- """Datetime extracts date portion."""
- dt = datetime.datetime(2024, 1, 15, 12, 30, 45)
- result = to_value_type(dt, datetime.date)
- assert result == datetime.date(2024, 1, 15)
-
-
-def test_date_conversion_invalid_str_to_date_raises() -> None:
- """Invalid string raises TypeError."""
- with pytest.raises(TypeError, match="Cannot convert str to date"):
- to_value_type("not a date", datetime.date)
-
-
-def test_time_conversion_str_iso_to_time() -> None:
- """ISO format string converts to time."""
- result = to_value_type("12:30:45", datetime.time)
- assert result == datetime.time(12, 30, 45)
-
-
-def test_time_conversion_datetime_to_time() -> None:
- """Datetime extracts time portion."""
- dt = datetime.datetime(2024, 1, 15, 12, 30, 45)
- result = to_value_type(dt, datetime.time)
- assert result == datetime.time(12, 30, 45)
-
-
-def test_time_conversion_invalid_str_to_time_raises() -> None:
- """Invalid string raises TypeError."""
- with pytest.raises(TypeError, match="Cannot convert str to time"):
- to_value_type("not a time", datetime.time)
-
-
-def test_decimal_conversion_int_to_decimal() -> None:
- """Integer converts to Decimal."""
- result = to_value_type(42, Decimal)
- assert result == Decimal(42)
-
-
-def test_decimal_conversion_float_to_decimal() -> None:
- """Float converts to Decimal (via string for precision)."""
- result = to_value_type(3.14, Decimal)
- assert result == Decimal("3.14")
-
-
-def test_decimal_conversion_str_to_decimal() -> None:
- """String converts to Decimal."""
- result = to_value_type("123.456789", Decimal)
- assert result == Decimal("123.456789")
-
-
-def test_decimal_conversion_invalid_str_to_decimal_raises() -> None:
- """Invalid string raises TypeError."""
- with pytest.raises(TypeError, match="Cannot convert str to Decimal"):
- to_value_type("not a number", Decimal)
-
-
-def test_uuid_conversion_str_to_uuid() -> None:
- """UUID string converts to UUID."""
- result = to_value_type("550e8400-e29b-41d4-a716-446655440000", UUID)
- assert result == UUID("550e8400-e29b-41d4-a716-446655440000")
-
-
-def test_uuid_conversion_str_uppercase_to_uuid() -> None:
- """Uppercase UUID string converts to UUID."""
- result = to_value_type("550E8400-E29B-41D4-A716-446655440000", UUID)
- assert result == UUID("550e8400-e29b-41d4-a716-446655440000")
-
-
-def test_uuid_conversion_bytes_to_uuid() -> None:
- """Bytes converts to UUID."""
- uuid_bytes = UUID("550e8400-e29b-41d4-a716-446655440000").bytes
- result = to_value_type(uuid_bytes, UUID)
- assert result == UUID("550e8400-e29b-41d4-a716-446655440000")
-
-
-def test_uuid_conversion_invalid_str_to_uuid_raises() -> None:
- """Invalid string raises TypeError."""
- with pytest.raises(TypeError, match="Cannot convert str to UUID"):
- to_value_type("not-a-uuid", UUID)
-
-
-def test_path_conversion_str_to_path() -> None:
- """String converts to Path."""
- result = to_value_type("/tmp/test.txt", Path)
- assert result == Path("/tmp/test.txt")
-
-
-def test_path_conversion_pure_path_to_path() -> None:
- """PurePath converts to Path."""
- pure = PurePosixPath("/tmp/test.txt")
- result = to_value_type(pure, Path)
- assert result == Path("/tmp/test.txt")
-
-
-def test_path_conversion_invalid_type_to_path_raises() -> None:
- """Invalid type raises TypeError."""
- with pytest.raises(TypeError, match="Cannot convert int to Path"):
- to_value_type(123, Path)
-
-
-def test_dict_conversion_json_str_to_dict() -> None:
- """JSON string converts to dict."""
- result = to_value_type('{"key": "value", "count": 42}', dict)
- assert result == {"key": "value", "count": 42}
-
-
-def test_dict_conversion_json_nested_to_dict() -> None:
- """Nested JSON string converts to dict."""
- result = to_value_type('{"outer": {"inner": [1, 2, 3]}}', dict)
- assert result == {"outer": {"inner": [1, 2, 3]}}
-
-
-def test_dict_conversion_json_array_to_dict_raises() -> None:
- """JSON array string raises TypeError when converting to dict."""
- with pytest.raises(TypeError, match="JSON string did not parse to dict"):
- to_value_type("[1, 2, 3]", dict)
-
-
-def test_dict_conversion_invalid_json_to_dict_raises() -> None:
- """Invalid JSON string raises TypeError."""
- with pytest.raises(TypeError, match="Cannot convert str to dict"):
- to_value_type("not json", dict)
-
-
-def test_list_conversion_json_array_str_to_list() -> None:
- """JSON array string converts to list."""
- result = to_value_type('[1, 2, 3, "four"]', list)
- assert result == [1, 2, 3, "four"]
-
-
-def test_list_conversion_json_nested_array_to_list() -> None:
- """Nested JSON array converts to list."""
- result = to_value_type("[[1, 2], [3, 4]]", list)
- assert result == [[1, 2], [3, 4]]
-
-
-def test_list_conversion_json_object_to_list_raises() -> None:
- """JSON object string raises TypeError when converting to list."""
- with pytest.raises(TypeError, match="JSON string did not parse to list"):
- to_value_type('{"key": "value"}', list)
-
-
-def test_list_conversion_tuple_to_list() -> None:
- """Tuple converts to list."""
- result = to_value_type((1, 2, 3), list)
- assert result == [1, 2, 3]
-
-
-def test_list_conversion_set_to_list() -> None:
- """Set converts to list (order may vary)."""
- result = to_value_type({1, 2, 3}, list)
- assert sorted(result) == [1, 2, 3]
-
-
-def test_list_conversion_frozenset_to_list() -> None:
- """Frozenset converts to list (order may vary)."""
- result = to_value_type(frozenset({1, 2, 3}), list)
- assert sorted(result) == [1, 2, 3]
-
-
-def test_list_conversion_invalid_json_to_list_raises() -> None:
- """Invalid JSON string raises TypeError."""
- with pytest.raises(TypeError, match="Cannot convert str to list"):
- to_value_type("not json", list)
-
-
-def test_edge_cases_empty_string_to_bool_is_false() -> None:
- """Empty string converts to False."""
- assert to_value_type("", bool) is False
-
-
-def test_edge_cases_zero_to_bool_is_false() -> None:
- """Zero converts to False."""
- assert to_value_type(0, bool) is False
- assert to_value_type(0.0, bool) is False
-
-
-def test_edge_cases_empty_dict_preserved() -> None:
- """Empty dict is preserved."""
- value: dict[str, str] = {}
- result = to_value_type(value, dict)
- assert result == {}
- assert result is value
-
-
-def test_edge_cases_empty_list_preserved() -> None:
- """Empty list is preserved."""
- value: list[int] = []
+@pytest.mark.parametrize(
+ ("value", "expected"),
+ [
+ pytest.param("2024-01-15", datetime.date(2024, 1, 15), id="iso_date_str"),
+ pytest.param("2024-01-15T12:30:45", datetime.date(2024, 1, 15), id="iso_datetime_str"),
+ pytest.param(datetime.datetime(2024, 1, 15, 12, 30, 45), datetime.date(2024, 1, 15), id="datetime_instance"),
+ ],
+)
+def test_date_conversions(value: Any, expected: datetime.date) -> None:
+ """Values convert to date instances."""
+ assert to_value_type(value, datetime.date) == expected
+
+
+@pytest.mark.parametrize(
+ ("value", "expected"),
+ [
+ pytest.param("12:30:45", datetime.time(12, 30, 45), id="iso_time_str"),
+ pytest.param(datetime.datetime(2024, 1, 15, 12, 30, 45), datetime.time(12, 30, 45), id="datetime_instance"),
+ ],
+)
+def test_time_conversions(value: Any, expected: datetime.time) -> None:
+ """Values convert to time instances."""
+ assert to_value_type(value, datetime.time) == expected
+
+
+@pytest.mark.parametrize(
+ ("value", "expected"),
+ [
+ pytest.param(42, Decimal(42), id="int"),
+ pytest.param(3.14, Decimal("3.14"), id="float"),
+ pytest.param("123.456789", Decimal("123.456789"), id="str"),
+ ],
+)
+def test_decimal_conversions(value: Any, expected: Decimal) -> None:
+ """Values convert to Decimal instances."""
+ assert to_value_type(value, Decimal) == expected
+
+
+@pytest.mark.parametrize(
+ "value",
+ [
+ pytest.param("550e8400-e29b-41d4-a716-446655440000", id="str_lowercase"),
+ pytest.param("550E8400-E29B-41D4-A716-446655440000", id="str_uppercase"),
+ pytest.param(UUID("550e8400-e29b-41d4-a716-446655440000").bytes, id="bytes"),
+ ],
+)
+def test_uuid_conversions(value: Any) -> None:
+ """Values convert to UUID instances."""
+ assert to_value_type(value, UUID) == UUID("550e8400-e29b-41d4-a716-446655440000")
+
+
+@pytest.mark.parametrize(
+ "value", [pytest.param("/tmp/test.txt", id="str"), pytest.param(PurePosixPath("/tmp/test.txt"), id="pure_path")]
+)
+def test_path_conversions(value: Any) -> None:
+ """Values convert to Path instances."""
+ assert to_value_type(value, Path) == Path("/tmp/test.txt")
+
+
+@pytest.mark.parametrize(
+ ("value", "expected"),
+ [
+ pytest.param('{"key": "value", "count": 42}', {"key": "value", "count": 42}, id="flat_json"),
+ pytest.param('{"outer": {"inner": [1, 2, 3]}}', {"outer": {"inner": [1, 2, 3]}}, id="nested_json"),
+ pytest.param("{}", {}, id="empty_json"),
+ ],
+)
+def test_dict_conversions(value: str, expected: "dict[str, Any]") -> None:
+ """JSON strings convert to dictionary structures."""
+ assert to_value_type(value, dict) == expected
+
+
+@pytest.mark.parametrize(
+ ("value", "expected"),
+ [
+ pytest.param('[1, 2, 3, "four"]', [1, 2, 3, "four"], id="flat_json_array"),
+ pytest.param("[[1, 2], [3, 4]]", [[1, 2], [3, 4]], id="nested_json_array"),
+ pytest.param("[]", [], id="empty_json_array"),
+ pytest.param((1, 2, 3), [1, 2, 3], id="tuple"),
+ ],
+)
+def test_list_conversions(value: Any, expected: "list[Any]") -> None:
+ """Sequences and JSON array strings convert to list structures."""
+ assert to_value_type(value, list) == expected
+
+
+@pytest.mark.parametrize(
+ "value", [pytest.param({1, 2, 3}, id="set"), pytest.param(frozenset({1, 2, 3}), id="frozenset")]
+)
+def test_set_to_list_conversions(value: Any) -> None:
+ """Sets and frozensets convert to lists."""
result = to_value_type(value, list)
- assert result == []
- assert result is value
-
-
-def test_edge_cases_empty_json_object_to_dict() -> None:
- """Empty JSON object converts to empty dict."""
- result = to_value_type("{}", dict)
- assert result == {}
+ assert sorted(result) == [1, 2, 3]
-def test_edge_cases_empty_json_array_to_list() -> None:
- """Empty JSON array converts to empty list."""
- result = to_value_type("[]", list)
- assert result == []
+@pytest.mark.parametrize(
+ ("value", "target_type", "match"),
+ [
+ pytest.param("not a number", int, "Cannot convert str to int", id="str_to_int"),
+ pytest.param("not a number", float, "Cannot convert str to float", id="str_to_float"),
+ pytest.param("not a date", datetime.datetime, "Cannot convert str to datetime", id="str_to_datetime"),
+ pytest.param("not a date", datetime.date, "Cannot convert str to date", id="str_to_date"),
+ pytest.param("not a time", datetime.time, "Cannot convert str to time", id="str_to_time"),
+ pytest.param("not a number", Decimal, "Cannot convert str to Decimal", id="str_to_decimal"),
+ pytest.param("not-a-uuid", UUID, "Cannot convert str to UUID", id="str_to_uuid"),
+ pytest.param(123, Path, "Cannot convert int to Path", id="int_to_path"),
+ pytest.param("[1, 2, 3]", dict, "JSON string did not parse to dict", id="json_array_to_dict"),
+ pytest.param("not json", dict, "Cannot convert str to dict", id="invalid_json_to_dict"),
+ pytest.param('{"key": "value"}', list, "JSON string did not parse to list", id="json_obj_to_list"),
+ pytest.param("not json", list, "Cannot convert str to list", id="invalid_json_to_list"),
+ ],
+)
+def test_conversion_type_errors(value: Any, target_type: type, match: str) -> None:
+ """Invalid input representations raise expected TypeError on conversion."""
+ with pytest.raises(TypeError, match=match):
+ to_value_type(value, target_type)
def test_fallback_conversion_custom_type_with_constructor() -> None:
@@ -661,22 +456,31 @@ class UserTypedDict(TypedDict):
email: str
-def test_pydantic_conversion_dict_to_pydantic() -> None:
- """Dict converts to Pydantic model."""
- data = {"name": "Alice", "email": "alice@example.com"}
- result = to_value_type(data, UserPydantic)
- assert isinstance(result, UserPydantic)
- assert result.name == "Alice"
- assert result.email == "alice@example.com"
-
-
-def test_pydantic_conversion_json_string_to_pydantic() -> None:
- """JSON string converts to Pydantic model."""
- json_str = '{"name": "Bob", "email": "bob@example.com"}'
- result = to_value_type(json_str, UserPydantic)
- assert isinstance(result, UserPydantic)
- assert result.name == "Bob"
- assert result.email == "bob@example.com"
+@pytest.mark.parametrize(
+ ("model_cls", "payload"),
+ [
+ pytest.param(UserPydantic, {"name": "Alice", "email": "alice@example.com"}, id="pydantic_dict"),
+ pytest.param(UserPydantic, '{"name": "Bob", "email": "bob@example.com"}', id="pydantic_json"),
+ pytest.param(UserDataclass, {"name": "Alice", "email": "alice@example.com"}, id="dataclass_dict"),
+ pytest.param(UserDataclass, '{"name": "Bob", "email": "bob@example.com"}', id="dataclass_json"),
+ pytest.param(UserMsgspec, {"name": "Alice", "email": "alice@example.com"}, id="msgspec_dict"),
+ pytest.param(UserMsgspec, '{"name": "Bob", "email": "bob@example.com"}', id="msgspec_json"),
+ pytest.param(UserAttrs, {"name": "Alice", "email": "alice@example.com"}, id="attrs_dict"),
+ pytest.param(UserAttrs, '{"name": "Bob", "email": "bob@example.com"}', id="attrs_json"),
+ ],
+)
+def test_schema_model_conversions(
+ model_cls: type[UserPydantic] | type[UserDataclass] | type[UserMsgspec] | type[UserAttrs], payload: Any
+) -> None:
+ """Dicts and JSON strings convert to supported schema model instances."""
+ result = to_value_type(payload, model_cls)
+ assert isinstance(result, model_cls)
+ if isinstance(payload, str):
+ assert result.name == "Bob"
+ assert result.email == "bob@example.com"
+ else:
+ assert result.name == "Alice"
+ assert result.email == "alice@example.com"
def test_pydantic_conversion_pydantic_identity() -> None:
@@ -697,76 +501,19 @@ def test_pydantic_conversion_schema_conversion_uses_cached_converter_path() -> N
assert result.name == "Alice"
-def test_dataclass_conversion_dict_to_dataclass() -> None:
- """Dict converts to dataclass."""
- data = {"name": "Alice", "email": "alice@example.com"}
- result = to_value_type(data, UserDataclass)
- assert isinstance(result, UserDataclass)
- assert result.name == "Alice"
- assert result.email == "alice@example.com"
-
-
-def test_dataclass_conversion_json_string_to_dataclass() -> None:
- """JSON string converts to dataclass."""
- json_str = '{"name": "Bob", "email": "bob@example.com"}'
- result = to_value_type(json_str, UserDataclass)
- assert isinstance(result, UserDataclass)
- assert result.name == "Bob"
- assert result.email == "bob@example.com"
-
-
-def test_msgspec_conversion_dict_to_msgspec() -> None:
- """Dict converts to msgspec Struct."""
- data = {"name": "Alice", "email": "alice@example.com"}
- result = to_value_type(data, UserMsgspec)
- assert isinstance(result, UserMsgspec)
- assert result.name == "Alice"
- assert result.email == "alice@example.com"
-
-
-def test_msgspec_conversion_json_string_to_msgspec() -> None:
- """JSON string converts to msgspec Struct."""
- json_str = '{"name": "Bob", "email": "bob@example.com"}'
- result = to_value_type(json_str, UserMsgspec)
- assert isinstance(result, UserMsgspec)
- assert result.name == "Bob"
- assert result.email == "bob@example.com"
-
-
-def test_attrs_conversion_dict_to_attrs() -> None:
- """Dict converts to attrs class."""
- data = {"name": "Alice", "email": "alice@example.com"}
- result = to_value_type(data, UserAttrs)
- assert isinstance(result, UserAttrs)
- assert result.name == "Alice"
- assert result.email == "alice@example.com"
-
-
-def test_attrs_conversion_json_string_to_attrs() -> None:
- """JSON string converts to attrs class."""
- json_str = '{"name": "Bob", "email": "bob@example.com"}'
- result = to_value_type(json_str, UserAttrs)
- assert isinstance(result, UserAttrs)
- assert result.name == "Bob"
- assert result.email == "bob@example.com"
-
-
-def test_typed_dict_conversion_dict_to_typed_dict() -> None:
- """Dict converts to TypedDict (returns dict since TypedDict is runtime dict)."""
- data = {"name": "Alice", "email": "alice@example.com"}
- result = to_value_type(data, UserTypedDict)
- assert isinstance(result, dict)
- assert result["name"] == "Alice"
- assert result["email"] == "alice@example.com"
-
-
-def test_typed_dict_conversion_json_string_to_typed_dict() -> None:
- """JSON string converts to TypedDict."""
- json_str = '{"name": "Bob", "email": "bob@example.com"}'
- result = to_value_type(json_str, UserTypedDict)
+@pytest.mark.parametrize(
+ ("payload", "expected_name", "expected_email"),
+ [
+ pytest.param({"name": "Alice", "email": "alice@example.com"}, "Alice", "alice@example.com", id="dict"),
+ pytest.param('{"name": "Bob", "email": "bob@example.com"}', "Bob", "bob@example.com", id="json_str"),
+ ],
+)
+def test_typed_dict_conversions(payload: Any, expected_name: str, expected_email: str) -> None:
+ """Dict and JSON strings convert to TypedDict mapping representations."""
+ result = to_value_type(payload, UserTypedDict)
assert isinstance(result, dict)
- assert result["name"] == "Bob"
- assert result["email"] == "bob@example.com"
+ assert result["name"] == expected_name
+ assert result["email"] == expected_email
def test_schema_type_edge_cases_nested_json_to_pydantic() -> None:
diff --git a/tests/unit/utils/test_type_guards.py b/tests/unit/utils/test_type_guards.py
index 153bffcce..d3b484560 100644
--- a/tests/unit/utils/test_type_guards.py
+++ b/tests/unit/utils/test_type_guards.py
@@ -74,6 +74,7 @@
resolve_row_format,
supports_arrow_results,
)
+from tests.conftest import is_compiled
_UNSET = object()
@@ -189,515 +190,427 @@ async def read(self) -> str:
return "async"
-def test_is_readable_accepts_sync_read_method() -> None:
- assert is_readable(SyncReadable()) is True
-
-
-def test_is_async_readable_rejects_sync_read_method() -> None:
- assert is_async_readable(SyncReadable()) is False
-
-
-def test_is_async_readable_accepts_async_read_method() -> None:
- assert is_async_readable(AsyncReadable()) is True
-
-
-def test_is_dataclass_instance_with_valid_dataclass() -> None:
- """Test is_dataclass_instance returns True for dataclass instances."""
- instance = SampleDataclass(name="test", age=25)
- assert is_dataclass_instance(instance) is True
-
-
-def test_is_dataclass_instance_with_dataclass_class() -> None:
- """Test is_dataclass_instance returns False for dataclass classes."""
- assert is_dataclass_instance(SampleDataclass) is False
-
-
-def test_is_dataclass_instance_with_non_dataclass() -> None:
- """Test is_dataclass_instance returns False for non-dataclass objects."""
- assert is_dataclass_instance("not a dataclass") is False
- assert is_dataclass_instance(42) is False
- assert is_dataclass_instance({}) is False
-
-
-def test_is_dataclass_with_dataclass_class() -> None:
- """Test is_dataclass returns True for dataclass classes."""
- assert is_dataclass(SampleDataclass) is True
-
-
-def test_is_dataclass_with_dataclass_instance() -> None:
- """Test is_dataclass returns True for dataclass instances."""
- instance = SampleDataclass(name="test", age=25)
- assert is_dataclass(instance) is True
-
-
-def test_is_dataclass_with_non_dataclass() -> None:
- """Test is_dataclass returns False for non-dataclass objects."""
- assert is_dataclass("not a dataclass") is False
- assert is_dataclass(42) is False
- assert is_dataclass({}) is False
-
-
-def test_is_dataclass_with_field_existing_field() -> None:
- """Test is_dataclass_with_field returns True when field exists."""
- instance = SampleDataclass(name="test", age=25)
- assert is_dataclass_with_field(instance, "name") is True
- assert is_dataclass_with_field(instance, "age") is True
-
-
-def test_is_dataclass_with_field_missing_field() -> None:
- """Test is_dataclass_with_field returns False when field doesn't exist."""
- instance = SampleDataclass(name="test", age=25)
- assert is_dataclass_with_field(instance, "nonexistent") is False
-
-
-def test_is_dataclass_with_field_non_dataclass() -> None:
- """Test is_dataclass_with_field returns False for non-dataclass objects."""
- assert is_dataclass_with_field("not a dataclass", "any_field") is False
-
-
-def test_is_dataclass_without_field_missing_field() -> None:
- """Test is_dataclass_without_field returns True when field doesn't exist."""
- instance = SampleDataclass(name="test", age=25)
- assert is_dataclass_without_field(instance, "nonexistent") is True
-
-
-def test_is_dataclass_without_field_existing_field() -> None:
- """Test is_dataclass_without_field returns False when field exists."""
- instance = SampleDataclass(name="test", age=25)
- assert is_dataclass_without_field(instance, "name") is False
-
-
-def test_is_dataclass_without_field_non_dataclass() -> None:
- """Test is_dataclass_without_field returns False for non-dataclass objects."""
- assert is_dataclass_without_field("not a dataclass", "any_field") is False
-
-
-def test_is_dict_with_dictionary() -> None:
- """Test is_dict returns True for dictionaries."""
- assert is_dict({}) is True
- assert is_dict({"key": "value"}) is True
-
-
-def test_is_dict_with_non_dictionary() -> None:
- """Test is_dict returns False for non-dictionary objects."""
- assert is_dict("not a dict") is False
- assert is_dict([]) is False
- assert is_dict(42) is False
-
-
-def test_is_dict_with_field_existing_key() -> None:
- """Test is_dict_with_field returns True when key exists."""
- data = {"name": "test", "age": 25}
- assert is_dict_with_field(data, "name") is True
- assert is_dict_with_field(data, "age") is True
-
-
-def test_is_dict_with_field_missing_key() -> None:
- """Test is_dict_with_field returns False when key doesn't exist."""
- data = {"name": "test"}
- assert is_dict_with_field(data, "nonexistent") is False
-
-
-def test_is_dict_with_field_non_dict() -> None:
- """Test is_dict_with_field returns False for non-dict objects."""
- assert is_dict_with_field("not a dict", "any_key") is False
-
-
-def test_is_dict_without_field_missing_key() -> None:
- """Test is_dict_without_field returns True when key doesn't exist."""
- data = {"name": "test"}
- assert is_dict_without_field(data, "nonexistent") is True
-
-
-def test_is_dict_without_field_existing_key() -> None:
- """Test is_dict_without_field returns False when key exists."""
- data = {"name": "test", "age": 25}
- assert is_dict_without_field(data, "name") is False
-
-
-def test_is_dict_without_field_non_dict() -> None:
- """Test is_dict_without_field returns False for non-dict objects."""
- assert is_dict_without_field("not a dict", "any_key") is False
-
-
-def test_is_dict_row_with_dictionary() -> None:
- """Test is_dict_row returns True for dictionaries (row data)."""
- assert is_dict_row({}) is True
- assert is_dict_row({"col1": "value1", "col2": "value2"}) is True
-
-
-def test_is_dict_row_with_non_dictionary() -> None:
- """Test is_dict_row returns False for non-dictionary objects."""
- assert is_dict_row("not a dict") is False
- assert is_dict_row([]) is False
- assert is_dict_row(42) is False
-
-
-def test_is_pydantic_model_when_not_installed() -> None:
- """Test is_pydantic_model returns False when pydantic not available."""
- assert is_pydantic_model("not a model") is False
- assert is_pydantic_model({}) is False
-
-
-def test_is_pydantic_model_with_field_when_not_installed() -> None:
- """Test is_pydantic_model_with_field returns False when pydantic not available."""
- assert is_pydantic_model_with_field("not a model", "field") is False
-
-
-def test_is_pydantic_model_without_field_when_not_installed() -> None:
- """Test is_pydantic_model_without_field returns False when pydantic not available."""
- assert is_pydantic_model_without_field("not a model", "field") is False
-
-
-def test_is_msgspec_struct_when_not_installed() -> None:
- """Test is_msgspec_struct returns False when msgspec not available."""
- assert is_msgspec_struct("not a struct") is False
- assert is_msgspec_struct({}) is False
-
-
-def test_is_msgspec_struct_with_field_when_not_installed() -> None:
- """Test is_msgspec_struct_with_field returns False when msgspec not available."""
- assert is_msgspec_struct_with_field("not a struct", "field") is False
+@pytest.mark.parametrize(
+ ("target", "guard", "expected"),
+ [
+ pytest.param(SyncReadable(), is_readable, True, id="sync_readable_with_is_readable"),
+ pytest.param(SyncReadable(), is_async_readable, False, id="sync_readable_with_is_async_readable"),
+ pytest.param(AsyncReadable(), is_async_readable, True, id="async_readable_with_is_async_readable"),
+ ],
+)
+def test_readable_guards(target: Any, guard: Any, expected: bool) -> None:
+ """Validate readable and async readable protocol guards."""
+ assert guard(target) is expected
-def test_is_msgspec_struct_without_field_when_not_installed() -> None:
- """Test is_msgspec_struct_without_field returns False when msgspec not available."""
- assert is_msgspec_struct_without_field("not a struct", "field") is False
+@pytest.mark.parametrize(
+ ("value", "expected"),
+ [
+ pytest.param(SampleDataclass(name="test", age=25), True, id="instance"),
+ pytest.param(SampleDataclass, False, id="class"),
+ pytest.param("not a dataclass", False, id="string"),
+ pytest.param(42, False, id="integer"),
+ pytest.param({}, False, id="dict"),
+ ],
+)
+def test_is_dataclass_instance(value: Any, expected: bool) -> None:
+ """Validate is_dataclass_instance returns True for dataclass instances only."""
+ assert is_dataclass_instance(value) is expected
-def test_is_attrs_instance_when_not_installed() -> None:
- """Test is_attrs_instance returns False when attrs not available."""
- assert is_attrs_instance("not attrs") is False
- assert is_attrs_instance({}) is False
+@pytest.mark.parametrize(
+ ("value", "expected"),
+ [
+ pytest.param(SampleDataclass, True, id="class"),
+ pytest.param(SampleDataclass(name="test", age=25), True, id="instance"),
+ pytest.param("not a dataclass", False, id="string"),
+ pytest.param(42, False, id="integer"),
+ pytest.param({}, False, id="dict"),
+ ],
+)
+def test_is_dataclass(value: Any, expected: bool) -> None:
+ """Validate is_dataclass returns True for dataclass classes and instances."""
+ assert is_dataclass(value) is expected
-def test_is_attrs_schema_when_not_installed() -> None:
- """Test is_attrs_schema returns False when attrs not available."""
- assert is_attrs_schema("not attrs") is False
- assert is_attrs_schema(dict) is False
+@pytest.mark.parametrize(
+ ("target", "field_name", "expected"),
+ [
+ pytest.param(SampleDataclass(name="test", age=25), "name", True, id="existing_field_name"),
+ pytest.param(SampleDataclass(name="test", age=25), "age", True, id="existing_field_age"),
+ pytest.param(SampleDataclass(name="test", age=25), "nonexistent", False, id="missing_field"),
+ pytest.param("not a dataclass", "any_field", False, id="non_dataclass"),
+ ],
+)
+def test_is_dataclass_with_field(target: Any, field_name: str, expected: bool) -> None:
+ """Validate is_dataclass_with_field returns True when field exists on dataclass."""
+ assert is_dataclass_with_field(target, field_name) is expected
-def test_is_attrs_instance_with_field_when_not_installed() -> None:
- """Test is_attrs_instance_with_field returns False when attrs not available."""
- assert is_attrs_instance_with_field("not attrs", "field") is False
+@pytest.mark.parametrize(
+ ("target", "field_name", "expected"),
+ [
+ pytest.param(SampleDataclass(name="test", age=25), "nonexistent", True, id="missing_field"),
+ pytest.param(SampleDataclass(name="test", age=25), "name", False, id="existing_field"),
+ pytest.param("not a dataclass", "any_field", False, id="non_dataclass"),
+ ],
+)
+def test_is_dataclass_without_field(target: Any, field_name: str, expected: bool) -> None:
+ """Validate is_dataclass_without_field returns True when field is absent from dataclass."""
+ assert is_dataclass_without_field(target, field_name) is expected
-def test_is_attrs_instance_without_field_when_not_installed() -> None:
- """Test is_attrs_instance_without_field returns False when attrs not available."""
- assert is_attrs_instance_without_field("not attrs", "field") is False
+@pytest.mark.parametrize(
+ ("value", "expected"),
+ [
+ pytest.param({}, True, id="empty_dict"),
+ pytest.param({"key": "value"}, True, id="populated_dict"),
+ pytest.param("not a dict", False, id="string"),
+ pytest.param([], False, id="list"),
+ pytest.param(42, False, id="integer"),
+ ],
+)
+def test_is_dict(value: Any, expected: bool) -> None:
+ """Validate is_dict returns True for dictionaries only."""
+ assert is_dict(value) is expected
-def test_is_schema_with_dataclass() -> None:
- """Test is_schema returns True for dataclass instances."""
- instance = SampleDataclass(name="test", age=25)
- assert is_schema(instance) is True
+@pytest.mark.parametrize(
+ ("target", "field_name", "expected"),
+ [
+ pytest.param({"name": "test", "age": 25}, "name", True, id="existing_key_name"),
+ pytest.param({"name": "test", "age": 25}, "age", True, id="existing_key_age"),
+ pytest.param({"name": "test"}, "nonexistent", False, id="missing_key"),
+ pytest.param("not a dict", "any_key", False, id="non_dict"),
+ ],
+)
+def test_is_dict_with_field(target: Any, field_name: str, expected: bool) -> None:
+ """Validate is_dict_with_field returns True when key exists in dictionary."""
+ assert is_dict_with_field(target, field_name) is expected
-def test_is_schema_with_non_schema() -> None:
- """Test is_schema returns False for non-schema objects."""
- assert is_schema("not a schema") is False
- assert is_schema(42) is False
- assert is_schema([]) is False
+@pytest.mark.parametrize(
+ ("target", "field_name", "expected"),
+ [
+ pytest.param({"name": "test"}, "nonexistent", True, id="missing_key"),
+ pytest.param({"name": "test", "age": 25}, "name", False, id="existing_key"),
+ pytest.param("not a dict", "any_key", False, id="non_dict"),
+ ],
+)
+def test_is_dict_without_field(target: Any, field_name: str, expected: bool) -> None:
+ """Validate is_dict_without_field returns True when key is absent from dictionary."""
+ assert is_dict_without_field(target, field_name) is expected
-def test_is_schema_or_dict_with_schema() -> None:
- """Test is_schema_or_dict returns True for schema objects."""
- instance = SampleDataclass(name="test", age=25)
- assert is_schema_or_dict(instance) is True
+@pytest.mark.parametrize(
+ ("value", "expected"),
+ [
+ pytest.param({}, True, id="empty_dict"),
+ pytest.param({"col1": "value1", "col2": "value2"}, True, id="populated_dict"),
+ pytest.param("not a dict", False, id="string"),
+ pytest.param([], False, id="list"),
+ pytest.param(42, False, id="integer"),
+ ],
+)
+def test_is_dict_row(value: Any, expected: bool) -> None:
+ """Validate is_dict_row returns True for dictionaries representing row data."""
+ assert is_dict_row(value) is expected
-def test_is_schema_or_dict_with_dict() -> None:
- """Test is_schema_or_dict returns True for dictionaries."""
- assert is_schema_or_dict({"key": "value"}) is True
+@pytest.mark.parametrize(
+ ("guard", "args"),
+ [
+ pytest.param(is_pydantic_model, ("not a model",), id="model_string"),
+ pytest.param(is_pydantic_model, ({},), id="model_dict"),
+ pytest.param(is_pydantic_model_with_field, ("not a model", "field"), id="with_field_string"),
+ pytest.param(is_pydantic_model_without_field, ("not a model", "field"), id="without_field_string"),
+ ],
+)
+def test_pydantic_model_fallback_guards(guard: Any, args: tuple[Any, ...]) -> None:
+ """Validate pydantic guard behavior when handling non-pydantic inputs."""
+ assert guard(*args) is False
-def test_is_schema_or_dict_with_neither() -> None:
- """Test is_schema_or_dict returns False for non-schema, non-dict objects."""
- assert is_schema_or_dict("not schema or dict") is False
- assert is_schema_or_dict(42) is False
+@pytest.mark.parametrize(
+ ("guard", "args"),
+ [
+ pytest.param(is_msgspec_struct, ("not a struct",), id="struct_string"),
+ pytest.param(is_msgspec_struct, ({},), id="struct_dict"),
+ pytest.param(is_msgspec_struct_with_field, ("not a struct", "field"), id="with_field_string"),
+ pytest.param(is_msgspec_struct_without_field, ("not a struct", "field"), id="without_field_string"),
+ ],
+)
+def test_msgspec_struct_fallback_guards(guard: Any, args: tuple[Any, ...]) -> None:
+ """Validate msgspec guard behavior when handling non-struct inputs."""
+ assert guard(*args) is False
-def test_is_schema_with_field_with_dataclass() -> None:
- """Test is_schema_with_field works with dataclass fields."""
- instance = SampleDataclass(name="test", age=25)
- assert is_schema_with_field(instance, "name") is False
- assert is_schema_with_field(instance, "nonexistent") is False
+@pytest.mark.parametrize(
+ ("guard", "args"),
+ [
+ pytest.param(is_attrs_instance, ("not attrs",), id="instance_string"),
+ pytest.param(is_attrs_instance, ({},), id="instance_dict"),
+ pytest.param(is_attrs_schema, ("not attrs",), id="schema_string"),
+ pytest.param(is_attrs_schema, (dict,), id="schema_dict_type"),
+ pytest.param(is_attrs_instance_with_field, ("not attrs", "field"), id="with_field_string"),
+ pytest.param(is_attrs_instance_without_field, ("not attrs", "field"), id="without_field_string"),
+ ],
+)
+def test_attrs_fallback_guards(guard: Any, args: tuple[Any, ...]) -> None:
+ """Validate attrs guard behavior when handling non-attrs inputs."""
+ assert guard(*args) is False
-def test_is_schema_without_field_with_dataclass() -> None:
- """Test is_schema_without_field works with dataclass fields."""
- instance = SampleDataclass(name="test", age=25)
- assert is_schema_without_field(instance, "nonexistent") is True
- assert is_schema_without_field(instance, "name") is True
+@pytest.mark.parametrize(
+ ("value", "expected"),
+ [
+ pytest.param(SampleDataclass(name="test", age=25), True, id="dataclass_instance"),
+ pytest.param("not a schema", False, id="string"),
+ pytest.param(42, False, id="integer"),
+ pytest.param([], False, id="list"),
+ ],
+)
+def test_is_schema(value: Any, expected: bool) -> None:
+ """Validate is_schema returns True for schema objects and False otherwise."""
+ assert is_schema(value) is expected
-def test_is_schema_or_dict_with_field_combined() -> None:
- """Test is_schema_or_dict_with_field works with both schemas and dicts."""
- instance = SampleDataclass(name="test", age=25)
- data = {"name": "test", "age": 25}
- assert is_schema_or_dict_with_field(instance, "name") is False
- assert is_schema_or_dict_with_field(data, "name") is True
- assert is_schema_or_dict_with_field(instance, "nonexistent") is False
- assert is_schema_or_dict_with_field(data, "nonexistent") is False
+@pytest.mark.parametrize(
+ ("value", "expected"),
+ [
+ pytest.param(SampleDataclass(name="test", age=25), True, id="schema_dataclass"),
+ pytest.param({"key": "value"}, True, id="dict"),
+ pytest.param("not schema or dict", False, id="string"),
+ pytest.param(42, False, id="integer"),
+ ],
+)
+def test_is_schema_or_dict(value: Any, expected: bool) -> None:
+ """Validate is_schema_or_dict returns True for schemas and dicts."""
+ assert is_schema_or_dict(value) is expected
-def test_is_schema_or_dict_without_field_combined() -> None:
- """Test is_schema_or_dict_without_field works with both schemas and dicts."""
+@pytest.mark.parametrize(
+ ("field_name", "expected_with", "expected_without"),
+ [pytest.param("name", False, True, id="existing_name"), pytest.param("nonexistent", False, True, id="nonexistent")],
+)
+def test_is_schema_field_guards_with_dataclass(field_name: str, expected_with: bool, expected_without: bool) -> None:
+ """Validate schema field guards on dataclass instances."""
instance = SampleDataclass(name="test", age=25)
- data = {"name": "test", "age": 25}
- assert is_schema_or_dict_without_field(instance, "nonexistent") is True
- assert is_schema_or_dict_without_field(data, "nonexistent") is True
- assert is_schema_or_dict_without_field(instance, "name") is True
- assert is_schema_or_dict_without_field(data, "name") is False
-
-
-def test_is_iterable_parameters_with_list() -> None:
- """Test is_iterable_parameters returns True for lists."""
- assert is_iterable_parameters([1, 2, 3]) is True
- assert is_iterable_parameters([]) is True
-
-
-def test_is_iterable_parameters_with_tuple() -> None:
- """Test is_iterable_parameters returns True for tuples."""
- assert is_iterable_parameters((1, 2, 3)) is True
- assert is_iterable_parameters(()) is True
-
+ assert is_schema_with_field(instance, field_name) is expected_with
+ assert is_schema_without_field(instance, field_name) is expected_without
-def test_is_iterable_parameters_with_string() -> None:
- """Test is_iterable_parameters returns False for strings."""
- assert is_iterable_parameters("string") is False
- assert is_iterable_parameters("") is False
-
-def test_is_iterable_parameters_with_bytes() -> None:
- """Test is_iterable_parameters returns False for bytes."""
- assert is_iterable_parameters(b"bytes") is False
- assert is_iterable_parameters(b"") is False
-
-
-def test_is_iterable_parameters_with_dict() -> None:
- """Test is_iterable_parameters returns False for dictionaries."""
- assert is_iterable_parameters({"key": "value"}) is False
- assert is_iterable_parameters({}) is False
-
-
-def test_is_iterable_parameters_with_non_iterable() -> None:
- """Test is_iterable_parameters returns False for non-iterable objects."""
- assert is_iterable_parameters(42) is False
- assert is_iterable_parameters(None) is False
-
-
-def test_is_dto_data_when_litestar_not_installed() -> None:
- """Test is_dto_data returns False when litestar not available."""
- assert is_dto_data("not dto data") is False
- assert is_dto_data({}) is False
-
-
-def test_is_expression_with_mock() -> None:
- """Test is_expression with mock SQLGlot expressions."""
- mock_expr = cast("exp.Expr", MockSQLGlotExpression())
- result = is_expression(mock_expr)
- assert isinstance(result, bool)
-
-
-def test_is_expression_with_non_expression() -> None:
- """Test is_expression returns False for non-expression objects."""
- assert is_expression("not an expression") is False
- assert is_expression(42) is False
- assert is_expression({}) is False
-
-
-def test_get_node_this_with_this_attribute() -> None:
- """Test get_node_this returns this attribute when present."""
- node = cast("exp.Expr", MockSQLGlotExpression(this="test_value"))
- assert get_node_this(node) == "test_value"
-
-
-def test_get_node_this_without_this_attribute() -> None:
- """Test get_node_this returns default when this attribute missing."""
- node = cast("exp.Expr", MockSQLGlotExpression())
- assert get_node_this(node, "default") == "default"
- assert get_node_this(node) is None
-
-
-def test_has_this_attribute_with_attribute() -> None:
- """Test has_this_attribute returns True when this exists."""
- node = cast("exp.Expr", MockSQLGlotExpression(this="test_value"))
- assert has_this_attribute(node) is True
-
-
-def test_has_this_attribute_without_attribute() -> None:
- """Test has_this_attribute returns False when this doesn't exist."""
- node = cast("exp.Expr", MockSQLGlotExpression())
- assert has_this_attribute(node) is False
-
-
-def test_get_node_expressions_with_expressions() -> None:
- """Test get_node_expressions returns expressions when present."""
- expressions = ["expr1", "expr2"]
- node = cast("exp.Expression", MockSQLGlotExpression(expressions=expressions))
- assert get_node_expressions(node) == expressions
-
-
-def test_get_node_expressions_without_expressions() -> None:
- """Test get_node_expressions returns default when expressions missing."""
- node = cast("exp.Expression", MockSQLGlotExpression())
- assert get_node_expressions(node, "default") == "default"
- assert get_node_expressions(node) is None
-
-
-def test_has_expressions_attribute_with_attribute() -> None:
- """Test has_expressions_attribute returns True when expressions exists."""
- node = cast("exp.Expression", MockSQLGlotExpression(expressions=["expr1"]))
- assert has_expressions_attribute(node) is True
-
-
-def test_has_expressions_attribute_without_attribute() -> None:
- """Test has_expressions_attribute returns False when expressions doesn't exist."""
- node = cast("exp.Expression", MockSQLGlotExpression())
- assert has_expressions_attribute(node) is False
-
-
-def test_get_literal_parent_with_parent() -> None:
- """Test get_literal_parent returns parent when present."""
- parent = "parent_node"
- literal = cast("exp.Expression", MockLiteral(parent=parent))
- assert get_literal_parent(literal) == parent
-
-
-def test_get_literal_parent_without_parent() -> None:
- """Test get_literal_parent returns default when parent missing."""
- literal = cast("exp.Expression", MockLiteral())
- assert get_literal_parent(literal, "default") == "default"
- assert get_literal_parent(literal) is None
-
-
-def test_has_parent_attribute_with_attribute() -> None:
- """Test has_parent_attribute returns True when parent exists."""
- literal = cast("exp.Expression", MockLiteral(parent="parent_node"))
- assert has_parent_attribute(literal) is True
-
-
-def test_has_parent_attribute_without_attribute() -> None:
- """Test has_parent_attribute returns False when parent doesn't exist."""
- literal = cast("exp.Expression", MockLiteral())
- assert has_parent_attribute(literal) is False
+@pytest.mark.parametrize(
+ ("target", "field_name", "expected_with", "expected_without"),
+ [
+ pytest.param(SampleDataclass(name="test", age=25), "name", False, True, id="dataclass_existing_field"),
+ pytest.param({"name": "test", "age": 25}, "name", True, False, id="dict_existing_field"),
+ pytest.param(SampleDataclass(name="test", age=25), "nonexistent", False, True, id="dataclass_missing_field"),
+ pytest.param({"name": "test", "age": 25}, "nonexistent", False, True, id="dict_missing_field"),
+ ],
+)
+def test_is_schema_or_dict_field_guards(
+ target: Any, field_name: str, expected_with: bool, expected_without: bool
+) -> None:
+ """Validate schema or dict field presence guards across schemas and dicts."""
+ assert is_schema_or_dict_with_field(target, field_name) is expected_with
+ assert is_schema_or_dict_without_field(target, field_name) is expected_without
-def test_is_string_literal_with_string_flag() -> None:
- """Test is_string_literal returns True when is_string is True."""
- literal = cast("exp.Literal", MockLiteral(is_string=True))
- assert is_string_literal(literal) is True
+@pytest.mark.parametrize(
+ ("value", "expected"),
+ [
+ pytest.param([1, 2, 3], True, id="populated_list"),
+ pytest.param([], True, id="empty_list"),
+ pytest.param((1, 2, 3), True, id="populated_tuple"),
+ pytest.param((), True, id="empty_tuple"),
+ pytest.param("string", False, id="populated_string"),
+ pytest.param("", False, id="empty_string"),
+ pytest.param(b"bytes", False, id="populated_bytes"),
+ pytest.param(b"", False, id="empty_bytes"),
+ pytest.param({"key": "value"}, False, id="populated_dict"),
+ pytest.param({}, False, id="empty_dict"),
+ pytest.param(42, False, id="integer"),
+ pytest.param(None, False, id="none"),
+ ],
+)
+def test_is_iterable_parameters(value: Any, expected: bool) -> None:
+ """Validate is_iterable_parameters returns True for lists and tuples only."""
+ assert is_iterable_parameters(value) is expected
-def test_is_string_literal_without_string_flag() -> None:
- """Test is_string_literal handles missing is_string attribute."""
- literal = cast("exp.Literal", MockLiteral(this="string_value"))
- assert is_string_literal(literal) is True
+@pytest.mark.parametrize("value", [pytest.param("not dto data", id="string"), pytest.param({}, id="dict")])
+def test_is_dto_data_when_litestar_not_installed(value: Any) -> None:
+ """Validate is_dto_data returns False for non-DTO data."""
+ assert is_dto_data(value) is False
-def test_is_string_literal_with_non_string_this() -> None:
- """Test is_string_literal returns False for non-string this."""
- literal = cast("exp.Literal", MockLiteral(this=42))
- assert is_string_literal(literal) is False
+@pytest.mark.parametrize(
+ ("value", "expected"),
+ [
+ pytest.param(exp.var("x"), True, id="sqlglot_expression"),
+ pytest.param(cast("exp.Expr", MockSQLGlotExpression()), False, id="mock_expression"),
+ pytest.param("not an expression", False, id="string"),
+ pytest.param(42, False, id="integer"),
+ pytest.param({}, False, id="dict"),
+ ],
+)
+def test_is_expression(value: Any, expected: bool) -> None:
+ """Validate is_expression returns True for SQLGlot expressions only."""
+ assert is_expression(value) is expected
-def test_is_number_literal_with_number_flag() -> None:
- """Test is_number_literal returns True when is_number is True."""
- literal = cast("exp.Literal", MockLiteral(is_number=True))
- assert is_number_literal(literal) is True
+@pytest.mark.parametrize(
+ ("node", "expected_this", "expected_has"),
+ [
+ pytest.param(cast("exp.Expr", MockSQLGlotExpression(this="test_value")), "test_value", True, id="with_this"),
+ pytest.param(cast("exp.Expr", MockSQLGlotExpression()), None, False, id="without_this"),
+ ],
+)
+def test_node_this_helpers(node: Any, expected_this: Any, expected_has: bool) -> None:
+ """Validate get_node_this and has_this_attribute behavior."""
+ assert get_node_this(node) == expected_this
+ if expected_this is None:
+ assert get_node_this(node, "default") == "default"
+ assert has_this_attribute(node) is expected_has
-def test_is_number_literal_without_number_flag() -> None:
- """Test is_number_literal handles missing is_number attribute."""
- literal = cast("exp.Literal", MockLiteral(this="123"))
- assert is_number_literal(literal) is True
+@pytest.mark.parametrize(
+ ("node", "expected_expressions", "expected_has"),
+ [
+ pytest.param(
+ cast("exp.Expression", MockSQLGlotExpression(expressions=["expr1", "expr2"])),
+ ["expr1", "expr2"],
+ True,
+ id="with_expressions",
+ ),
+ pytest.param(cast("exp.Expression", MockSQLGlotExpression()), None, False, id="without_expressions"),
+ ],
+)
+def test_node_expressions_helpers(node: Any, expected_expressions: Any, expected_has: bool) -> None:
+ """Validate get_node_expressions and has_expressions_attribute behavior."""
+ assert get_node_expressions(node) == expected_expressions
+ if expected_expressions is None:
+ assert get_node_expressions(node, "default") == "default"
+ assert has_expressions_attribute(node) is expected_has
-def test_is_number_literal_with_non_number_this() -> None:
- """Test is_number_literal returns False for non-numeric this."""
- literal = cast("exp.Literal", MockLiteral(this="not_a_number"))
- assert is_number_literal(literal) is False
+@pytest.mark.parametrize(
+ ("literal", "expected_parent", "expected_has"),
+ [
+ pytest.param(cast("exp.Expression", MockLiteral(parent="parent_node")), "parent_node", True, id="with_parent"),
+ pytest.param(cast("exp.Expression", MockLiteral()), None, False, id="without_parent"),
+ ],
+)
+def test_literal_parent_helpers(literal: Any, expected_parent: Any, expected_has: bool) -> None:
+ """Validate get_literal_parent and has_parent_attribute behavior."""
+ assert get_literal_parent(literal) == expected_parent
+ if expected_parent is None:
+ assert get_literal_parent(literal, "default") == "default"
+ assert has_parent_attribute(literal) is expected_has
-def test_get_param_style_and_name_with_attributes() -> None:
- """Test get_param_style_and_name returns style and name when present."""
- param = MockParameterProtocol(style="named", name="test_param")
- (style, name) = get_param_style_and_name(param)
- assert style == "named"
- assert name == "test_param"
+@pytest.mark.parametrize(
+ ("literal", "expected"),
+ [
+ pytest.param(cast("exp.Literal", MockLiteral(is_string=True)), True, id="string_flag"),
+ pytest.param(cast("exp.Literal", MockLiteral(this="string_value")), True, id="string_this"),
+ pytest.param(cast("exp.Literal", MockLiteral(this="")), True, id="empty_string_this"),
+ pytest.param(cast("exp.Literal", MockLiteral(this=42)), False, id="non_string_this"),
+ ],
+)
+def test_is_string_literal(literal: Any, expected: bool) -> None:
+ """Validate is_string_literal with various mock literal configurations."""
+ assert is_string_literal(literal) is expected
-def test_get_param_style_and_name_without_attributes() -> None:
- """Test get_param_style_and_name returns None, None when attributes missing."""
- param = object()
- (style, name) = get_param_style_and_name(param)
- assert style is None
- assert name is None
+@pytest.mark.parametrize(
+ ("literal", "expected"),
+ [
+ pytest.param(cast("exp.Literal", MockLiteral(is_number=True)), True, id="number_flag"),
+ pytest.param(cast("exp.Literal", MockLiteral(this="123")), True, id="numeric_string_this"),
+ pytest.param(cast("exp.Literal", MockLiteral(this="0")), True, id="zero_string_this"),
+ pytest.param(cast("exp.Literal", MockLiteral(this="not_a_number")), False, id="non_numeric_this"),
+ ],
+)
+def test_is_number_literal(literal: Any, expected: bool) -> None:
+ """Validate is_number_literal with various mock literal configurations."""
+ assert is_number_literal(literal) is expected
-def test_get_value_attribute_with_value() -> None:
- """Test get_value_attribute returns value when present."""
- obj = MockValueWrapper("test_value")
- assert get_value_attribute(obj) == "test_value"
+@pytest.mark.parametrize(
+ ("param", "expected_style", "expected_name"),
+ [
+ pytest.param(
+ MockParameterProtocol(style="named", name="test_param"), "named", "test_param", id="with_attributes"
+ ),
+ pytest.param(object(), None, None, id="without_attributes"),
+ ],
+)
+def test_get_param_style_and_name(param: Any, expected_style: "str | None", expected_name: "str | None") -> None:
+ """Validate get_param_style_and_name with and without protocol attributes."""
+ style, name = get_param_style_and_name(param)
+ assert style == expected_style
+ assert name == expected_name
-def test_get_value_attribute_without_value() -> None:
- """Test get_value_attribute returns object when value missing."""
- obj = "no_value_attribute"
- assert get_value_attribute(obj) == "no_value_attribute"
+@pytest.mark.parametrize(
+ ("target", "expected"),
+ [
+ pytest.param(MockValueWrapper("test_value"), "test_value", id="with_value"),
+ pytest.param("no_value_attribute", "no_value_attribute", id="without_value"),
+ ],
+)
+def test_get_value_attribute(target: Any, expected: Any) -> None:
+ """Validate get_value_attribute returns wrapped value or original object."""
+ assert get_value_attribute(target) == expected
-def test_get_initial_expression_with_attribute() -> None:
- """Test get_initial_expression returns expression when present."""
+@pytest.mark.parametrize(
+ ("has_initial", "expected_match"),
+ [
+ pytest.param(True, True, id="with_initial_expression"),
+ pytest.param(False, False, id="without_initial_expression"),
+ ],
+)
+def test_get_initial_expression(has_initial: bool, expected_match: bool) -> None:
+ """Validate get_initial_expression extracts initial_expression if present."""
mock_expr = MockSQLGlotExpression()
class MockContext:
def __init__(self) -> None:
- self.initial_expression = mock_expr
+ if has_initial:
+ self.initial_expression = mock_expr
context = MockContext()
- assert cast("object", get_initial_expression(context)) is cast("object", mock_expr)
-
-
-def test_get_initial_expression_without_attribute() -> None:
- """Test get_initial_expression returns None when attribute missing."""
- context = object()
- assert get_initial_expression(context) is None
-
-
-def test_expression_has_limit_with_limit() -> None:
- """Test expression_has_limit returns True when limit in args."""
- expr = cast("exp.Expression", MockSQLGlotExpression(args={"limit": "10"}))
- assert expression_has_limit(expr) is True
-
-
-def test_expression_has_limit_without_limit() -> None:
- """Test expression_has_limit returns False when no limit in args."""
- expr = cast("exp.Expression", MockSQLGlotExpression(args={"other": "value"}))
- assert expression_has_limit(expr) is False
-
-
-def test_expression_has_limit_with_none() -> None:
- """Test expression_has_limit returns False for None expression."""
- assert expression_has_limit(None) is False
+ result = get_initial_expression(context)
+ if expected_match:
+ assert cast("object", result) is cast("object", mock_expr)
+ else:
+ assert result is None
-def test_expression_has_limit_without_args() -> None:
- """Test expression_has_limit handles missing args attribute."""
- expr = cast("exp.Expression", object())
- assert expression_has_limit(expr) is False
-
-
-def test_is_copy_statement_with_none() -> None:
- """Test is_copy_statement returns False for None."""
- assert is_copy_statement(None) is False
+@pytest.mark.parametrize(
+ ("expr", "expected"),
+ [
+ pytest.param(cast("exp.Expression", MockSQLGlotExpression(args={"limit": "10"})), True, id="with_limit"),
+ pytest.param(cast("exp.Expression", MockSQLGlotExpression(args={"other": "value"})), False, id="without_limit"),
+ pytest.param(None, False, id="none"),
+ pytest.param(cast("exp.Expression", object()), False, id="without_args"),
+ ],
+)
+def test_expression_has_limit(expr: Any, expected: bool) -> None:
+ """Validate expression_has_limit across various expression shapes."""
+ assert expression_has_limit(expr) is expected
-def test_is_copy_statement_with_non_expression() -> None:
- """Test is_copy_statement returns False for non-expression objects."""
- assert is_copy_statement("not an expression") is False
- assert is_copy_statement(42) is False
+@pytest.mark.parametrize(
+ "value",
+ [pytest.param(None, id="none"), pytest.param("not an expression", id="string"), pytest.param(42, id="integer")],
+)
+def test_is_copy_statement_non_expression(value: Any) -> None:
+ """Validate is_copy_statement returns False for non-expression objects."""
+ assert is_copy_statement(value) is False
def test_extract_dataclass_fields_basic() -> None:
@@ -849,6 +762,7 @@ def test_serializer_pipeline_reuses_entry() -> None:
assert pipeline is same_pipeline
+@pytest.mark.skipif(is_compiled(), reason="mypyc direct calls bypass patched metrics globals")
def test_serializer_metrics_track_hits_and_misses(monkeypatch: pytest.MonkeyPatch) -> None:
from sqlspec.utils.serializers import _schema as schema_module
@@ -905,13 +819,19 @@ def test_multiple_type_guards_chain() -> None:
assert is_iterable_parameters([1, 2, 3]) is True
-def test_type_guards_with_none() -> None:
- """Test type guards handle None gracefully."""
- assert is_dict(None) is False
- assert is_dataclass(None) is False
- assert is_schema(None) is False
- assert is_expression(None) is False
- assert is_iterable_parameters(None) is False
+@pytest.mark.parametrize(
+ "guard_func",
+ [
+ pytest.param(is_dict, id="is_dict"),
+ pytest.param(is_dataclass, id="is_dataclass"),
+ pytest.param(is_schema, id="is_schema"),
+ pytest.param(is_expression, id="is_expression"),
+ pytest.param(is_iterable_parameters, id="is_iterable_parameters"),
+ ],
+)
+def test_type_guards_with_none(guard_func: Any) -> None:
+ """Validate that type guards handle None gracefully by returning False."""
+ assert guard_func(None) is False
def test_type_guards_with_empty_containers() -> None:
@@ -972,25 +892,32 @@ class MockMsgspecStructWithoutConfig(msgspec.Struct):
test_name: str = "test"
-def test_get_msgspec_rename_config_with_camel_rename() -> None:
- """Test get_msgspec_rename_config returns 'camel' for camel rename config."""
- schema_type = MockMsgspecStructWithCamelRename
- result = get_msgspec_rename_config(schema_type)
- assert result == "camel"
+class _InvalidConfigStructString:
+ __struct_config__ = "not a dict"
-def test_get_msgspec_rename_config_with_kebab_rename() -> None:
- """Test get_msgspec_rename_config returns 'kebab' for kebab rename config."""
- schema_type = MockMsgspecStructWithKebabRename
- result = get_msgspec_rename_config(schema_type)
- assert result == "kebab"
+class _InvalidConfigStructNone:
+ __struct_config__ = None
-def test_get_msgspec_rename_config_with_pascal_rename() -> None:
- """Test get_msgspec_rename_config returns 'pascal' for pascal rename config."""
- schema_type = MockMsgspecStructWithPascalRename
- result = get_msgspec_rename_config(schema_type)
- assert result == "pascal"
+@pytest.mark.parametrize(
+ ("schema_type", "expected"),
+ [
+ pytest.param(MockMsgspecStructWithCamelRename, "camel", id="camel"),
+ pytest.param(MockMsgspecStructWithKebabRename, "kebab", id="kebab"),
+ pytest.param(MockMsgspecStructWithPascalRename, "pascal", id="pascal"),
+ pytest.param(MockMsgspecStructWithoutRename, None, id="without_rename"),
+ pytest.param(MockMsgspecStructWithoutConfig, None, id="without_struct_config"),
+ pytest.param(SampleDataclass, None, id="dataclass"),
+ pytest.param(dict, None, id="dict"),
+ pytest.param(list, None, id="list"),
+ pytest.param(_InvalidConfigStructString, None, id="invalid_config_string"),
+ pytest.param(_InvalidConfigStructNone, None, id="invalid_config_none"),
+ ],
+)
+def test_get_msgspec_rename_config(schema_type: Any, expected: "str | None") -> None:
+ """Validate get_msgspec_rename_config handles configured and unconfigured types."""
+ assert get_msgspec_rename_config(schema_type) == expected
def test_get_msgspec_rename_config_caches_per_type(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -1012,71 +939,6 @@ def count_fields(schema_type: Any) -> Any:
assert call_count == 1
-def test_is_typed_dict_with_typeddict_class() -> None:
- """Test is_typed_dict returns True for TypedDict classes."""
- assert is_typed_dict(SampleTypedDict) is True
-
-
-def test_is_typed_dict_with_typeddict_instance() -> None:
- """Test is_typed_dict returns False for TypedDict instances (they are dicts)."""
- sample_data: SampleTypedDict = {"name": "test", "age": 25, "optional_field": "value"}
- assert is_typed_dict(sample_data) is False
-
-
-def test_is_typed_dict_with_non_typeddict() -> None:
- """Test is_typed_dict returns False for non-TypedDict types."""
- assert is_typed_dict(dict) is False
- assert is_typed_dict(SampleDataclass) is False
- assert is_typed_dict(str) is False
- assert is_typed_dict(42) is False
- assert is_typed_dict({}) is False
-
-
-def test_is_typed_dict_with_regular_dict() -> None:
- """Test is_typed_dict returns False for regular dict instances."""
- assert is_typed_dict({"key": "value"}) is False
-
-
-def test_get_msgspec_rename_config_without_rename() -> None:
- """Test get_msgspec_rename_config returns None when no rename config."""
- schema_type = MockMsgspecStructWithoutRename
- result = get_msgspec_rename_config(schema_type)
- assert result is None
-
-
-def test_get_msgspec_rename_config_without_struct_config() -> None:
- """Test get_msgspec_rename_config returns None when no __struct_config__."""
- schema_type = MockMsgspecStructWithoutConfig
- result = get_msgspec_rename_config(schema_type)
- assert result is None
-
-
-def test_get_msgspec_rename_config_with_non_msgspec_class() -> None:
- """Test get_msgspec_rename_config returns None for non-msgspec classes."""
- result = get_msgspec_rename_config(SampleDataclass)
- assert result is None
- result = get_msgspec_rename_config(dict)
- assert result is None
- result = get_msgspec_rename_config(list)
- assert result is None
-
-
-def test_get_msgspec_rename_config_with_invalid_config_structure() -> None:
- """Test get_msgspec_rename_config handles invalid config structures."""
-
- class InvalidConfigStruct:
- __struct_config__ = "not a dict"
-
- result = get_msgspec_rename_config(InvalidConfigStruct)
- assert result is None
-
- class InvalidConfigStruct2:
- __struct_config__ = None
-
- result = get_msgspec_rename_config(InvalidConfigStruct2)
- assert result is None
-
-
def test_get_msgspec_rename_config_performance() -> None:
"""Test get_msgspec_rename_config performs efficiently."""
schema_type = MockMsgspecStructWithCamelRename
@@ -1085,50 +947,68 @@ def test_get_msgspec_rename_config_performance() -> None:
assert result == "camel"
-def test_supports_arrow_results_with_protocol_implementation() -> None:
- """Test supports_arrow_results with object implementing SupportsArrowResults."""
-
- class MockDriverWithArrow:
- def select_to_arrow(
- self,
- statement,
- /,
- *parameters,
- statement_config=None,
- return_format="table",
- native_only=False,
- batch_size=None,
- arrow_schema=None,
- **kwargs,
- ):
- pass
-
- driver = MockDriverWithArrow()
- assert supports_arrow_results(driver) is True
-
-
-def test_supports_arrow_results_without_protocol_implementation() -> None:
- """Test supports_arrow_results with object not implementing protocol."""
-
- class MockDriverWithoutArrow:
- def execute(self, sql):
- pass
+@pytest.mark.parametrize(
+ ("target", "expected"),
+ [
+ pytest.param(SampleTypedDict, True, id="typed_dict_class"),
+ pytest.param(
+ cast("SampleTypedDict", {"name": "test", "age": 25, "optional_field": "value"}),
+ False,
+ id="typed_dict_instance",
+ ),
+ pytest.param(dict, False, id="dict_type"),
+ pytest.param(SampleDataclass, False, id="dataclass_type"),
+ pytest.param(str, False, id="str_type"),
+ pytest.param(42, False, id="integer"),
+ pytest.param({}, False, id="empty_dict"),
+ pytest.param({"key": "value"}, False, id="dict_instance"),
+ ],
+)
+def test_is_typed_dict(target: Any, expected: bool) -> None:
+ """Validate is_typed_dict distinguishes TypedDict classes from instances and other types."""
+ assert is_typed_dict(target) is expected
+
+
+class MockDriverWithArrow:
+ """Mock driver implementing SupportsArrowResults protocol."""
+
+ def select_to_arrow(
+ self,
+ statement: Any,
+ /,
+ *parameters: Any,
+ statement_config: Any = None,
+ return_format: str = "table",
+ native_only: bool = False,
+ batch_size: Any = None,
+ arrow_schema: Any = None,
+ **kwargs: Any,
+ ) -> None:
+ pass
- driver = MockDriverWithoutArrow()
- assert supports_arrow_results(driver) is False
+class MockDriverWithoutArrow:
+ """Mock driver not implementing SupportsArrowResults protocol."""
-def test_supports_arrow_results_with_none() -> None:
- """Test supports_arrow_results with None."""
- assert supports_arrow_results(None) is False
+ def execute(self, sql: Any) -> None:
+ pass
-def test_supports_arrow_results_with_primitive_types() -> None:
- """Test supports_arrow_results with primitive types."""
- assert supports_arrow_results("string") is False
- assert supports_arrow_results(42) is False
- assert supports_arrow_results([1, 2, 3]) is False
- assert supports_arrow_results({"key": "value"}) is False
+@pytest.mark.parametrize(
+ ("target", "expected"),
+ [
+ pytest.param(MockDriverWithArrow(), True, id="supports_arrow"),
+ pytest.param(MockDriverWithoutArrow(), False, id="missing_arrow_method"),
+ pytest.param(None, False, id="none"),
+ pytest.param("string", False, id="string"),
+ pytest.param(42, False, id="integer"),
+ pytest.param([1, 2, 3], False, id="list"),
+ pytest.param({"key": "value"}, False, id="dict"),
+ ],
+)
+def test_supports_arrow_results(target: Any, expected: bool) -> None:
+ """Validate supports_arrow_results against protocol-compliant and non-compliant objects."""
+ assert supports_arrow_results(target) is expected
def test_typing_module_supported_schema_model_includes_mapping() -> None:
diff --git a/tools/benchmark_cache_key.py b/tools/benchmark_cache_key.py
deleted file mode 100644
index c444434a2..000000000
--- a/tools/benchmark_cache_key.py
+++ /dev/null
@@ -1,29 +0,0 @@
-import hashlib
-import time
-
-SQL = "INSERT INTO notes (body) VALUES (?)"
-PARAM_FINGERPRINT = "seq:(str,)"
-HASH_DATA = (SQL, PARAM_FINGERPRINT, "qmark", "qmark", "sqlite", False)
-ITERATIONS = 10000
-
-
-def bench_make_cache_key() -> float:
- start = time.perf_counter()
- for _ in range(ITERATIONS):
- # Current logic in SQLProcessor._make_cache_key
- hash_str = hashlib.blake2b(repr(HASH_DATA).encode("utf-8"), digest_size=8).hexdigest()
- _ = f"sql_{hash_str}"
- return time.perf_counter() - start
-
-
-def bench_tuple_key() -> float:
- start = time.perf_counter()
- for _ in range(ITERATIONS):
- # Alternative: use tuple directly as key
- _ = HASH_DATA
- return time.perf_counter() - start
-
-
-if __name__ == "__main__":
- bench_make_cache_key()
- bench_tuple_key()
diff --git a/tools/benchmark_results.py b/tools/benchmark_results.py
deleted file mode 100644
index c55dc2702..000000000
--- a/tools/benchmark_results.py
+++ /dev/null
@@ -1,29 +0,0 @@
-import time
-
-ROWS = 10000
-COLS = 5
-COL_NAMES = [f"col_{i}" for i in range(COLS)]
-DATA = [tuple(range(COLS)) for _ in range(ROWS)]
-
-
-def bench_fetchall_sim() -> list[tuple[int, ...]]:
- # Simulate fetchall() returning list of tuples
- start = time.perf_counter()
- res = list(DATA)
- time.perf_counter() - start
- return res
-
-
-def bench_dict_construction() -> list[dict[str, int]]:
- rows = list(DATA)
- names = COL_NAMES
- start = time.perf_counter()
- # This matches sqlspec/adapters/sqlite/core.py:collect_rows
- data = [dict(zip(names, row, strict=False)) for row in rows]
- time.perf_counter() - start
- return data
-
-
-if __name__ == "__main__":
- bench_fetchall_sim()
- bench_dict_construction()
diff --git a/tools/benchmark_sqlglot.py b/tools/benchmark_sqlglot.py
deleted file mode 100644
index d92925b92..000000000
--- a/tools/benchmark_sqlglot.py
+++ /dev/null
@@ -1,37 +0,0 @@
-import time
-
-import sqlglot
-
-SQL = "INSERT INTO notes (body) VALUES (?)"
-DIALECT = "sqlite"
-ITERATIONS = 10000
-
-
-def bench_parse() -> float:
- start = time.perf_counter()
- for _ in range(ITERATIONS):
- sqlglot.parse_one(SQL, read=DIALECT)
- return time.perf_counter() - start
-
-
-def bench_build() -> float:
- parsed = sqlglot.parse_one(SQL, read=DIALECT)
- start = time.perf_counter()
- for _ in range(ITERATIONS):
- parsed.sql(dialect=DIALECT)
- return time.perf_counter() - start
-
-
-def bench_raw_string() -> float:
- start = time.perf_counter()
- for _ in range(ITERATIONS):
- _ = str(SQL)
- return time.perf_counter() - start
-
-
-if __name__ == "__main__":
- parse_time = bench_parse()
- build_time = bench_build()
- raw_time = bench_raw_string()
-
- total_sqlglot = parse_time + build_time
diff --git a/tools/benchmark_transform.py b/tools/benchmark_transform.py
deleted file mode 100644
index 6bd611e36..000000000
--- a/tools/benchmark_transform.py
+++ /dev/null
@@ -1,26 +0,0 @@
-import time
-
-from sqlspec.core.parameters import ParameterProfile, ParameterStyle, ParameterStyleConfig
-
-# Mocking enough state for _transform_cached_parameters
-CONFIG = ParameterStyleConfig(ParameterStyle.QMARK)
-PROFILE = ParameterProfile([]) # Simplified
-PARAMS = ("note",)
-INPUT_NAMES = ()
-
-
-def bench_transform() -> None:
- from sqlspec.core.parameters import ParameterProcessor
-
- proc = ParameterProcessor()
-
- start = time.perf_counter()
- for _ in range(10000):
- _ = proc._transform_cached_parameters(
- PARAMS, PROFILE, CONFIG, input_named_parameters=INPUT_NAMES, is_many=False, apply_wrap_types=False
- )
- time.perf_counter() - start
-
-
-if __name__ == "__main__":
- bench_transform()
diff --git a/tools/build_docs.py b/tools/build_docs.py
deleted file mode 100644
index 8ee090501..000000000
--- a/tools/build_docs.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from __future__ import annotations
-
-import argparse
-import shutil
-import subprocess
-from pathlib import Path
-
-parser = argparse.ArgumentParser()
-parser.add_argument("output")
-
-
-def build(output_dir: str) -> None:
- subprocess.run(["make", "docs"], check=True) # noqa: S607
-
- docs_src_path = Path("docs/_build/html")
- output_path = Path(output_dir)
-
- output_path.mkdir(parents=True, exist_ok=True)
- output_path.joinpath(".nojekyll").touch(exist_ok=True)
-
- for item in docs_src_path.iterdir():
- dest = output_path / item.name
- if item.is_dir():
- shutil.copytree(item, dest, dirs_exist_ok=True)
- else:
- shutil.copy2(item, dest)
-
-
-def main() -> None:
- args = parser.parse_args()
- build(output_dir=args.output)
-
-
-if __name__ == "__main__":
- main()
diff --git a/tools/fix_documentation.py b/tools/fix_documentation.py
deleted file mode 100644
index 077ee71d7..000000000
--- a/tools/fix_documentation.py
+++ /dev/null
@@ -1,227 +0,0 @@
-# ruff: noqa: T201
-"""Fix all critical documentation issues identified in validation report."""
-
-import re
-from pathlib import Path
-
-DOCS_DIR = Path(__file__).parent.parent / "docs"
-
-
-def fix_base_rst() -> None:
- """Fix docs/reference/base.rst - remove non-existent classes."""
- file_path = DOCS_DIR / "reference" / "base.rst"
- content = file_path.read_text()
-
- # Remove SQLConfig section
- content = re.sub(
- r"Configuration Types\n=+\n\n\.\. autoclass:: SQLConfig.*?(?=\n\n[A-Z]|\n\nConnection Pooling)",
- "Configuration Types\n===================\n\nAll database adapter configurations inherit from base protocol classes defined in ``sqlspec.config``.",
- content,
- flags=re.DOTALL,
- )
-
- # Remove ConnectionPoolConfig section
- content = re.sub(
- r"Connection Pooling\n=+\n\n\.\. autoclass:: ConnectionPoolConfig.*?(?=\n\n[A-Z]|\nSession)",
- "Connection Pooling\n==================\n\nConnection pooling is configured via adapter-specific TypedDicts passed to the ``pool_config`` parameter.",
- content,
- flags=re.DOTALL,
- )
-
- # Remove SessionProtocol sections
- content = re.sub(
- r"Session Protocols\n-+\n\n\.\. autoclass:: .*?SessionProtocol.*?(?=\n\n[A-Z]|\n\n\.\. autoclass)",
- "Session Protocols\n-----------------\n\nSessions are provided by driver adapter classes: ``SyncDriverAdapterBase`` and ``AsyncDriverAdapterBase``.",
- content,
- flags=re.DOTALL,
- )
-
- # Remove on_startup/on_shutdown examples
- content = content.replace("await sql.on_startup()", "# Pools created lazily on first use")
- content = content.replace("await sql.on_shutdown()", "await sql.close_all_pools()")
-
- file_path.write_text(content)
- print(f"✅ Fixed {file_path}")
-
-
-def fix_driver_rst() -> None:
- """Fix docs/reference/driver.rst - correct class names and methods."""
- file_path = DOCS_DIR / "reference" / "driver.rst"
- content = file_path.read_text()
-
- # Fix class names
- content = content.replace("BaseSyncDriver", "SyncDriverAdapterBase")
- content = content.replace("BaseAsyncDriver", "AsyncDriverAdapterBase")
-
- # Remove begin_transaction context manager example
- content = re.sub(r"async with driver\.transaction\(\):.*?\n", "", content, flags=re.DOTALL)
-
- file_path.write_text(content)
- print(f"✅ Fixed {file_path}")
-
-
-def fix_adapters_rst() -> None:
- """Fix docs/reference/adapters.rst - correct config classes."""
- file_path = DOCS_DIR / "reference" / "adapters.rst"
- content = file_path.read_text()
-
- # Fix Psycopg config
- content = re.sub(
- r"PsycopgConfig\([^)]*is_async=True[^)]*\)",
- 'PsycopgAsyncConfig(connection_config={"conninfo": "postgresql://user:pass@localhost/db"})',
- content,
- )
- content = re.sub(
- r"PsycopgConfig\([^)]*is_async=False[^)]*\)",
- 'PsycopgSyncConfig(connection_config={"conninfo": "postgresql://user:pass@localhost/db"})',
- content,
- )
-
- # Fix Oracle config
- content = re.sub(
- r"OracleDBConfig\([^)]*is_async=True[^)]*\)",
- 'OracleAsyncConfig(connection_config={"user": "system", "password": "oracle", "dsn": "localhost:1521/xe"})',
- content,
- )
- content = re.sub(
- r"OracleDBConfig\([^)]*is_async=False[^)]*\)",
- 'OracleSyncConfig(connection_config={"user": "system", "password": "oracle", "dsn": "localhost:1521/xe"})',
- content,
- )
-
- file_path.write_text(content)
- print(f"✅ Fixed {file_path}")
-
-
-def fix_extensions_rst() -> None:
- """Fix docs/reference/extensions.rst - remove empty integrations."""
- file_path = DOCS_DIR / "reference" / "extensions.rst"
- content = file_path.read_text()
-
- # Remove FastAPI, Flask, Sanic, Starlette sections
- content = re.sub(
- r"FastAPI Integration\n=+.*?(?=\n\n[A-Z][a-z]+ Integration\n=|Litestar Integration)",
- "",
- content,
- flags=re.DOTALL,
- )
- content = re.sub(
- r"Flask Integration\n=+.*?(?=\n\n[A-Z][a-z]+ Integration\n=|Litestar Integration)", "", content, flags=re.DOTALL
- )
- content = re.sub(
- r"Sanic Integration\n=+.*?(?=\n\n[A-Z][a-z]+ Integration\n=|Litestar Integration)", "", content, flags=re.DOTALL
- )
- content = re.sub(
- r"Starlette Integration\n=+.*?(?=\n\n[A-Z][a-z]+ Integration\n=|Litestar Integration)",
- "",
- content,
- flags=re.DOTALL,
- )
-
- # Remove SQLSpecConfig and SQLSpecSessionBackend
- content = re.sub(r"\.\. autoclass:: SQLSpecConfig.*?(?=\n\n\.\. autoclass|\n\n[A-Z])", "", content, flags=re.DOTALL)
- content = re.sub(
- r"\.\. autoclass:: SQLSpecSessionBackend.*?(?=\n\n\.\. autoclass|\n\n[A-Z])",
- ".. autoclass:: BaseSQLSpecStore\n :members:\n :undoc-members:\n :show-inheritance:\n\n Abstract base class for session storage backends.",
- content,
- flags=re.DOTALL,
- )
-
- file_path.write_text(content)
- print(f"✅ Fixed {file_path}")
-
-
-def fix_configuration_rst() -> None:
- """Fix docs/usage/configuration.rst - remove validation classes."""
- file_path = DOCS_DIR / "usage" / "configuration.rst"
- content = file_path.read_text()
-
- # Remove validation section
- content = re.sub(r"from sqlspec\.core\.validation import.*?\n\n", "", content, flags=re.DOTALL)
- content = re.sub(r"SecurityValidator.*?(?=\n\n[A-Z])", "", content, flags=re.DOTALL)
-
- # Fix ParameterStyle enum values
- content = content.replace("ParameterStyle.FORMAT", "ParameterStyle.POSITIONAL_PYFORMAT")
- content = content.replace("ParameterStyle.PYFORMAT", "ParameterStyle.NAMED_PYFORMAT")
-
- # Remove type_coercion_map references
- content = re.sub(r"type_coercion_map=\{[^}]+\},?\n", "", content)
-
- file_path.write_text(content)
- print(f"✅ Fixed {file_path}")
-
-
-def fix_drivers_and_querying_rst() -> None:
- """Fix docs/usage/drivers_and_querying.rst - correct method names."""
- file_path = DOCS_DIR / "usage" / "drivers_and_querying.rst"
- content = file_path.read_text()
-
- # Remove session.select() method references
- content = re.sub(r"session\.select\([^)]+\)", "session.execute", content)
-
- # Fix begin_transaction to begin
- content = content.replace("session.begin_transaction()", "session.begin()")
- content = content.replace(
- "async with session.begin_transaction():", "# Use session.begin(), session.commit(), session.rollback()"
- )
-
- file_path.write_text(content)
- print(f"✅ Fixed {file_path}")
-
-
-def fix_framework_integrations_rst() -> None:
- """Fix docs/usage/framework_integrations.rst - correct initialization."""
- file_path = DOCS_DIR / "usage" / "framework_integrations.rst"
- content = file_path.read_text()
-
- # Fix SQLSpecPlugin initialization
- content = re.sub(
- r"SQLSpecPlugin\(config=config\)",
- "spec = SQLSpec()\\nspec.add_config(config)\\nsqlspec_plugin = SQLSpecPlugin(sqlspec=spec)",
- content,
- )
-
- # Fix result.data to result.rows
- content = content.replace("result.data", "result.rows")
-
- file_path.write_text(content)
- print(f"✅ Fixed {file_path}")
-
-
-def fix_data_flow_rst() -> None:
- """Fix docs/usage/data_flow.rst - remove non-existent validators."""
- file_path = DOCS_DIR / "usage" / "data_flow.rst"
- content = file_path.read_text()
-
- # Remove validation sections
- content = re.sub(r"\*\*SecurityValidator\*\*.*?(?=\n\n\*\*|\n\nStage)", "", content, flags=re.DOTALL)
- content = re.sub(r"\*\*PerformanceValidator\*\*.*?(?=\n\n\*\*|\n\nStage)", "", content, flags=re.DOTALL)
- content = re.sub(r"\*\*DMLSafetyValidator\*\*.*?(?=\n\n\*\*|\n\nStage)", "", content, flags=re.DOTALL)
- content = re.sub(r"from sqlspec\.core\.validation import.*?\n", "", content)
-
- # Remove ParameterizeLiterals transformer references
- content = re.sub(r"ParameterizeLiterals.*?(?=\n\n[A-Z])", "", content, flags=re.DOTALL)
-
- file_path.write_text(content)
- print(f"✅ Fixed {file_path}")
-
-
-def main() -> None:
- """Apply all documentation fixes."""
- print("🔧 Applying documentation fixes...\n")
-
- fix_base_rst()
- fix_driver_rst()
- fix_adapters_rst()
- fix_extensions_rst()
- fix_configuration_rst()
- fix_drivers_and_querying_rst()
- fix_framework_integrations_rst()
- fix_data_flow_rst()
-
- print("\n✅ All critical documentation fixes applied!")
- print("\nRun 'uv run sphinx-build -b html docs docs/_build/html' to verify.")
-
-
-if __name__ == "__main__":
- main()
diff --git a/tools/hooks/__init__.py b/tools/hooks/__init__.py
deleted file mode 100644
index 870dd5776..000000000
--- a/tools/hooks/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Local quality-gate hooks."""
diff --git a/tools/hooks/no_future_annotations.py b/tools/hooks/no_future_annotations.py
deleted file mode 100644
index 06d4ba6c0..000000000
--- a/tools/hooks/no_future_annotations.py
+++ /dev/null
@@ -1,47 +0,0 @@
-"""Disallow postponed evaluation of annotations in SQLSpec package code."""
-
-import ast
-import sys
-from pathlib import Path
-
-
-def main(paths: list[str]) -> int:
- """Scan Python files for forbidden future-annotations imports.
-
- Args:
- paths: File paths supplied by the hook runner.
-
- Returns:
- A nonzero status when any file imports ``__future__.annotations``.
- """
- offending_paths = [path for raw_path in paths if (path := Path(raw_path)).suffix == ".py" and _is_offending(path)]
- if not offending_paths:
- return 0
-
- sys.stderr.write("Disallowed future import found. Remove `from __future__ import annotations` from:\n")
- for path in offending_paths:
- sys.stderr.write(f" - {path}\n")
- return 1
-
-
-def _is_offending(path: Path) -> bool:
- try:
- source = path.read_text(encoding="utf-8")
- except OSError:
- return False
-
- try:
- tree = ast.parse(source, filename=str(path))
- except SyntaxError:
- return "from __future__ import annotations" in source
-
- return any(
- isinstance(node, ast.ImportFrom)
- and node.module == "__future__"
- and any(alias.name == "annotations" for alias in node.names)
- for node in tree.body
- )
-
-
-if __name__ == "__main__":
- raise SystemExit(main(sys.argv[1:]))
diff --git a/tools/local-infra.sh b/tools/local-infra.sh
deleted file mode 100755
index 6bab7bc20..000000000
--- a/tools/local-infra.sh
+++ /dev/null
@@ -1,686 +0,0 @@
-#!/usr/bin/env bash
-
-# =============================================================================
-# SQLSpec Development Infrastructure Setup
-# =============================================================================
-#
-# A comprehensive script to start and manage development database containers
-# with automatic Docker/Podman detection and non-standard ports.
-#
-# Author: SQLSpec Development Team
-# License: MIT
-# =============================================================================
-
-set -euo pipefail
-
-# -----------------------------------------------------------------------------
-# Configuration and Constants
-# -----------------------------------------------------------------------------
-
-readonly SCRIPT_NAME="$(basename "$0")"
-readonly VERSION="1.0.0"
-
-# Colors and formatting
-readonly RED='\033[0;31m'
-readonly GREEN='\033[0;32m'
-readonly YELLOW='\033[1;33m'
-readonly BLUE='\033[0;34m'
-readonly PURPLE='\033[0;35m'
-readonly CYAN='\033[0;36m'
-readonly WHITE='\033[1;37m'
-readonly BOLD='\033[1m'
-readonly NC='\033[0m' # No Color
-
-# Icons
-readonly CHECK="✓"
-readonly CROSS="✗"
-readonly INFO="ℹ"
-readonly WARN="⚠"
-readonly ROCKET="🚀"
-readonly DATABASE="🗄️"
-readonly CLOUD="☁️"
-
-# Development ports (non-standard to avoid conflicts)
-readonly DEV_POSTGRES_PORT=5433
-readonly DEV_ORACLE_PORT=1522
-readonly DEV_MYSQL_PORT=3307
-readonly DEV_BIGQUERY_PORT=9050
-readonly DEV_MINIO_PORT=9001
-readonly DEV_MINIO_CONSOLE_PORT=9002
-
-# Container names
-readonly POSTGRES_CONTAINER="sqlspec-dev-postgres"
-readonly ORACLE_CONTAINER="sqlspec-dev-oracle"
-readonly MYSQL_CONTAINER="sqlspec-dev-mysql"
-readonly BIGQUERY_CONTAINER="sqlspec-dev-bigquery"
-readonly MINIO_CONTAINER="sqlspec-dev-minio"
-
-# Images
-readonly POSTGRES_IMAGE="postgres:16-alpine"
-readonly ORACLE_IMAGE="gvenzl/oracle-free:23-slim-faststart"
-readonly MYSQL_IMAGE="mysql:8.0"
-readonly BIGQUERY_IMAGE="ghcr.io/goccy/bigquery-emulator:latest"
-readonly MINIO_IMAGE="minio/minio:latest"
-
-# Global variables
-CONTAINER_ENGINE=""
-SERVICES=()
-QUIET_MODE=false
-FORCE_RECREATE=false
-
-# -----------------------------------------------------------------------------
-# Utility Functions
-# -----------------------------------------------------------------------------
-
-print_header() {
- echo ""
- echo -e "${BOLD}${BLUE}════════════════════════════════════════════════════════════════════════════════${NC}"
- echo -e "${BOLD}${WHITE} ${DATABASE} SQLSpec Development Infrastructure Setup v${VERSION} ${DATABASE}${NC}"
- echo -e "${BOLD}${BLUE}════════════════════════════════════════════════════════════════════════════════${NC}"
- echo ""
-}
-
-print_banner() {
- local message="$1"
- echo ""
- echo -e "${BOLD}${CYAN}────────────────────────────────────────────────────────────────────────────────${NC}"
- echo -e "${BOLD}${WHITE} ${message}${NC}"
- echo -e "${BOLD}${CYAN}────────────────────────────────────────────────────────────────────────────────${NC}"
- echo ""
-}
-
-log_info() {
- [[ "$QUIET_MODE" == "true" ]] && return
- echo -e "${BLUE}${INFO}${NC} $1"
-}
-
-log_success() {
- echo -e "${GREEN}${CHECK}${NC} $1"
-}
-
-log_warn() {
- echo -e "${YELLOW}${WARN}${NC} $1"
-}
-
-log_error() {
- echo -e "${RED}${CROSS}${NC} $1" >&2
-}
-
-log_rocket() {
- echo -e "${PURPLE}${ROCKET}${NC} $1"
-}
-
-log_database() {
- echo -e "${CYAN}${DATABASE}${NC} $1"
-}
-
-show_usage() {
- cat << EOF
-${BOLD}USAGE:${NC}
- ${SCRIPT_NAME} [OPTIONS] [SERVICES...]
-
-${BOLD}COMMANDS:${NC}
- up Start development infrastructure
- down Stop development infrastructure
- status Show status of all containers
- list List available services
- cleanup Remove all containers and volumes
-
-${BOLD}SERVICES:${NC}
- postgres PostgreSQL database (port ${DEV_POSTGRES_PORT})
- oracle Oracle Free database (port ${DEV_ORACLE_PORT})
- mysql MySQL database (port ${DEV_MYSQL_PORT})
- bigquery BigQuery emulator (port ${DEV_BIGQUERY_PORT})
- minio MinIO cloud storage (port ${DEV_MINIO_PORT})
- all All services (default)
-
-${BOLD}OPTIONS:${NC}
- -h, --help Show this help message
- -v, --version Show version information
- -q, --quiet Quiet mode (minimal output)
- -f, --force Force recreate containers (up command only)
-
-${BOLD}EXAMPLES:${NC}
- ${SCRIPT_NAME} up # Start all services
- ${SCRIPT_NAME} up postgres mysql # Start only PostgreSQL and MySQL
- ${SCRIPT_NAME} up --force # Force recreate all containers
- ${SCRIPT_NAME} down # Stop all services
- ${SCRIPT_NAME} down postgres # Stop only PostgreSQL
- ${SCRIPT_NAME} status # Show container status
- ${SCRIPT_NAME} cleanup # Clean up everything
-
-${BOLD}PORTS:${NC}
- PostgreSQL: ${DEV_POSTGRES_PORT} Oracle: ${DEV_ORACLE_PORT} MySQL: ${DEV_MYSQL_PORT}
- BigQuery: ${DEV_BIGQUERY_PORT} MinIO: ${DEV_MINIO_PORT}
-
-${BOLD}CONNECTION EXAMPLES:${NC}
- PostgreSQL: postgresql://postgres:postgres@localhost:${DEV_POSTGRES_PORT}/postgres
- Oracle: oracle://system:oracle@localhost:${DEV_ORACLE_PORT}/FREEPDB1
- MySQL: mysql://root:mysql@localhost:${DEV_MYSQL_PORT}/test
- MinIO: http://localhost:${DEV_MINIO_PORT} (admin:password123)
-
-EOF
-}
-
-show_version() {
- echo "${SCRIPT_NAME} version ${VERSION}"
-}
-
-# -----------------------------------------------------------------------------
-# Container Engine Detection
-# -----------------------------------------------------------------------------
-
-detect_container_engine() {
- log_info "Detecting container engine..."
-
- if command -v podman >/dev/null 2>&1; then
- CONTAINER_ENGINE="podman"
- log_success "Found Podman container engine"
- elif command -v docker >/dev/null 2>&1; then
- CONTAINER_ENGINE="docker"
- log_success "Found Docker container engine"
- else
- log_error "Neither Docker nor Podman found. Please install one of them."
- exit 1
- fi
-
- # Test if engine is actually working
- if ! $CONTAINER_ENGINE info >/dev/null 2>&1; then
- log_error "${CONTAINER_ENGINE} is installed but not running or accessible"
- log_info "Try: sudo systemctl start ${CONTAINER_ENGINE}"
- exit 1
- fi
-}
-
-# -----------------------------------------------------------------------------
-# Port Management
-# -----------------------------------------------------------------------------
-
-check_port() {
- local port=$1
- if netstat -tuln 2>/dev/null | grep -q ":${port} " || \
- ss -tuln 2>/dev/null | grep -q ":${port} "; then
- return 1 # Port is in use
- fi
- return 0 # Port is free
-}
-
-wait_for_port() {
- local host=$1
- local port=$2
- local service=$3
- local timeout=${4:-30}
-
- log_info "Waiting for ${service} to be ready on port ${port}..."
-
- for ((i=1; i<=timeout; i++)); do
- if nc -z "$host" "$port" 2>/dev/null; then
- log_success "${service} is ready!"
- return 0
- fi
- sleep 1
- done
-
- log_warn "${service} is not responding after ${timeout} seconds"
- return 1
-}
-
-# -----------------------------------------------------------------------------
-# Container Management
-# -----------------------------------------------------------------------------
-
-container_exists() {
- local name=$1
- $CONTAINER_ENGINE ps -a --format "{{.Names}}" | grep -q "^${name}$"
-}
-
-container_running() {
- local name=$1
- $CONTAINER_ENGINE ps --format "{{.Names}}" | grep -q "^${name}$"
-}
-
-stop_container() {
- local name=$1
- if container_running "$name"; then
- log_info "Stopping container: $name"
- $CONTAINER_ENGINE stop "$name" >/dev/null
- log_success "Stopped: $name"
- fi
-}
-
-remove_container() {
- local name=$1
- if container_exists "$name"; then
- stop_container "$name"
- log_info "Removing container: $name"
- $CONTAINER_ENGINE rm "$name" >/dev/null
- log_success "Removed: $name"
- fi
-}
-
-# -----------------------------------------------------------------------------
-# Service Implementations
-# -----------------------------------------------------------------------------
-
-start_postgres() {
- local container_name="$POSTGRES_CONTAINER"
- log_database "Starting PostgreSQL..."
-
- if container_running "$container_name"; then
- log_success "PostgreSQL is already running"
- return 0
- fi
-
- if ! check_port $DEV_POSTGRES_PORT; then
- log_error "Port $DEV_POSTGRES_PORT is already in use"
- return 1
- fi
-
- [[ "$FORCE_RECREATE" == "true" ]] && remove_container "$container_name"
-
- if ! container_exists "$container_name"; then
- log_info "Creating PostgreSQL container..."
- $CONTAINER_ENGINE run -d \
- --name "$container_name" \
- -p "${DEV_POSTGRES_PORT}:5432" \
- -e POSTGRES_USER=postgres \
- -e POSTGRES_PASSWORD=postgres \
- -e POSTGRES_DB=postgres \
- -e POSTGRES_INITDB_ARGS="--auth-host=scram-sha-256" \
- --tmpfs /var/lib/postgresql/data:noexec,nosuid,size=1G \
- "$POSTGRES_IMAGE" >/dev/null
- else
- log_info "Starting existing PostgreSQL container..."
- $CONTAINER_ENGINE start "$container_name" >/dev/null
- fi
-
- wait_for_port localhost $DEV_POSTGRES_PORT "PostgreSQL"
- log_success "PostgreSQL ready on port $DEV_POSTGRES_PORT"
- echo -e " ${BOLD}Connection:${NC} postgresql://postgres:postgres@localhost:${DEV_POSTGRES_PORT}/postgres"
-}
-
-start_oracle() {
- local container_name="$ORACLE_CONTAINER"
- log_database "Starting Oracle..."
-
- if container_running "$container_name"; then
- log_success "Oracle is already running"
- return 0
- fi
-
- if ! check_port $DEV_ORACLE_PORT; then
- log_error "Port $DEV_ORACLE_PORT is already in use"
- return 1
- fi
-
- [[ "$FORCE_RECREATE" == "true" ]] && remove_container "$container_name"
-
- if ! container_exists "$container_name"; then
- log_info "Creating Oracle container (this may take a while)..."
- $CONTAINER_ENGINE run -d \
- --name "$container_name" \
- -p "${DEV_ORACLE_PORT}:1521" \
- -e ORACLE_PASSWORD=oracle \
- -e ORACLE_DATABASE=FREEPDB1 \
- --tmpfs /opt/oracle/oradata:noexec,nosuid,size=2G \
- "$ORACLE_IMAGE" >/dev/null
- else
- log_info "Starting existing Oracle container..."
- $CONTAINER_ENGINE start "$container_name" >/dev/null
- fi
-
- wait_for_port localhost $DEV_ORACLE_PORT "Oracle" 60
- log_success "Oracle ready on port $DEV_ORACLE_PORT"
- echo -e " ${BOLD}Connection:${NC} oracle://system:oracle@localhost:${DEV_ORACLE_PORT}/FREEPDB1"
-}
-
-start_mysql() {
- local container_name="$MYSQL_CONTAINER"
- log_database "Starting MySQL..."
-
- if container_running "$container_name"; then
- log_success "MySQL is already running"
- return 0
- fi
-
- if ! check_port $DEV_MYSQL_PORT; then
- log_error "Port $DEV_MYSQL_PORT is already in use"
- return 1
- fi
-
- [[ "$FORCE_RECREATE" == "true" ]] && remove_container "$container_name"
-
- if ! container_exists "$container_name"; then
- log_info "Creating MySQL container..."
- $CONTAINER_ENGINE run -d \
- --name "$container_name" \
- -p "${DEV_MYSQL_PORT}:3306" \
- -e MYSQL_ROOT_PASSWORD=mysql \
- -e MYSQL_DATABASE=test \
- -e MYSQL_USER=user \
- -e MYSQL_PASSWORD=password \
- --tmpfs /var/lib/mysql:noexec,nosuid,size=1G \
- "$MYSQL_IMAGE" >/dev/null
- else
- log_info "Starting existing MySQL container..."
- $CONTAINER_ENGINE start "$container_name" >/dev/null
- fi
-
- wait_for_port localhost $DEV_MYSQL_PORT "MySQL"
- log_success "MySQL ready on port $DEV_MYSQL_PORT"
- echo -e " ${BOLD}Connection:${NC} mysql://root:mysql@localhost:${DEV_MYSQL_PORT}/test"
-}
-
-start_bigquery() {
- local container_name="$BIGQUERY_CONTAINER"
- log_database "Starting BigQuery Emulator..."
-
- if container_running "$container_name"; then
- log_success "BigQuery Emulator is already running"
- return 0
- fi
-
- if ! check_port $DEV_BIGQUERY_PORT; then
- log_error "Port $DEV_BIGQUERY_PORT is already in use"
- return 1
- fi
-
- [[ "$FORCE_RECREATE" == "true" ]] && remove_container "$container_name"
-
- if ! container_exists "$container_name"; then
- log_info "Creating BigQuery Emulator container..."
- $CONTAINER_ENGINE run -d \
- --name "$container_name" \
- -p "${DEV_BIGQUERY_PORT}:9050" \
- -e PROJECT_ID=test-project \
- "$BIGQUERY_IMAGE" >/dev/null
- else
- log_info "Starting existing BigQuery Emulator container..."
- $CONTAINER_ENGINE start "$container_name" >/dev/null
- fi
-
- wait_for_port localhost $DEV_BIGQUERY_PORT "BigQuery Emulator"
- log_success "BigQuery Emulator ready on port $DEV_BIGQUERY_PORT"
- echo -e " ${BOLD}Endpoint:${NC} http://localhost:${DEV_BIGQUERY_PORT}"
-}
-
-start_minio() {
- local container_name="$MINIO_CONTAINER"
- log_database "Starting MinIO..."
-
- if container_running "$container_name"; then
- log_success "MinIO is already running"
- return 0
- fi
-
- if ! check_port $DEV_MINIO_PORT || ! check_port $DEV_MINIO_CONSOLE_PORT; then
- log_error "MinIO ports ($DEV_MINIO_PORT, $DEV_MINIO_CONSOLE_PORT) are in use"
- return 1
- fi
-
- [[ "$FORCE_RECREATE" == "true" ]] && remove_container "$container_name"
-
- if ! container_exists "$container_name"; then
- log_info "Creating MinIO container..."
- $CONTAINER_ENGINE run -d \
- --name "$container_name" \
- -p "${DEV_MINIO_PORT}:9000" \
- -p "${DEV_MINIO_CONSOLE_PORT}:9001" \
- -e MINIO_ROOT_USER=admin \
- -e MINIO_ROOT_PASSWORD=password123 \
- --tmpfs /data:noexec,nosuid,size=1G \
- "$MINIO_IMAGE" server /data --console-address ":9001" >/dev/null
- else
- log_info "Starting existing MinIO container..."
- $CONTAINER_ENGINE start "$container_name" >/dev/null
- fi
-
- wait_for_port localhost $DEV_MINIO_PORT "MinIO"
- log_success "MinIO ready on ports $DEV_MINIO_PORT (API) and $DEV_MINIO_CONSOLE_PORT (Console)"
- echo -e " ${BOLD}API:${NC} http://localhost:$DEV_MINIO_PORT"
- echo -e " ${BOLD}Console:${NC} http://localhost:$DEV_MINIO_CONSOLE_PORT (admin:password123)"
-}
-
-# -----------------------------------------------------------------------------
-# Status and Management Functions
-# -----------------------------------------------------------------------------
-
-show_status() {
- print_banner "Container Status"
-
- local containers=("$POSTGRES_CONTAINER" "$ORACLE_CONTAINER" "$MYSQL_CONTAINER" "$BIGQUERY_CONTAINER" "$MINIO_CONTAINER")
- local services=("PostgreSQL" "Oracle" "MySQL" "BigQuery" "MinIO")
- local ports=("$DEV_POSTGRES_PORT" "$DEV_ORACLE_PORT" "$DEV_MYSQL_PORT" "$DEV_BIGQUERY_PORT" "$DEV_MINIO_PORT")
-
- for i in "${!containers[@]}"; do
- local container="${containers[$i]}"
- local service="${services[$i]}"
- local port="${ports[$i]}"
-
- if container_running "$container"; then
- log_success "${service} is running on port ${port}"
- elif container_exists "$container"; then
- log_warn "${service} exists but is stopped"
- else
- echo -e "${BLUE}${INFO}${NC} ${service} is not created"
- fi
- done
-}
-
-stop_all() {
- print_banner "Stopping Development Infrastructure"
-
- local containers=("$POSTGRES_CONTAINER" "$ORACLE_CONTAINER" "$MYSQL_CONTAINER" "$BIGQUERY_CONTAINER" "$MINIO_CONTAINER")
-
- for container in "${containers[@]}"; do
- stop_container "$container"
- done
-
- log_success "All development containers stopped"
-}
-
-stop_services() {
- local services=("$@")
-
- if [[ ${#services[@]} -eq 0 ]] || [[ "${services[0]}" == "all" ]]; then
- stop_all
- return
- fi
-
- print_banner "Stopping Selected Services"
-
- for service in "${services[@]}"; do
- case $service in
- postgres) stop_container "$POSTGRES_CONTAINER" ;;
- oracle) stop_container "$ORACLE_CONTAINER" ;;
- mysql) stop_container "$MYSQL_CONTAINER" ;;
- bigquery) stop_container "$BIGQUERY_CONTAINER" ;;
- minio) stop_container "$MINIO_CONTAINER" ;;
- *) log_error "Unknown service: $service" ;;
- esac
- done
-}
-
-cleanup_all() {
- print_banner "Cleaning Up All Development Containers"
-
- local containers=("$POSTGRES_CONTAINER" "$ORACLE_CONTAINER" "$MYSQL_CONTAINER" "$BIGQUERY_CONTAINER" "$MINIO_CONTAINER")
-
- for container in "${containers[@]}"; do
- remove_container "$container"
- done
-
- log_info "Removing unused volumes..."
- $CONTAINER_ENGINE volume prune -f >/dev/null 2>&1 || true
-
- log_success "Cleanup complete"
-}
-
-list_services() {
- print_banner "Available Services"
-
- echo -e "${BOLD}Database Services:${NC}"
- echo -e " postgres PostgreSQL 16 (port ${DEV_POSTGRES_PORT})"
- echo -e " oracle Oracle Free 23c (port ${DEV_ORACLE_PORT})"
- echo -e " mysql MySQL 8.0 (port ${DEV_MYSQL_PORT})"
- echo -e " bigquery BigQuery Emulator (port ${DEV_BIGQUERY_PORT})"
- echo ""
- echo -e "${BOLD}Storage Services:${NC}"
- echo -e " minio MinIO Cloud Storage (ports ${DEV_MINIO_PORT}, ${DEV_MINIO_CONSOLE_PORT})"
- echo ""
- echo -e "${BOLD}Meta Services:${NC}"
- echo -e " all Start all services (default)"
-}
-
-# -----------------------------------------------------------------------------
-# Main Logic
-# -----------------------------------------------------------------------------
-
-start_services() {
- local services=("$@")
-
- # Default to all services if none specified
- if [[ ${#services[@]} -eq 0 ]]; then
- services=("all")
- fi
-
- # Start services
- print_banner "Starting Development Infrastructure"
-
- local start_all=false
- for service in "${services[@]}"; do
- if [[ "$service" == "all" ]]; then
- start_all=true
- break
- fi
- done
-
- if [[ "$start_all" == "true" ]]; then
- log_rocket "Starting all development services..."
- start_postgres
- start_oracle
- start_mysql
- start_bigquery
- start_minio
- else
- for service in "${services[@]}"; do
- case $service in
- postgres) start_postgres ;;
- oracle) start_oracle ;;
- mysql) start_mysql ;;
- bigquery) start_bigquery ;;
- minio) start_minio ;;
- *) log_error "Unknown service: $service" ;;
- esac
- done
- fi
-
- # Show final status
- echo ""
- log_rocket "Development infrastructure is ready!"
- echo ""
- echo -e "${BOLD}Quick Commands:${NC}"
- echo -e " ${SCRIPT_NAME} status Show container status"
- echo -e " ${SCRIPT_NAME} down Stop all containers"
- echo -e " ${SCRIPT_NAME} cleanup Remove all containers"
- echo ""
-}
-
-main() {
- # Check for command
- if [[ $# -eq 0 ]]; then
- log_error "Missing command. Use --help for usage information."
- exit 1
- fi
-
- local command=""
- local options=()
- local services=()
-
- # Parse command line arguments
- while [[ $# -gt 0 ]]; do
- case $1 in
- -h|--help)
- show_usage
- exit 0
- ;;
- -v|--version)
- show_version
- exit 0
- ;;
- -q|--quiet)
- QUIET_MODE=true
- shift
- ;;
- -f|--force)
- FORCE_RECREATE=true
- shift
- ;;
- up|down|status|list|cleanup)
- if [[ -n "$command" ]]; then
- log_error "Multiple commands specified: $command and $1"
- exit 1
- fi
- command="$1"
- shift
- ;;
- postgres|oracle|mysql|bigquery|minio|all)
- services+=("$1")
- shift
- ;;
- *)
- log_error "Unknown option: $1"
- echo "Use --help for usage information."
- exit 1
- ;;
- esac
- done
-
- # Validate command
- if [[ -z "$command" ]]; then
- log_error "No command specified. Use --help for usage information."
- exit 1
- fi
-
- # Show header for interactive commands
- if [[ "$command" != "status" && "$command" != "list" && "$QUIET_MODE" != "true" ]]; then
- print_header
- fi
-
- # Detect container engine for commands that need it
- if [[ "$command" != "list" ]]; then
- detect_container_engine
- fi
-
- # Execute command
- case $command in
- up)
- start_services "${services[@]}"
- ;;
- down)
- stop_services "${services[@]}"
- ;;
- status)
- show_status
- ;;
- list)
- list_services
- ;;
- cleanup)
- echo -e "${YELLOW}${WARN}${NC} This will remove all development containers and volumes."
- read -r -p "Are you sure? (y/N): " confirm
- if [[ $confirm =~ ^[Yy]$ ]]; then
- cleanup_all
- else
- log_info "Cleanup cancelled"
- fi
- ;;
- *)
- log_error "Unknown command: $command"
- exit 1
- ;;
- esac
-}
-
-# Run main function with all arguments
-main "$@"
diff --git a/tools/pypi_readme.py b/tools/pypi_readme.py
deleted file mode 100644
index f814d5183..000000000
--- a/tools/pypi_readme.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import re
-from pathlib import Path
-
-PYPI_BANNER = '
'
-
-
-def generate_pypi_readme() -> None:
- source = Path("README.md").read_text(encoding="utf-8")
- output = re.sub(r"[\w\W]*", PYPI_BANNER, source)
- output = re.sub(r"[\w\W]*", "", output)
- output = re.sub(r"", "", output)
-
- # ensure a newline here so the other pre-commit hooks don't complain
- output = output.strip() + "\n"
- Path("docs/PYPI_README.md").write_text(output, encoding="utf-8")
-
-
-if __name__ == "__main__":
- generate_pypi_readme()
diff --git a/tools/scripts/mypyc_boundary_map.py b/tools/scripts/mypyc_boundary_map.py
deleted file mode 100644
index 9903b85e5..000000000
--- a/tools/scripts/mypyc_boundary_map.py
+++ /dev/null
@@ -1,428 +0,0 @@
-"""Map current interpreted/compiled hot boundaries for mypyc rollout work."""
-
-import ast
-from fnmatch import fnmatch
-from pathlib import Path
-from typing import Any
-
-__all__ = (
- "ANY_AUDIT_SEAMS",
- "CONFIG_RUNTIME_BOUNDARIES",
- "EXCLUSION_REVALIDATION_SEED",
- "HELPER_SPLIT_DESIGNS",
- "ROLLOUT_FEEDBACK",
- "STORAGE_ARROW_BOUNDARIES",
- "build_boundary_map",
- "classify_module",
- "collect_adapter_core_boundaries",
- "collect_serializer_bridges",
- "load_mypyc_patterns",
-)
-
-try:
- import tomllib # type: ignore[import-not-found]
-except ModuleNotFoundError: # pragma: no cover
- import tomli as tomllib
-
-
-CONFIG_RUNTIME_BOUNDARIES: tuple[dict[str, Any], ...] = (
- {
- "from_module": "sqlspec/config.py",
- "to_module": "sqlspec/core/config_runtime.py",
- "sites": [
- {"line": 11, "symbol": "config_runtime import"},
- {"line": 1210, "symbol": "build_default_statement_config"},
- {"line": 1211, "symbol": "seed_runtime_driver_features"},
- {"line": 1555, "symbol": "create_sync_pool"},
- {"line": 1568, "symbol": "close_sync_pool"},
- {"line": 1752, "symbol": "create_async_pool"},
- {"line": 1765, "symbol": "close_async_pool"},
- ],
- "classification": "interpreted_runtime_helper_boundary",
- "reason": "Base config shells stay interpreted and currently delegate statement defaults, driver feature seeding, and pool helpers to another interpreted runtime helper layer.",
- },
- {
- "from_module": "sqlspec/config.py",
- "to_module": "sqlspec/utils/module_loader.py",
- "sites": [
- {"line": 22, "symbol": "ensure_pyarrow import"},
- {"line": 824, "symbol": "_build_storage_capabilities"},
- {"line": 828, "symbol": "_dependency_available(ensure_pyarrow)"},
- ],
- "classification": "interpreted_optional_dependency_boundary",
- "reason": "Storage capability detection remains interpreted because it probes optional PyArrow availability at runtime.",
- },
-)
-
-
-STORAGE_ARROW_BOUNDARIES: tuple[dict[str, Any], ...] = (
- {
- "from_module": "sqlspec/storage/pipeline.py",
- "to_module": "sqlspec/storage/_arrow_payload.py",
- "sites": [
- {"line": 15, "symbol": "decode_arrow_payload/encode_arrow_payload import"},
- {"line": 226, "symbol": "_encode_arrow_payload"},
- {"line": 342, "symbol": "_decode_arrow_payload"},
- {"line": 357, "symbol": "SyncStoragePipeline.write_arrow"},
- {"line": 382, "symbol": "SyncStoragePipeline.read_arrow"},
- {"line": 500, "symbol": "AsyncStoragePipeline.write_arrow"},
- {"line": 578, "symbol": "AsyncStoragePipeline.read_arrow_async"},
- ],
- "classification": "compiled_to_interpreted_arrow_boundary",
- "reason": "Compiled pipeline orchestration delegates PyArrow payload encoding and decoding to interpreted `_arrow_payload.py`.",
- },
- {
- "from_module": "sqlspec/storage/_arrow_payload.py",
- "to_module": "sqlspec/storage/_utils.py",
- "sites": [{"line": 5, "symbol": "import_pyarrow/import_pyarrow_csv/import_pyarrow_parquet"}],
- "classification": "interpreted_to_compiled_optional_dependency_boundary",
- "reason": "Interpreted Arrow payload codecs call compiled optional-dependency import helpers before importing PyArrow.",
- },
- {
- "from_module": "sqlspec/storage/_utils.py",
- "to_module": "sqlspec/utils/module_loader.py",
- "sites": [
- {"line": 5, "symbol": "ensure_pyarrow import"},
- {"line": 18, "symbol": "import_pyarrow"},
- {"line": 31, "symbol": "import_pyarrow_parquet"},
- {"line": 44, "symbol": "import_pyarrow_csv"},
- ],
- "classification": "interpreted_optional_dependency_boundary",
- "reason": "Arrow helpers remain isolated behind optional-dependency probes in `module_loader.py`.",
- },
- {
- "from_module": "sqlspec/utils/serializers.py",
- "to_module": "sqlspec/utils/serializers/_json.py",
- "sites": [{"line": 4, "symbol": "decode_json as from_json"}, {"line": 5, "symbol": "encode_json as to_json"}],
- "classification": "interpreted_facade_to_compiled_json_boundary",
- "reason": "The interpreted serializers package facade re-exports the compiled JSON engine.",
- },
-)
-
-
-ANY_AUDIT_SEAMS: tuple[dict[str, Any], ...] = (
- {
- "module": "sqlspec/config.py",
- "line": 86,
- "symbol": "_DriverFeatureHookWrapper.__init__",
- "annotation": "Callable[..., Any]",
- "reason": "Lifecycle hook callbacks accept heterogeneous driver/pool/session payloads.",
- },
- {
- "module": "sqlspec/config.py",
- "line": 107,
- "symbol": "LifecycleConfig",
- "annotation": "Callable[[Any], None] and query hooks with dict[str, Any]",
- "reason": "Observability lifecycle hooks bridge raw driver objects and event payload maps.",
- },
- {
- "module": "sqlspec/storage/pipeline.py",
- "line": 198,
- "symbol": "_encode_row_payload",
- "annotation": "list[Any]",
- "reason": "Storage bridge accepts pre-serialized row payloads without schema specialization.",
- },
- {
- "module": "sqlspec/storage/pipeline.py",
- "line": 216,
- "symbol": "_encode_arrow_payload",
- "annotation": "write_options: dict[str, Any] | None",
- "reason": "CSV/Parquet writer options pass backend-specific dictionaries through unchanged.",
- },
- {
- "module": "sqlspec/adapters/psqlpy/config.py",
- "line": 79,
- "symbol": "PsqlpyPoolParams.configure",
- "annotation": "Callable[..., Any]",
- "reason": "psqlpy exposes raw driver configure callbacks that are opaque to the shared config shell.",
- },
- {
- "module": "sqlspec/adapters/psqlpy/config.py",
- "line": 126,
- "symbol": "_PsqlpySessionFactory._ctx",
- "annotation": "Any | None",
- "reason": "Pool acquire context objects are driver-owned and not weakref/Protocol-friendly.",
- },
-)
-
-
-EXCLUSION_REVALIDATION_SEED: dict[str, dict[str, str]] = {
- "sqlspec/utils/arrow_helpers.py": {
- "bucket": "hard_block",
- "reason": "Direct PyArrow table/batch boundary with prior segfault history.",
- },
- "sqlspec/adapters/**/data_dictionary.py": {
- "bucket": "hard_block",
- "reason": "Still carries native_class=False and inline cache patterns to avoid mypyc crashes.",
- },
- "sqlspec/data_dictionary/_loader.py": {
- "bucket": "helper_split",
- "reason": "Path discovery is the risky piece; cache/query wrapper logic is otherwise straightforward.",
- },
- "sqlspec/dialects/**": {
- "bucket": "helper_split",
- "reason": "Custom SQLGlot generator/operator helpers compile, but subclass/registration modules remain interpreted after native class import failures.",
- },
- "sqlspec/dialects/postgres/_pgvector.py": {
- "bucket": "hard_block",
- "reason": "SQLGlot tokenizer/dialect subclass module fails native class import under mypyc.",
- },
- "sqlspec/dialects/postgres/_paradedb.py": {
- "bucket": "hard_block",
- "reason": "SQLGlot dialect subclass module fails native class import under mypyc.",
- },
- "sqlspec/dialects/postgres/_pg_textsearch.py": {
- "bucket": "hard_block",
- "reason": "SQLGlot dialect subclass module fails native class import under mypyc.",
- },
- "sqlspec/dialects/spanner/_spanner.py": {
- "bucket": "hard_block",
- "reason": "SQLGlot tokenizer/dialect subclass module fails native class import under mypyc.",
- },
- "sqlspec/dialects/spanner/_spangres.py": {
- "bucket": "hard_block",
- "reason": "SQLGlot dialect subclass module fails native class import under mypyc.",
- },
- "sqlspec/extensions/events/_channel.py": {
- "bucket": "compiled",
- "reason": "Event channel iteration uses explicit iterator classes and is covered by compiled-wheel smoke.",
- },
- "sqlspec/extensions/events/_models.py": {
- "bucket": "compiled",
- "reason": "EventMessage is a final slot dataclass with concrete annotations and is now in the compiled include set.",
- },
- "sqlspec/extensions/events/_queue.py": {
- "bucket": "compiled",
- "reason": "Table-backed event queue helpers are final, avoid async generators, and are now in the compiled include set.",
- },
- "sqlspec/extensions/adk/converters.py": {
- "bucket": "hard_block",
- "reason": "Imports Google ADK models at module import time and reconstructs Pydantic payloads; compile only record type modules.",
- },
- "sqlspec/observability/_formatting.py": {
- "bucket": "compiled",
- "reason": "OTel console formatter is now included in the compiled observability utility surface.",
- },
- "sqlspec/migrations/commands.py": {
- "bucket": "low_roi",
- "reason": "Large CLI/orchestration shell with dynamic inspection and Rich output, not a hot path.",
- },
-}
-
-
-HELPER_SPLIT_DESIGNS: tuple[dict[str, Any], ...] = (
- {
- "surface": "sqlspec/data_dictionary/_loader.py",
- "split_kind": "extract_loader_state_and_path_resolution",
- "extract_module": "sqlspec/data_dictionary/_loader_core.py",
- "compile_target": "sqlspec/data_dictionary/_loader_core.py",
- "safe_symbols": (
- "build_sql_dir_path",
- "ensure_dialect_path",
- "list_sql_dialects",
- "get_or_create_loader",
- "mark_dialect_loaded",
- "is_dialect_loaded",
- ),
- "keep_interpreted_symbols": (
- "SQL_DIR",
- "DataDictionaryLoader._ensure_dialect_loaded",
- "DataDictionaryLoader.get_query",
- "DataDictionaryLoader.get_query_text",
- "get_data_dictionary_loader",
- ),
- "reason": "Path discovery and loader-cache mutation are straightforward helpers; keep singleton lifecycle and SQLFileLoader orchestration interpreted.",
- "feeds_chapter": "exclusion-revalidation",
- },
- {
- "surface": "sqlspec/adapters/**/data_dictionary.py",
- "split_kind": "extract_query_plans_and_version_resolution",
- "extract_module": "sqlspec/data_dictionary/_plans.py",
- "compile_target": "sqlspec/data_dictionary/_plans.py",
- "safe_symbols": (
- "resolve_schema_name",
- "resolve_feature_flag_from_version",
- "resolve_optimal_type_from_version",
- "build_query_plan",
- "build_sqlite_query_text_plan",
- "collect_index_columns_metadata",
- ),
- "keep_interpreted_symbols": (
- "SyncDataDictionaryBase subclasses",
- "AsyncDataDictionaryBase subclasses",
- "get_version",
- "get_tables",
- "get_columns",
- "get_indexes",
- "get_foreign_keys",
- ),
- "reason": "Cross-module inheritance and driver I/O stay unsafe, but repeated schema resolution, feature gating, and query-plan assembly can be centralized into compiled helpers.",
- "feeds_chapter": "storage-runtime-expansion",
- },
-)
-
-
-ROLLOUT_FEEDBACK: tuple[dict[str, str], ...] = (
- {
- "task_id": "sqlspec-k1a.4",
- "recommendation": "Compile SQLGlot custom dialect helper modules as their own boundary; keep subclass/registration modules interpreted and do not reopen adapter driver compilation for dialect/vector registration.",
- },
- {
- "task_id": "sqlspec-k1a.5",
- "recommendation": "Keep Arrow boundaries interpreted and only route data-dictionary query-plan helpers toward future storage/runtime widening.",
- },
- {
- "task_id": "sqlspec-k1a.6.3",
- "recommendation": "Prioritize `_loader_core.py` and shared data-dictionary plan helpers before any file-level exclusion removal.",
- },
-)
-
-
-def load_mypyc_patterns(root: Path) -> tuple[list[str], list[str]]:
- """Load mypyc include/exclude globs from pyproject.toml."""
-
- config = tomllib.loads((root / "pyproject.toml").read_text())
- mypyc_config = config["tool"]["hatch"]["build"]["targets"]["wheel"]["hooks"]["mypyc"]
- return list(mypyc_config["include"]), list(mypyc_config["exclude"])
-
-
-def classify_module(module_path: str, include_patterns: list[str], exclude_patterns: list[str]) -> str:
- """Classify a module as currently compiled or interpreted."""
-
- included = any(fnmatch(module_path, pattern) for pattern in include_patterns)
- excluded = any(fnmatch(module_path, pattern) for pattern in exclude_patterns)
- return "compiled" if included and not excluded else "interpreted"
-
-
-def _module_path_from_file(root: Path, file_path: Path) -> str:
- return str(file_path.relative_to(root)).replace("\\", "/")
-
-
-def _read_ast(file_path: Path) -> ast.Module:
- return ast.parse(file_path.read_text(), filename=str(file_path))
-
-
-def collect_adapter_core_boundaries(root: Path) -> list[dict[str, Any]]:
- """Collect adapter config.py imports that cross into core.py helpers."""
-
- include_patterns, exclude_patterns = load_mypyc_patterns(root)
- boundaries: list[dict[str, Any]] = []
-
- for config_path in sorted((root / "sqlspec" / "adapters").glob("*/config.py")):
- module_path = _module_path_from_file(root, config_path)
- tree = _read_ast(config_path)
-
- for node in tree.body:
- if not isinstance(node, ast.ImportFrom) or node.module is None:
- continue
- if not node.module.startswith("sqlspec.adapters.") or not node.module.endswith(".core"):
- continue
-
- target_module = f"{node.module.replace('.', '/')}.py"
- imported_symbols = sorted(alias.name for alias in node.names if alias.name != "*")
- boundaries.append({
- "from_module": module_path,
- "from_status": classify_module(module_path, include_patterns, exclude_patterns),
- "to_module": target_module,
- "to_status": classify_module(target_module, include_patterns, exclude_patterns),
- "import_line": node.lineno,
- "helpers": imported_symbols,
- "classification": "interpreted_to_compiled"
- if classify_module(module_path, include_patterns, exclude_patterns) == "interpreted"
- and classify_module(target_module, include_patterns, exclude_patterns) == "compiled"
- else "same_mode_import",
- })
-
- return boundaries
-
-
-def collect_serializer_bridges(root: Path) -> list[dict[str, Any]]:
- """Collect compiled helper modules that import JSON helpers from utils.serializers."""
-
- include_patterns, exclude_patterns = load_mypyc_patterns(root)
- bridges: list[dict[str, Any]] = []
-
- for module_path in sorted(
- str(path.relative_to(root)).replace("\\", "/") for path in (root / "sqlspec").rglob("*.py")
- ):
- if classify_module(module_path, include_patterns, exclude_patterns) != "compiled":
- continue
-
- file_path = root / module_path
- tree = _read_ast(file_path)
- for node in tree.body:
- if not isinstance(node, ast.ImportFrom) or node.module != "sqlspec.utils.serializers":
- continue
- imported_symbols = sorted(alias.name for alias in node.names if alias.name != "*")
- bridges.append({
- "from_module": module_path,
- "from_status": "compiled",
- "via_module": "sqlspec/utils/serializers.py",
- "via_status": classify_module("sqlspec/utils/serializers.py", include_patterns, exclude_patterns),
- "terminal_module": "sqlspec/utils/serializers/_json.py",
- "terminal_status": classify_module(
- "sqlspec/utils/serializers/_json.py", include_patterns, exclude_patterns
- ),
- "import_line": node.lineno,
- "helpers": imported_symbols,
- "classification": "compiled_to_interpreted_facade_to_compiled_json_boundary",
- })
- break
-
- return bridges
-
-
-def build_boundary_map(root: Path | None = None) -> dict[str, Any]:
- """Build the current hot boundary map for mypyc rollout planning."""
-
- project_root = root or Path(__file__).resolve().parents[2]
- include_patterns, exclude_patterns = load_mypyc_patterns(project_root)
-
- config_boundaries = [
- {
- **entry,
- "from_status": classify_module(entry["from_module"], include_patterns, exclude_patterns),
- "to_status": classify_module(entry["to_module"], include_patterns, exclude_patterns),
- }
- for entry in CONFIG_RUNTIME_BOUNDARIES
- ]
- storage_boundaries = [
- {
- **entry,
- "from_status": classify_module(entry["from_module"], include_patterns, exclude_patterns),
- "to_status": classify_module(entry["to_module"], include_patterns, exclude_patterns),
- }
- for entry in STORAGE_ARROW_BOUNDARIES
- ]
- adapter_boundaries = collect_adapter_core_boundaries(project_root)
- serializer_bridges = collect_serializer_bridges(project_root)
-
- interpreted_to_compiled_adapter_edges = [
- entry for entry in adapter_boundaries if entry["classification"] == "interpreted_to_compiled"
- ]
-
- return {
- "summary": {
- "config_runtime_edges": len(config_boundaries),
- "adapter_config_core_edges": len(adapter_boundaries),
- "interpreted_to_compiled_adapter_edges": len(interpreted_to_compiled_adapter_edges),
- "serializer_bridges": len(serializer_bridges),
- "storage_arrow_edges": len(storage_boundaries),
- "any_audit_seams": len(ANY_AUDIT_SEAMS),
- "exclusion_revalidation_buckets": len(EXCLUSION_REVALIDATION_SEED),
- "helper_split_designs": len(HELPER_SPLIT_DESIGNS),
- "rollout_feedback_entries": len(ROLLOUT_FEEDBACK),
- },
- "config_runtime_boundaries": config_boundaries,
- "adapter_config_core_boundaries": adapter_boundaries,
- "serializer_bridges": serializer_bridges,
- "storage_arrow_boundaries": storage_boundaries,
- "any_audit_matrix": list(ANY_AUDIT_SEAMS),
- "exclusion_revalidation_seed": EXCLUSION_REVALIDATION_SEED,
- "helper_split_designs": list(HELPER_SPLIT_DESIGNS),
- "rollout_feedback": list(ROLLOUT_FEEDBACK),
- }
-
-
-if __name__ == "__main__": # pragma: no cover
- pass
diff --git a/tools/scripts/mypyc_smoke.py b/tools/scripts/mypyc_smoke.py
index 4c1c9d70a..d3b226885 100644
--- a/tools/scripts/mypyc_smoke.py
+++ b/tools/scripts/mypyc_smoke.py
@@ -2,6 +2,7 @@
import argparse
import importlib
+import importlib.machinery
import inspect
import json
import subprocess
@@ -13,7 +14,7 @@
__all__ = ("SMOKE_IMPORTS", "SmokeImport", "is_compiled_module", "main", "run_construction_checks", "run_smoke")
-COMPILED_SUFFIXES = (".so", ".pyd")
+COMPILED_SUFFIXES: tuple[str, ...] = tuple(dict.fromkeys((*importlib.machinery.EXTENSION_SUFFIXES, ".so", ".pyd")))
class SmokeImport(NamedTuple):
diff --git a/tools/sphinx_ext/__init__.py b/tools/sphinx_ext/__init__.py
index 849f9faad..77ab57093 100644
--- a/tools/sphinx_ext/__init__.py
+++ b/tools/sphinx_ext/__init__.py
@@ -1,16 +1,15 @@
+"""Sphinx extension package for SQLSpec documentation."""
+
from __future__ import annotations
from typing import TYPE_CHECKING
-from tools.sphinx_ext import changelog, missing_references
+from tools.sphinx_ext import missing_references
if TYPE_CHECKING:
from sphinx.application import Sphinx
def setup(app: Sphinx) -> dict[str, bool]:
- ext_config = {}
- ext_config.update(missing_references.setup(app)) # pyright: ignore[reportUnknownMemberType]
- ext_config.update(changelog.setup(app)) # type: ignore[arg-type] # pyright: ignore[reportUnknownMemberType]
-
- return ext_config # pyright: ignore[reportUnknownVariableType]
+ """Initialize active Sphinx extensions for SQLSpec."""
+ return missing_references.setup(app)
diff --git a/tools/sphinx_ext/changelog.py b/tools/sphinx_ext/changelog.py
deleted file mode 100644
index 7894f727c..000000000
--- a/tools/sphinx_ext/changelog.py
+++ /dev/null
@@ -1,156 +0,0 @@
-"""Sphinx extension for changelog and change directives."""
-
-from __future__ import annotations
-
-from functools import partial
-from typing import TYPE_CHECKING, Any, ClassVar, Literal
-
-from docutils import nodes
-from docutils.parsers.rst import directives
-from sphinx.util.docutils import SphinxDirective
-
-if TYPE_CHECKING:
- from sphinx.application import Sphinx
-
-_GH_BASE_URL = "https://github.com/litestar-org/litestar-htmx"
-
-
-def _parse_gh_reference(raw: str, type_: Literal["issues", "pull"]) -> list[str]:
- return [f"{_GH_BASE_URL}/{type_}/{r.strip()}" for r in raw.split(" ") if r]
-
-
-class Change(nodes.General, nodes.Element):
- pass
-
-
-class ChangeDirective(SphinxDirective):
- required_arguments = 1
- has_content = True
- final_argument_whitespace = True
- option_spec: ClassVar[dict[str, Any]] = { # pyright: ignore[reportIncompatibleVariableOverride]
- "type": partial(directives.choice, values=("feature", "bugfix", "misc")),
- "breaking": directives.flag,
- "issue": directives.unchanged,
- "pr": directives.unchanged,
- }
-
- def run(self) -> list[nodes.Node]:
- self.assert_has_content()
-
- change_type = self.options.get("type", "misc").lower()
- title = self.arguments[0]
-
- change_node = nodes.container("\n".join(self.content))
- change_node.attributes["classes"].append("changelog-change")
-
- self.state.nested_parse(self.content, self.content_offset, change_node)
-
- reference_links = [
- *_parse_gh_reference(self.options.get("issue", ""), "issues"),
- *_parse_gh_reference(self.options.get("pr", ""), "pull"),
- ]
-
- references_paragraph = nodes.paragraph()
- references_paragraph.append(nodes.Text("References: "))
- for i, link in enumerate(reference_links, 1):
- link_node = nodes.inline()
- link_node += nodes.reference("", link, refuri=link, external=True)
- references_paragraph.append(link_node)
- if i != len(reference_links):
- references_paragraph.append(nodes.Text(", "))
-
- change_node.append(references_paragraph)
-
- return [
- Change(
- "",
- change_node,
- title=self.state.inliner.parse(title, 0, self.state.memo, change_node)[0],
- change_type=change_type,
- breaking="breaking" in self.options,
- )
- ]
-
-
-class ChangelogDirective(SphinxDirective):
- required_arguments = 1
- has_content = True
- option_spec: ClassVar[dict[str, Any]] = {"date": directives.unchanged} # pyright: ignore[reportIncompatibleVariableOverride]
-
- def run(self) -> list[nodes.Node]:
- self.assert_has_content()
-
- version = self.arguments[0]
- release_date = self.options.get("date")
-
- changelog_node = nodes.section()
- changelog_node += nodes.title(version, version)
- section_target = nodes.target("", "", ids=[version])
-
- if release_date:
- changelog_node += nodes.strong("", "Released: ")
- changelog_node += nodes.Text(release_date)
-
- self.state.nested_parse(self.content, self.content_offset, changelog_node)
-
- change_group_lists = {
- "feature": nodes.definition_list(),
- "bugfix": nodes.definition_list(),
- "misc": nodes.definition_list(),
- }
-
- change_group_titles = {"bugfix": "Bugfixes", "feature": "Features", "misc": "Other changes"}
-
- nodes_to_remove = []
-
- for i, change_node in enumerate(changelog_node.findall(Change)):
- change_type = change_node.attributes["change_type"]
- title = change_node.attributes["title"]
-
- list_item = nodes.definition_list_item("")
-
- term = nodes.term()
- term += title
- target_id = f"{version}-{change_type}-{i}"
- term += nodes.reference(
- "#", "#", refuri=f"#{target_id}", internal=True, classes=["headerlink"], ids=[target_id]
- )
- if change_node.attributes["breaking"]:
- breaking_notice = nodes.inline("breaking", "breaking")
- breaking_notice.attributes["classes"].append("breaking-change")
- term += breaking_notice
-
- list_item += [term]
-
- list_item += nodes.definition("", change_node.children[0])
-
- nodes_to_remove.append(change_node)
-
- change_group_lists[change_type] += list_item
-
- for node in nodes_to_remove:
- changelog_node.remove(node)
-
- for change_group_type, change_group_list in change_group_lists.items():
- if not change_group_list.children:
- continue
-
- section = nodes.section()
-
- target_id = f"{version}-{change_group_type}"
- target_node = nodes.target("", "", ids=[target_id])
- title = change_group_titles[change_group_type]
-
- section += nodes.title(title, title)
- section += change_group_list
-
- changelog_node += [target_node, section]
-
- return [section_target, changelog_node]
-
-
-def setup(app: Sphinx) -> dict[str, str | bool]:
- app.add_directive("changelog", ChangelogDirective)
- app.add_directive("change", ChangeDirective)
-
- return {"parallel_read_safe": True, "parallel_write_safe": True}
diff --git a/uv.lock b/uv.lock
index 61c51d25f..bb2082fd7 100644
--- a/uv.lock
+++ b/uv.lock
@@ -417,15 +417,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" },
]
-[[package]]
-name = "appnope"
-version = "1.0.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/11/f7/a82489c2b6ebe32d3e2831895ae19c77861f0eadf3bb16034484d965dbb2/appnope-1.0.0.tar.gz", hash = "sha256:685db59cb6043c3c2e528adc0b3bce3a5f8d09bcf7492c6ea650d1b7421f3c49", size = 5454, upload-time = "2026-08-20T21:36:13.748Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/46/c7/6b687cb0f83d2a51017d47953f2ee430ffad6fec2a8ebc964e0718a33eb1/appnope-1.0.0-py3-none-any.whl", hash = "sha256:6fe0c04218aab65c54c4ff81638cdbf848d89f5653b74d68638a137f200dd16e", size = 4158, upload-time = "2026-08-20T21:36:12.444Z" },
-]
-
[[package]]
name = "arrow-odbc"
version = "10.4.2"
@@ -507,15 +498,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0d/45/c7cd8d36d3b506bbd02db5066fae3340284781168f0d08dac25deef5f69d/ast_serialize-0.9.0-cp39-abi3-win_arm64.whl", hash = "sha256:74473258a5c55855d5306c864a5c799fbff03a0f0ea1197346b2b5cc5b4ea48a", size = 1128237, upload-time = "2026-09-02T15:50:43.496Z" },
]
-[[package]]
-name = "asttokens"
-version = "3.0.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/25/1e/faf0f247f6f881b98fc4d6d07e14085cb89d13665084e6d6ac1dc2c03d0b/asttokens-3.0.2.tar.gz", hash = "sha256:3ecdbd8f2cc195f53ccada3a613538bb5f9ef6f6869129f13e03c30a677b8fe2", size = 63136, upload-time = "2026-07-12T03:31:49.084Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d4/2b/04b8a15f3a1c77bc79ddf5c73875327f34b4fa75982df2b76e45e402d364/asttokens-3.0.2-py3-none-any.whl", hash = "sha256:9da13157f5b28becde0bd374fc677dcd3c290614264eff096f167c469cd9f933", size = 28702, upload-time = "2026-07-12T03:31:47.542Z" },
-]
-
[[package]]
name = "async-timeout"
version = "5.0.1"
@@ -740,36 +722,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" },
]
-[[package]]
-name = "beautifulsoup4"
-version = "4.15.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "soupsieve" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" },
-]
-
-[[package]]
-name = "bleach"
-version = "6.4.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "webencodings" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/48/3c/e12ac860709702bd5ebeb9b56a4fe334f1001246ee1b8f2b7ee28912df7d/bleach-6.4.0.tar.gz", hash = "sha256:4202482733d85cedd04e59fcb2f89f4e4c7c385a78d3c3c23c30446843a37452", size = 204857, upload-time = "2026-06-05T13:01:13.734Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl", hash = "sha256:4b6b6a54fff2e69a3dde9d21cc6301220bee3c3cb792187d11403fd795031081", size = 165109, upload-time = "2026-06-05T13:01:12.504Z" },
-]
-
-[package.optional-dependencies]
-css = [
- { name = "tinycss2" },
-]
-
[[package]]
name = "blinker"
version = "1.9.0"
@@ -1207,15 +1159,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
-[[package]]
-name = "comm"
-version = "0.2.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" },
-]
-
[[package]]
name = "covdefaults"
version = "2.3.0"
@@ -1418,44 +1361,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/71/44/711e61f7d014be825ef79b285b047292d1bf893732ac1bc030a351fb517f/cryptography-50.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b", size = 3824006, upload-time = "2026-08-25T19:45:37.281Z" },
]
-[[package]]
-name = "debugpy"
-version = "1.8.21"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f2/aa/12037145b7a56eaa5b29b41872f7a21b538e807e13f32c4d3c46e59be084/debugpy-1.8.21.tar.gz", hash = "sha256:a3c53278e84c94e11bd87c53970ec391d1a67396c8b22609fcac576520e611a6", size = 1697577, upload-time = "2026-06-01T19:30:35.156Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/fc/f3/6b1d4c71f4cbb5360009f928934a03b42906f28fc7b3f7f35f04e58acead/debugpy-1.8.21-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:8eeab7b5462f683452c57c0126aaa5ec4e974ddb705f39ba87dff8818c8e08f9", size = 2113873, upload-time = "2026-06-01T19:30:37.148Z" },
- { url = "https://files.pythonhosted.org/packages/1c/f2/17c3bf91cebc173bfbf5734cd2669723d0a35c0cf9d2fd2124546efeae83/debugpy-1.8.21-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:0fddfdc130ac6d8bfc0415b0409822fa901c8f310e5c945ac5653a0352532344", size = 3004715, upload-time = "2026-06-01T19:30:38.888Z" },
- { url = "https://files.pythonhosted.org/packages/5a/22/1f8efd80c7b5909e760f9cfd0c9e8681d2d35d532f7c0a40760cd4da4a19/debugpy-1.8.21-cp310-cp310-win32.whl", hash = "sha256:72b5d676c4cbfac3bac5bb01c138a4656e843f93f03ce2a5f4e394ad49fbee73", size = 5303455, upload-time = "2026-06-01T19:30:40.52Z" },
- { url = "https://files.pythonhosted.org/packages/da/ce/54c79abd6cccef92fa7b43d97e3acafedf4d645557267ece05e948b5e4b8/debugpy-1.8.21-cp310-cp310-win_amd64.whl", hash = "sha256:a7fe47fd23da57b9e0bec3f4a8ee65a2dc55782455ed7f2141d75ab5d2eaeef5", size = 5331751, upload-time = "2026-06-01T19:30:42.146Z" },
- { url = "https://files.pythonhosted.org/packages/89/fb/cbf306d6e07a313a91e7171a98669054502840931432c227cfd505ee367f/debugpy-1.8.21-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:da456226c7b4c69e35dbe35dcee6623d912000a77816db7856a41af1c72a0264", size = 2203120, upload-time = "2026-06-01T19:30:43.964Z" },
- { url = "https://files.pythonhosted.org/packages/aa/57/aa739bd4ad2cbf96aeb1b20b56918ddd5ae4c28b68709bfcd327f02123ee/debugpy-1.8.21-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:f68b891688e61bdc08b8d364d919ff0051e0b94657b39dcd027bc3173edb7cdc", size = 3059958, upload-time = "2026-06-01T19:30:45.622Z" },
- { url = "https://files.pythonhosted.org/packages/a8/31/453d2c9a23d133fe2c8ec7ca1d816ded52a913487fe3ffef7c01b4b706af/debugpy-1.8.21-cp311-cp311-win32.whl", hash = "sha256:f843a8b08c2edeaf9b1582eed4f25441af21a297c22ff16bf76a662557aa9c9e", size = 5236515, upload-time = "2026-06-01T19:30:47.461Z" },
- { url = "https://files.pythonhosted.org/packages/60/94/6660de2f2d7bf388f229335ba4637646eebabdbf38564cb439a95a9193c9/debugpy-1.8.21-cp311-cp311-win_amd64.whl", hash = "sha256:84c564d8cc701d41843b29a92814c1f1bef6798724ca9d675c284ad9f6a547d7", size = 5256138, upload-time = "2026-06-01T19:30:49.113Z" },
- { url = "https://files.pythonhosted.org/packages/a2/df/bf625547431a9cadc9f4cbfeda38866e2b17f6aed147b625377e87834449/debugpy-1.8.21-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:9f96713896f39c3dff0ee841f47320c3f2983d33c341e009361bb0ebc79adc4e", size = 2483609, upload-time = "2026-06-01T19:30:50.794Z" },
- { url = "https://files.pythonhosted.org/packages/bf/09/59324b903599031ff9faaec1758292409f6561a0ec2492fe4b703327705a/debugpy-1.8.21-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:c193d474f0a211191f2b4449d2d06157c689013035bd952f3b617e0ef422b176", size = 3968900, upload-time = "2026-06-01T19:30:52.341Z" },
- { url = "https://files.pythonhosted.org/packages/14/cd/27f65b805d7fe005c44e1a36b9183ecdfbcdbf9d3e721a5115d461ecc7ee/debugpy-1.8.21-cp312-cp312-win32.whl", hash = "sha256:4743373c1cac7f9e74a1b9915bf1dbe0e900eca657ffb170ae07ac8363205ae9", size = 5336340, upload-time = "2026-06-01T19:30:54.047Z" },
- { url = "https://files.pythonhosted.org/packages/77/1d/c84e30c0c674184948b66f076ab271c01d940618a2824c23cd035a27bc20/debugpy-1.8.21-cp312-cp312-win_amd64.whl", hash = "sha256:bd7ba9dd3daa7c2f942c6ca8d4695a16bf9ac16b63615261c7982bc74f7ed20c", size = 5374751, upload-time = "2026-06-01T19:30:55.891Z" },
- { url = "https://files.pythonhosted.org/packages/77/6b/d817e1f8cc77aa055d37fba092e0febfdff40fe652d8d53d4cd7a86ad98d/debugpy-1.8.21-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:13678151fc401e2d68c9880b91e28714f797d40422994572b24560ef80910a88", size = 2477398, upload-time = "2026-06-01T19:30:57.644Z" },
- { url = "https://files.pythonhosted.org/packages/48/57/412421516afc3055fa577516f00beec3d663f9b0ab330639547ae6c57720/debugpy-1.8.21-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:ecbd158386c31ffe71d46f72d44d56e66331ab9b16cad649156d514368f23ab2", size = 3962096, upload-time = "2026-06-01T19:30:59.235Z" },
- { url = "https://files.pythonhosted.org/packages/c1/62/2c616337cf6ba7b07ebbc97f02c6c945a8e2f76b365e33ee809c32ee36d1/debugpy-1.8.21-cp313-cp313-win32.whl", hash = "sha256:2c2ae706dec41d99a9ca1f7ebc987a83e65578363be6f6b3ac9067504917fae1", size = 5336288, upload-time = "2026-06-01T19:31:00.79Z" },
- { url = "https://files.pythonhosted.org/packages/f8/99/9175103392f84c4b1bf7622888cdc68da07f0ff7d9e581266428f6776033/debugpy-1.8.21-cp313-cp313-win_amd64.whl", hash = "sha256:aa648733047443eb1d07682c4ef287d36a54507b643ffdf38b09a3ef002c72a0", size = 5376567, upload-time = "2026-06-01T19:31:02.56Z" },
- { url = "https://files.pythonhosted.org/packages/ce/3d/f4bbb323a548bfab2af3d6b4ffd9bf22636e55956a1285d317a1de643aad/debugpy-1.8.21-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9bb2a685287a2ac9b181cde89edcec64845cb51de7faaa75badb9a698bc24782", size = 2477209, upload-time = "2026-06-01T19:31:04.157Z" },
- { url = "https://files.pythonhosted.org/packages/8c/2d/6e7ec524984a1702777868de49a4c53202bddac2a432a76a093469587750/debugpy-1.8.21-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:3d6922439bf33fd38a3e2c447869ebc7b97da5cd3d329ff1ef9bc06c4903437e", size = 3927115, upload-time = "2026-06-01T19:31:05.863Z" },
- { url = "https://files.pythonhosted.org/packages/97/47/d1aa6d64005a98a9144647d99306b419396f9ad7bf1d73c119e17a81fb4d/debugpy-1.8.21-cp314-cp314-win32.whl", hash = "sha256:15d4963bd5ffa48f0da0947fd06757fa7621945048a14ad7705431566d3c0e7c", size = 5336724, upload-time = "2026-06-01T19:31:07.711Z" },
- { url = "https://files.pythonhosted.org/packages/5f/67/b905b90d163af11878c1af8abafa4a25206335e112e284e413454543a6da/debugpy-1.8.21-cp314-cp314-win_amd64.whl", hash = "sha256:fe0744a12353406de0ae8ccff0d0a4a666f00801a3db8fd04e7a5f761cd520e8", size = 5373803, upload-time = "2026-06-01T19:31:09.469Z" },
- { url = "https://files.pythonhosted.org/packages/95/51/67e7cf11a53e40694f720457d5b3a1cdaaa3d5a9a633e482f225456b93ff/debugpy-1.8.21-py2.py3-none-any.whl", hash = "sha256:b1e37d333663c8851516a47364ef473da127f9caebe4417e6df6f5825a7e9a92", size = 5352888, upload-time = "2026-06-01T19:31:25.186Z" },
-]
-
-[[package]]
-name = "decorator"
-version = "5.3.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" },
-]
-
[[package]]
name = "deepmerge"
version = "3.0.1"
@@ -1465,15 +1370,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/91/600003aaad107e27553fbc9cbfc57e96fa37e0223d2ef9d7d3a0e8d8d070/deepmerge-3.0.1-py3-none-any.whl", hash = "sha256:35c96f6a68fcf90719a5b31d9f8042ecef6c00fb56836660d33455d0f5cfda65", size = 14909, upload-time = "2026-09-01T14:09:43.364Z" },
]
-[[package]]
-name = "defusedxml"
-version = "0.7.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
-]
-
[[package]]
name = "dishka"
version = "1.10.1"
@@ -1633,15 +1529,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" },
]
-[[package]]
-name = "executing"
-version = "2.2.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" },
-]
-
[[package]]
name = "extra-platforms"
version = "13.7.1"
@@ -1679,15 +1566,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" },
]
-[[package]]
-name = "fastjsonschema"
-version = "2.22.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/33/a4/9473c7c3b87009d9c1d74034e4a0f6a35ff0d42dd0f9866d0c3ec4e9217b/fastjsonschema-2.22.2.tar.gz", hash = "sha256:72064e12356a7d6ef02165be2946b9abadbdf238536e07eb587e3dbaa33099cf", size = 385171, upload-time = "2026-08-15T19:47:08.853Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/49/82/2755c7c982086f00d4dab85bc120ec35045a9fc2191893a6ce79afe94443/fastjsonschema-2.22.2-py3-none-any.whl", hash = "sha256:0fb3915616adac85ccfdd737d26be1089845d2019819505b42d39888458f74d4", size = 27413, upload-time = "2026-08-15T19:47:04.406Z" },
-]
-
[[package]]
name = "fastnanoid"
version = "0.4.3"
@@ -2645,124 +2523,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
-[[package]]
-name = "ipykernel"
-version = "7.3.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "appnope", marker = "sys_platform == 'darwin'" },
- { name = "comm" },
- { name = "debugpy" },
- { name = "ipython", version = "8.39.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "ipython", version = "9.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
- { name = "jupyter-client" },
- { name = "jupyter-core" },
- { name = "matplotlib-inline" },
- { name = "nest-asyncio2" },
- { name = "packaging" },
- { name = "psutil" },
- { name = "pyzmq" },
- { name = "tornado" },
- { name = "traitlets" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/3d/c4/e4a38f579de4225a561305666f7541cdabb30075def2aa1ac17bd73c1fb5/ipykernel-7.3.0.tar.gz", hash = "sha256:9acaaaf97d16355166e4085afe9d225bfbdf2b7ef520f9df3be8f2b248275e09", size = 184899, upload-time = "2026-06-10T08:41:25.481Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl", hash = "sha256:897eb64da762549ef610698fca5e9675195ec6ac8ec7f19d81ce1ca20c876057", size = 120583, upload-time = "2026-06-10T08:41:23.648Z" },
-]
-
-[[package]]
-name = "ipython"
-version = "8.39.0"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version < '3.11'",
-]
-dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
- { name = "decorator" },
- { name = "exceptiongroup" },
- { name = "jedi" },
- { name = "matplotlib-inline" },
- { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
- { name = "prompt-toolkit" },
- { name = "pygments" },
- { name = "stack-data" },
- { name = "traitlets" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/40/18/f8598d287006885e7136451fdea0755af4ebcbfe342836f24deefaed1164/ipython-8.39.0.tar.gz", hash = "sha256:4110ae96012c379b8b6db898a07e186c40a2a1ef5d57a7fa83166047d9da7624", size = 5513971, upload-time = "2026-03-27T10:02:13.94Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c0/56/4cc7fc9e9e3f38fd324f24f8afe0ad8bb5fa41283f37f1aaf9de0612c968/ipython-8.39.0-py3-none-any.whl", hash = "sha256:bb3c51c4fa8148ab1dea07a79584d1c854e234ea44aa1283bcb37bc75054651f", size = 831849, upload-time = "2026-03-27T10:02:07.846Z" },
-]
-
-[[package]]
-name = "ipython"
-version = "9.17.1"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.15' and sys_platform == 'win32'",
- "python_full_version >= '3.15' and sys_platform == 'emscripten'",
- "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'",
- "python_full_version == '3.14.*' and sys_platform == 'win32'",
- "python_full_version == '3.14.*' and sys_platform == 'emscripten'",
- "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
- "python_full_version == '3.13.*' and sys_platform == 'win32'",
- "python_full_version == '3.13.*' and sys_platform == 'emscripten'",
- "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
- "python_full_version == '3.12.*' and sys_platform == 'win32'",
- "python_full_version == '3.12.*' and sys_platform == 'emscripten'",
- "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
- "python_full_version == '3.11.*' and sys_platform == 'win32'",
- "python_full_version == '3.11.*' and sys_platform == 'emscripten'",
- "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
-]
-dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
- { name = "ipython-pygments-lexers" },
- { name = "jedi" },
- { name = "matplotlib-inline" },
- { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
- { name = "prompt-toolkit" },
- { name = "psutil", marker = "sys_platform != 'cygwin' and sys_platform != 'emscripten'" },
- { name = "pygments" },
- { name = "stack-data" },
- { name = "traitlets" },
- { name = "typing-extensions", marker = "python_full_version < '3.12'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/b9/32/99451b1283ec5d92ad77073f12e1c667dc10775384d8f15c2914207149dd/ipython-9.17.1.tar.gz", hash = "sha256:8919be8c27f20a6f4423145028063f6637b42a03ce57665bb12015ee1f073529", size = 4539289, upload-time = "2026-09-01T08:29:32.6Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2d/1e/65b59cf518c106aa755e7f7da3099027738687a862ec785060702a481320/ipython-9.17.1-py3-none-any.whl", hash = "sha256:6d1645743cfd1a07eb695d85aa2b5fa66721f8cbae9431d4049f7084bbf06509", size = 639038, upload-time = "2026-09-01T08:29:30.673Z" },
-]
-
-[[package]]
-name = "ipython-pygments-lexers"
-version = "1.1.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pygments" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" },
-]
-
-[[package]]
-name = "ipywidgets"
-version = "8.1.9"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "comm" },
- { name = "ipython", version = "8.39.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "ipython", version = "9.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
- { name = "jupyterlab-widgets" },
- { name = "traitlets" },
- { name = "widgetsnbextension" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/c9/7c/6db60eddf38547353b06d57941f5eee22a990640ce30479fd71a810507f2/ipywidgets-8.1.9.tar.gz", hash = "sha256:bcccba38a6ec3253f7a39c943cea5b9ad01999ce071396171adbc51c6a6a8613", size = 117252, upload-time = "2026-08-18T08:54:24.123Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c3/55/298e9b3b864a198234997e87a1471c1b17d7f3546ace6d18fb5cf1ce24b2/ipywidgets-8.1.9-py3-none-any.whl", hash = "sha256:f2b8cbcaae10252b809fbe4d7470db75c09b769a32cbf816d20e5ca6d3c5a79d", size = 140101, upload-time = "2026-08-18T08:54:22.339Z" },
-]
-
[[package]]
name = "itsdangerous"
version = "2.2.0"
@@ -2772,18 +2532,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
]
-[[package]]
-name = "jedi"
-version = "0.20.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "parso" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/46/b7/a3635f6a2d7cf5b5dd98064fc1d5fbbafcb25477bcea204a3a92145d158b/jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011", size = 3119416, upload-time = "2026-05-01T23:38:47.814Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67", size = 4884812, upload-time = "2026-05-01T23:38:43.919Z" },
-]
-
[[package]]
name = "jinja2"
version = "3.1.6"
@@ -2845,74 +2593,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
]
-[[package]]
-name = "jupyter-client"
-version = "8.10.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "jupyter-core" },
- { name = "python-dateutil" },
- { name = "pyzmq" },
- { name = "tornado" },
- { name = "traitlets" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/c5/2a/906772148a06e48885039e0250c340b770a55bbf37b08d6ee0449df369c3/jupyter_client-8.10.0.tar.gz", hash = "sha256:9f7116294dca55f1785be880057d44544db9b1567718d92cb33c58886afb9497", size = 360653, upload-time = "2026-08-28T12:17:10.854Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0f/88/7c548de1f6c2ade7c931a3282da73f9274fa6a1531091be682f89c85efb9/jupyter_client-8.10.0-py3-none-any.whl", hash = "sha256:5f73f24f22fa25192cfff6b23c051932a2473a797b05734aff495b392103e14e", size = 110184, upload-time = "2026-08-28T12:17:09.028Z" },
-]
-
-[[package]]
-name = "jupyter-core"
-version = "5.9.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "platformdirs" },
- { name = "traitlets" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" },
-]
-
-[[package]]
-name = "jupyter-sphinx"
-version = "0.5.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "ipykernel" },
- { name = "ipython", version = "8.39.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "ipython", version = "9.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
- { name = "ipywidgets" },
- { name = "nbconvert" },
- { name = "nbformat" },
- { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
- { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/1a/b5/40f540cc9e54ee829f79daac43456a8d5ab4b70c8d26f0b9eca0dfcb4ad5/jupyter_sphinx-0.5.3.tar.gz", hash = "sha256:2e23699a3a1cf5db31b10981da5aa32606ee730f6b73a844d1e76d800756af56", size = 17532, upload-time = "2023-12-28T12:19:41.047Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/6f/1c/45251d4b9624e42b9e4f369dae2a64f5ea19b9387ba492ceb7be65343dda/jupyter_sphinx-0.5.3-py3-none-any.whl", hash = "sha256:a67b3208d4da5b3508dbb8260d3b359ae476c36c6c642747b78a2520e5be0b05", size = 21918, upload-time = "2023-12-28T12:19:39.38Z" },
-]
-
-[[package]]
-name = "jupyterlab-pygments"
-version = "0.3.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" },
-]
-
-[[package]]
-name = "jupyterlab-widgets"
-version = "3.0.17"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/21/8b/e739cf9066ad5037a2d4b0a403f06da374fdccb9748221661c8b492d3dbc/jupyterlab_widgets-3.0.17.tar.gz", hash = "sha256:6e61fe21ca8a66039180a5cc52a433e07279d2fee79c8be963e00d55193f17a8", size = 213919, upload-time = "2026-08-18T08:52:17.511Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/33/ef/6d27fc118f58cb24886da413545a7efb0853d405fddbfd8b2d9ac09fbed4/jupyterlab_widgets-3.0.17-py3-none-any.whl", hash = "sha256:40ac1e9955acf116c4d995d9bfa082d86ad9ec6d91c4f134827cf5e0a5eb75e0", size = 217292, upload-time = "2026-08-18T08:52:15.47Z" },
-]
-
[[package]]
name = "librt"
version = "0.15.0"
@@ -3206,18 +2886,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
]
-[[package]]
-name = "matplotlib-inline"
-version = "0.2.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "traitlets" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/bd/c0/9f7c9a46090390368a4d7bcb76bb87a4a36c421e4c0792cdb53486ffac7a/matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79", size = 8150, upload-time = "2026-05-08T17:33:33.49Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" },
-]
-
[[package]]
name = "mdit-py-plugins"
version = "0.6.1"
@@ -3240,18 +2908,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
-[[package]]
-name = "mistune"
-version = "3.3.4"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "typing-extensions", marker = "python_full_version < '3.11'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/7b/92/328a294a6de83bacb95bed01f04e0eaff4e3616ee359fc821a5dfc539b02/mistune-3.3.4.tar.gz", hash = "sha256:58b5c96d6fcb61190dfe5fae498d2b2065f99cf61e9649418fd54cf1ada86dfe", size = 121426, upload-time = "2026-07-22T05:22:30.89Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/77/e4/288365afae98953bc01de09f686f40d8ee84578135aa7767d5d4e60b5278/mistune-3.3.4-py3-none-any.whl", hash = "sha256:ee015381e955e370962968befe1d729ab60fafb6a715ac6751763fbce38c8d4a", size = 66862, upload-time = "2026-07-22T05:22:29.419Z" },
-]
-
[[package]]
name = "mmh3"
version = "5.3.0"
@@ -3856,90 +3512,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/09/dc/f3dfb7488b770f3f67e6545085bf2abea5172e88f57b8ad25ef860ca704c/myst_parser-5.1.0-py3-none-any.whl", hash = "sha256:9c91c52b3cdb4d94a6506e4fab4e2f296c7623a0da0dcbe6de1565c3dad67a8a", size = 85817, upload-time = "2026-05-13T09:38:17.904Z" },
]
-[[package]]
-name = "nbclient"
-version = "0.11.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "jupyter-client" },
- { name = "jupyter-core" },
- { name = "nbformat" },
- { name = "traitlets" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/28/a5/b3bae4b590c0cbcada2c63a34f7580024e834a8ba213e949a2f906705787/nbclient-0.11.0.tar.gz", hash = "sha256:04a134a5b087f2c5887f228aca155db50169b8cd9334dee6942c8e927e56081a", size = 62535, upload-time = "2026-06-05T07:52:41.746Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl", hash = "sha256:ef7fa0d59d6e1d41103933d8a445a18d5de860ca6b613b87b8574accdb3c2895", size = 25288, upload-time = "2026-06-05T07:52:40.115Z" },
-]
-
-[[package]]
-name = "nbconvert"
-version = "7.17.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "beautifulsoup4" },
- { name = "bleach", extra = ["css"] },
- { name = "defusedxml" },
- { name = "jinja2" },
- { name = "jupyter-core" },
- { name = "jupyterlab-pygments" },
- { name = "markupsafe" },
- { name = "mistune" },
- { name = "nbclient" },
- { name = "nbformat" },
- { name = "packaging" },
- { name = "pandocfilters" },
- { name = "pygments" },
- { name = "traitlets" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/01/b1/708e53fe2e429c103c6e6e159106bcf0357ac41aa4c28772bd8402339051/nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2", size = 865311, upload-time = "2026-04-08T00:44:14.914Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl", hash = "sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8", size = 261927, upload-time = "2026-04-08T00:44:12.845Z" },
-]
-
-[[package]]
-name = "nbformat"
-version = "5.11.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "fastjsonschema" },
- { name = "jsonschema" },
- { name = "jupyter-core" },
- { name = "traitlets" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/31/72/b3446efab8756e7df4b8ec587f8e611cb5a7249e4323db480802f1d3be04/nbformat-5.11.1.tar.gz", hash = "sha256:32d4521c68c6e7d5b29c76defaeed9f42ea733142b9b19f88277ce10390b9c4d", size = 147775, upload-time = "2026-08-17T08:10:51.942Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/1a/69/ee613f74085ca7103f79cd08d579c4f3177d1d26e5d3d9528d2d6536a707/nbformat-5.11.1-py3-none-any.whl", hash = "sha256:cc6698fa75f4fab8755ead786317815f13a6fee3b53311c0abb1a8b51d52f7ec", size = 79849, upload-time = "2026-08-17T08:10:50.18Z" },
-]
-
-[[package]]
-name = "nbsphinx"
-version = "0.9.8"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
- { name = "jinja2" },
- { name = "nbconvert" },
- { name = "nbformat" },
- { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
- { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
- { name = "traitlets" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/e7/d1/82081750f8a78ad0399c6ed831d42623b891904e8e7b8a75878225cf1dce/nbsphinx-0.9.8.tar.gz", hash = "sha256:d0765908399a8ee2b57be7ae881cf2ea58d66db3af7bbf33e6eb48f83bea5495", size = 417469, upload-time = "2025-11-28T17:41:02.336Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/03/78/843bcf0cf31f88d2f8a9a063d2d80817b1901657d83d65b89b3aa835732e/nbsphinx-0.9.8-py3-none-any.whl", hash = "sha256:92d95ee91784e56bc633b60b767a6b6f23a0445f891e24641ce3c3f004759ccf", size = 31961, upload-time = "2025-11-28T17:41:00.796Z" },
-]
-
-[[package]]
-name = "nest-asyncio2"
-version = "1.7.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b4/73/731debf26e27e0a0323d7bda270dc2f634b398e38f040a09da1f4351d0aa/nest_asyncio2-1.7.2.tar.gz", hash = "sha256:1921d70b92cc4612c374928d081552efb59b83d91b2b789d935c665fa01729a8", size = 14743, upload-time = "2026-02-13T00:34:04.386Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c5/3c/3179b85b0e1c3659f0369940200cd6d0fa900e6cefcc7ea0bc6dd0e29ffb/nest_asyncio2-1.7.2-py3-none-any.whl", hash = "sha256:f5dfa702f3f81f6a03857e9a19e2ba578c0946a4ad417b4c50a24d7ba641fe01", size = 7843, upload-time = "2026-02-13T00:34:02.691Z" },
-]
-
[[package]]
name = "nodeenv"
version = "1.10.0"
@@ -4613,24 +4185,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/c6/df1fe324248424f77b89371116dab5243db7f052c32cc9fe7442ad9c5f75/pandas_stubs-2.3.3.260113-py3-none-any.whl", hash = "sha256:ec070b5c576e1badf12544ae50385872f0631fc35d99d00dc598c2954ec564d3", size = 168246, upload-time = "2026-01-13T22:30:15.244Z" },
]
-[[package]]
-name = "pandocfilters"
-version = "1.5.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" },
-]
-
-[[package]]
-name = "parso"
-version = "0.8.7"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/30/4b/90c937815137d43ce71ba043cd3566221e9df6b9c805f24b5d138c9d40a7/parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1", size = 401824, upload-time = "2026-05-01T23:13:02.138Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" },
-]
-
[[package]]
name = "pathspec"
version = "1.1.1"
@@ -4640,18 +4194,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" },
]
-[[package]]
-name = "pexpect"
-version = "4.9.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "ptyprocess" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" },
-]
-
[[package]]
name = "pgvector"
version = "0.5.0"
@@ -4661,15 +4203,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/e4/a5573f2c579ca9ad133293bfb624148ba0893674ca4a6eeec85ced9a6a09/pgvector-0.5.0-py3-none-any.whl", hash = "sha256:fedc9800894e6da2be51358d7b7c574bf34f247ca741a5a09513622135f5964f", size = 30958, upload-time = "2026-07-06T18:27:26.797Z" },
]
-[[package]]
-name = "platformdirs"
-version = "4.11.7"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/69/b7/802a56eca9f2fac455b8bab5375a2647b0f0e14a2cd63ef077de3c4a7658/platformdirs-4.11.7.tar.gz", hash = "sha256:4f41487eeeeeb07f3a6625e61d9bc0ae6809f92d3386dbd74392fbb76108104d", size = 35127, upload-time = "2026-09-01T13:35:10.502Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/27/6e/80993e10a0482f630cef528635789233224f36b1ffd11592aa15d13ff9ce/platformdirs-4.11.7-py3-none-any.whl", hash = "sha256:8a02cb259042c79d1cd0450facc2fe6dc9d303ae7901afbe33bf8ea0b188cef6", size = 23938, upload-time = "2026-09-01T13:35:09.02Z" },
-]
-
[[package]]
name = "pluggy"
version = "1.6.0"
@@ -5123,24 +4656,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/37/ed/89c2c620af0e1660354cd8aabf9f5b21f911597ce22acb37c805d6c86bc8/psycopg_pool-3.3.1-py3-none-any.whl", hash = "sha256:2af5b432941c4c9ad5c87b3fa410aec910ec8f7c122855897983a06c45f2e4b5", size = 40023, upload-time = "2026-05-01T23:31:53.136Z" },
]
-[[package]]
-name = "ptyprocess"
-version = "0.7.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" },
-]
-
-[[package]]
-name = "pure-eval"
-version = "0.2.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" },
-]
-
[[package]]
name = "pyarrow"
version = "25.0.1"
@@ -5737,86 +5252,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
-[[package]]
-name = "pyzmq"
-version = "27.2.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "cffi", marker = "implementation_name == 'pypy'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/e7/8d/5b3d5631c2f4b4b8862f64cd0c9eb777b5710eeb5125b4be8dd0a200a4c0/pyzmq-27.2.0.tar.gz", hash = "sha256:54d4259d1bfae24ecdb5ca79f7acc2eac6c286a02d6a0ae617797cb45f0726d3", size = 292316, upload-time = "2026-08-20T19:08:21.19Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/38/de/c9d653d686ec686bac6ae5f953c784ffc4d2e4a33f9c20d0326aff549ca8/pyzmq-27.2.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:480dba27b145373b5e103890f17969d891bc9e86746d6b8b29dd70b0d4addc62", size = 1458528, upload-time = "2026-08-20T19:06:11.455Z" },
- { url = "https://files.pythonhosted.org/packages/13/3a/e3ae8e56fdb87cefbef655dc1d808d5ac76eacf40af850c5d78fd81a7faf/pyzmq-27.2.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:722f0a6940be1a483c81029a271d950e04dc2ff113a42e21b3d2b7a0d8e59638", size = 985810, upload-time = "2026-08-20T19:06:13.409Z" },
- { url = "https://files.pythonhosted.org/packages/03/ee/0ace0abf6315f3f481388826f1e9218802d9b5bb4a01bae5ce42018c3b4e/pyzmq-27.2.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ee556ed1cf836f96de9d5e545563116426d4a94f21b8041fdc79408eff18ebb", size = 713840, upload-time = "2026-08-20T19:06:14.85Z" },
- { url = "https://files.pythonhosted.org/packages/3b/fd/aee8c87f4854a012e232a6557b1167b6cd207506f59f378954b2d8bb6ed1/pyzmq-27.2.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d64da42cae09e6b0c61368b4cc8ca80f23ce3af17584d08053f3dc957433d5ed", size = 888214, upload-time = "2026-08-20T19:06:16.295Z" },
- { url = "https://files.pythonhosted.org/packages/94/2f/9be34eab874a26aa91315be0b27f94d43d35dad544cbdf28bcd41691afb2/pyzmq-27.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:376981d106598beb70be384f44d8f589832fd0051d184d38d10043da3cc3b080", size = 1705252, upload-time = "2026-08-20T19:06:17.89Z" },
- { url = "https://files.pythonhosted.org/packages/7f/26/362344e337d6b5905d65d0c205e91090b39b3b5a59ec9b84dbe88a5dd60c/pyzmq-27.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:40d96cb7a8f6a43aa9617c00215c2b73e1b5e4a1d6cbc9f5860ed7ac682599f0", size = 2075298, upload-time = "2026-08-20T19:06:19.27Z" },
- { url = "https://files.pythonhosted.org/packages/1d/ee/3b76b91e2bb8c5c12f8ae9ee553eb6254ba0bb048c2f0dea7d945420a0d3/pyzmq-27.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4ebc7889b31bc11c72e9f17ba3ebb0a8b0911cce413f41b498e55383a94819a3", size = 1926812, upload-time = "2026-08-20T19:06:20.621Z" },
- { url = "https://files.pythonhosted.org/packages/9e/ae/83d0740f125a6ef91d5ed29e560679e4c5beeaa3ab1fc87e5fe1128c296d/pyzmq-27.2.0-cp310-cp310-win32.whl", hash = "sha256:650c6cd7cb39a069e7048261efe66fce8bf2e0052c831a7a099b7a0f2ea860d7", size = 571057, upload-time = "2026-08-20T19:06:22.078Z" },
- { url = "https://files.pythonhosted.org/packages/ce/b3/3e99a7af1c25e84f68c6c7b16c51b664dd51e12d0bc3d252a48d298e9a08/pyzmq-27.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:82a09aa67871d4f2fcafd47bf670fb93210b232a7c2d4b8a54676314edf04033", size = 643232, upload-time = "2026-08-20T19:06:23.431Z" },
- { url = "https://files.pythonhosted.org/packages/06/cb/785d002a08e630807141b102fd2740cc11623c366bb4606465450bd12e03/pyzmq-27.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:bad4813f270592cedf56977e31ac1fc374fb0f6f67ea5134a5e37c19cb429a8e", size = 567590, upload-time = "2026-08-20T19:06:24.664Z" },
- { url = "https://files.pythonhosted.org/packages/1d/2e/8897afa4538707d86645f51cc50e66b2b84900edb1be9dc9af2c2fc04e5d/pyzmq-27.2.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:9216132843d139a123f243c07fe70f7487dce5041093dd77040f9adb5dc91872", size = 1457109, upload-time = "2026-08-20T19:06:26.022Z" },
- { url = "https://files.pythonhosted.org/packages/d1/bc/dbce7bc1654fa25b1e68b9bad9e547906f581ce919c186a88ed951cb794c/pyzmq-27.2.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:d41ebb260b69329b7d4a2936d44c872c86dd785355b51366c8b14e07ed7e9373", size = 985701, upload-time = "2026-08-20T19:06:27.481Z" },
- { url = "https://files.pythonhosted.org/packages/95/cf/6981738b57c83fef33f356141ad83bf51e92f2f70c9d5767affd1a699f07/pyzmq-27.2.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:468139ddb2e494d06e586bd3a6835077e8b3764560c8db552fe685c5867fc24e", size = 714285, upload-time = "2026-08-20T19:06:28.962Z" },
- { url = "https://files.pythonhosted.org/packages/50/b5/13657961a845e29c28a4e7ac4202999ec90b3bba1890a5469ce2ae90359d/pyzmq-27.2.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39755dc4a923021bd0677990ffdbc21cff0e1ee1cf07fe3817acea153ef4cb67", size = 888465, upload-time = "2026-08-20T19:06:30.4Z" },
- { url = "https://files.pythonhosted.org/packages/58/5a/ca7ee7a767413d4ba858e93748b95e30b35b8c139849fba94de4433ea2e5/pyzmq-27.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:714f8cbd66c7e405338d668f79d2fe83fe923defe348e843be998603cf92eeff", size = 1705731, upload-time = "2026-08-20T19:06:31.819Z" },
- { url = "https://files.pythonhosted.org/packages/0b/8b/083f6184e4eba566c9a3cc9974b1b0fe327b7093788135ba8133edaa67a6/pyzmq-27.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1132805970045adb9f5f05dd57040978286a8e21a5475f2c2ddf1bc983b9a2c7", size = 2074243, upload-time = "2026-08-20T19:06:33.36Z" },
- { url = "https://files.pythonhosted.org/packages/57/f5/249362b664ae725d534c8843214fa9fd7fccd74532a19e24603954a88a7d/pyzmq-27.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b26f2d0493b79ce3c3112c8a12649418915582ba4707b8ed9f44febf2be71f42", size = 1926859, upload-time = "2026-08-20T19:06:34.796Z" },
- { url = "https://files.pythonhosted.org/packages/bf/cc/23c613c15f06d879f13364d14c17e5e4e8304049411e96c1410e6e56c3ea/pyzmq-27.2.0-cp311-cp311-win32.whl", hash = "sha256:44f261eca7dfb9904ea2b56428f59ab693bbe2715c0413a701f17b067ebf877c", size = 570747, upload-time = "2026-08-20T19:06:36.337Z" },
- { url = "https://files.pythonhosted.org/packages/dc/bc/bbbcf89003c93f18e33665c26e3c48d75e3915c3dd22887f3a7aea2c5e26/pyzmq-27.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:8b86e04f55af0f4d8cd8ecf14c0b8b81ebc8fd66fa20126b753514628ecadc7e", size = 644845, upload-time = "2026-08-20T19:06:37.711Z" },
- { url = "https://files.pythonhosted.org/packages/14/c5/4635d0ba2b8493edf6d5541fff0b07fa1d986fdfc29c596a53a21e20f9af/pyzmq-27.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:917d601e9540098f580d2723d0ce6402cdb6f02bc8dc2de74e0dca6e13bffd1b", size = 567495, upload-time = "2026-08-20T19:06:39.246Z" },
- { url = "https://files.pythonhosted.org/packages/57/8a/153532fa53db30e116118164f3af269a1f3966b3e2ba32c89b12fe864bd8/pyzmq-27.2.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:591c8de5851c5ea372194469fe97587b97c3b641e9a70f31bb3474acbfde0241", size = 1431074, upload-time = "2026-08-20T19:06:40.601Z" },
- { url = "https://files.pythonhosted.org/packages/c8/ef/c08b91248bb90a9efa81fa00ba81b69c157c74d0c5efbb2c319d91babb62/pyzmq-27.2.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:00e73942ef12cecbc7951c4a9104bb8ffaed742abb13af2da6833d90dd368cef", size = 973915, upload-time = "2026-08-20T19:06:42.037Z" },
- { url = "https://files.pythonhosted.org/packages/b4/78/a3a3a86c2b00fadb92ece1ca4f8f028d62b2ce9ac3526097239ab2d6fba9/pyzmq-27.2.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f8079d0521fe94bbb401fe9407578b28f3701627c8be2c9f7e0c5b77dcb0109", size = 697722, upload-time = "2026-08-20T19:06:43.325Z" },
- { url = "https://files.pythonhosted.org/packages/62/2c/d5828306f795e8d34676d266823b74e2101e0ad3760d12083de3e02abbb2/pyzmq-27.2.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dea74fd65f1fc5f7fe167916a473ebe6ed6174e5e5d9de11ea6583661be6cf43", size = 872258, upload-time = "2026-08-20T19:06:44.627Z" },
- { url = "https://files.pythonhosted.org/packages/09/52/51253b78fd8739293e283407eeecb14215c02c71b6519af21f6eed8e69cd/pyzmq-27.2.0-cp312-abi3-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcc99ca132b667a4ed750afd42db4ea73288f18425a9b2e3c0af095665c491f5", size = 739591, upload-time = "2026-08-20T19:06:46.214Z" },
- { url = "https://files.pythonhosted.org/packages/e6/3e/142c85b67a4c9678629b0cf6d5125b29663d75be69bfaa57a3cac344d780/pyzmq-27.2.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b8d5f66e4a8246cf77f7b8f7902af64f00553368fa0373c89d99b78f0ad79394", size = 1689031, upload-time = "2026-08-20T19:06:47.612Z" },
- { url = "https://files.pythonhosted.org/packages/0e/ee/0776fb0f98ed1eb74d77240087fef0ab045b6ad15cb09555c6c5134c98ad/pyzmq-27.2.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:d1526b42a2e725b84ed226f37becedc250c6347594e5ed304e4e9aff68c9aec3", size = 2059547, upload-time = "2026-08-20T19:06:49.064Z" },
- { url = "https://files.pythonhosted.org/packages/aa/0e/ec77f691a4aebe29ab6329f996fb0e0270c876a3016086e3ca6ef733bcae/pyzmq-27.2.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f707bcf2c1d007d14d70531d4dd7b41060881c73efa845580bf6faaf9ea24d42", size = 1910457, upload-time = "2026-08-20T19:06:50.783Z" },
- { url = "https://files.pythonhosted.org/packages/30/97/1f5530ff4fc271b4597048371d5af972c2baab51be132ba15874e0327a6a/pyzmq-27.2.0-cp312-abi3-win32.whl", hash = "sha256:fdaaa4ea3242f6ad298eb5177eb042aea5c73c30e76d20caee7b15af20d24ec2", size = 563450, upload-time = "2026-08-20T19:06:52.307Z" },
- { url = "https://files.pythonhosted.org/packages/02/8b/b83f7780dad22e0878e4c7bd9158ebd24ed12bc3d5e3a471cd0576f77ded/pyzmq-27.2.0-cp312-abi3-win_amd64.whl", hash = "sha256:2c218c6ab8bc447ba62054b581fd30209689d199c6ecb253f79615ca74a38e12", size = 628633, upload-time = "2026-08-20T19:06:53.809Z" },
- { url = "https://files.pythonhosted.org/packages/52/aa/3918b5ac7f9987bd9c421b065074fd7409ded88f856f2c704a24341877ec/pyzmq-27.2.0-cp312-abi3-win_arm64.whl", hash = "sha256:348d6fd3e4b81ae4580622ea8c2ea60224e84b2ac1b3be4482e6edc7de06e7a3", size = 556006, upload-time = "2026-08-20T19:06:55.242Z" },
- { url = "https://files.pythonhosted.org/packages/83/5e/d0541596b48c5a19f85dcbea83d6673d8e91681cdf853eb194c31fc9766e/pyzmq-27.2.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:c551b9e2f86dc625fcb1a032c0d68042678caf96a8dd7c28796766b673bd5b52", size = 1127193, upload-time = "2026-08-20T19:06:56.545Z" },
- { url = "https://files.pythonhosted.org/packages/50/9f/8c7411bb283982d46e6d56dca6a095678c87eb0398daead12776d9881ac2/pyzmq-27.2.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:288cc790da0e3064a14a38ddc56ba169dada8c8af4cb86518db2bcbd380eedbb", size = 1166833, upload-time = "2026-08-20T19:06:58.011Z" },
- { url = "https://files.pythonhosted.org/packages/f9/84/a849161ff88b2de9b991cc8ab332218824741122fdc4fdf222a5b822ac8c/pyzmq-27.2.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:3d45189c0c3c99f817b7fefff0d32eeef684cf33e1e3c0fc4281515357c54702", size = 1134452, upload-time = "2026-08-20T19:06:59.898Z" },
- { url = "https://files.pythonhosted.org/packages/3c/34/ff4aaff0cfba2a4d7ad1a16ffedc52c6deb89fcf673d455085446b23f215/pyzmq-27.2.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:d61910b52be5b2cd8b248dbcbe3a1b0275556a7d99fb613fc43323b546e273b8", size = 1167520, upload-time = "2026-08-20T19:07:01.283Z" },
- { url = "https://files.pythonhosted.org/packages/b6/07/42111e9dc1041d78b4443d6eb1b82b027f1a58178dc8a38385effbc72ad5/pyzmq-27.2.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:3ab6eb88590e510ab16715c32dbba12000da9bee989fdadd9ee19a234c492eb7", size = 1466289, upload-time = "2026-08-20T19:07:02.738Z" },
- { url = "https://files.pythonhosted.org/packages/4b/b4/def7a478458da78665840564161772e7e938600c32a89f28e8b221b54d2d/pyzmq-27.2.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1ecbdd131b9669f62d3a45afee5527c7ae9f141e4301267f21714c90bd21725f", size = 975868, upload-time = "2026-08-20T19:07:04.155Z" },
- { url = "https://files.pythonhosted.org/packages/38/d5/e3e85f7fea37153097aaff49db9e33093909cc2a7b22c1ac4ebe546600fc/pyzmq-27.2.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3146385b94a760236c5eceff468a66a296a716ca98a2e0f9217b1518118466b1", size = 706054, upload-time = "2026-08-20T19:07:05.623Z" },
- { url = "https://files.pythonhosted.org/packages/1c/ef/3b7d9449b223183222bf517245e1e53d5f1ab8c10be8b45f6a301b2f994a/pyzmq-27.2.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9846e881620dd62566ca76a53e384c3f37490faf4b9240aebc7498810dfca853", size = 878984, upload-time = "2026-08-20T19:07:07.153Z" },
- { url = "https://files.pythonhosted.org/packages/be/a5/8b49dbd494f6dcfda69dc4cade322a4b02706ef4e3d30cc366d4e369899f/pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d9527e3dbaef1edaeeb2446fa7379446814a43ade8adc7c4a5ebe69437815ddd", size = 1697489, upload-time = "2026-08-20T19:07:08.945Z" },
- { url = "https://files.pythonhosted.org/packages/da/5a/4bb8280901130c26ea25f0cbb4a6d39d94250860c6b3dbd912f1cf48fca7/pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:56b48fa9d478a3af7254f397697a62f5ad3e1bb677e200b2701f0c290d97e5af", size = 2064236, upload-time = "2026-08-20T19:07:10.384Z" },
- { url = "https://files.pythonhosted.org/packages/de/38/f433af66922554adb2b5f79e897018c8e19a90b9eaeb49c4814f8355ebe4/pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bf0b6e4ce1bb089751c504c5493d6b0557eabd02dd21b76e9086cf964234b103", size = 1917424, upload-time = "2026-08-20T19:07:11.909Z" },
- { url = "https://files.pythonhosted.org/packages/36/81/ea1c1ae3f801d96ba2c269e056761ebcfe023476e651d3af2a7817962051/pyzmq-27.2.0-cp314-cp314t-win32.whl", hash = "sha256:fba8afcf265c6e9fbe1594cb045d4765c6c9a7d607653a8196067ef23566b843", size = 591103, upload-time = "2026-08-20T19:07:13.451Z" },
- { url = "https://files.pythonhosted.org/packages/8a/04/149a627707e780fa9f2c1ede3590c14fa6b18b5576d15744342622299a50/pyzmq-27.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d1bc1d380a91d954ed5fc9f12915dba014eed0978d2de05ee7ca688bdaac144a", size = 670215, upload-time = "2026-08-20T19:07:15.069Z" },
- { url = "https://files.pythonhosted.org/packages/30/ba/f9c3c1536c41ef3dbf765ea04218990e2056e558f98184ecd883767fc501/pyzmq-27.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:c7cfb75caa83f5153c687e9d2107f64b5ef0ef0d6edd260d3ff920baaaa69101", size = 582252, upload-time = "2026-08-20T19:07:16.582Z" },
- { url = "https://files.pythonhosted.org/packages/fa/00/78fe097a304a408275747ce43f20428789130b059c5649956277c20f30cf/pyzmq-27.2.0-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:c5129a8fe43ecc49b99eb75616603d483a3c2fcaef504988fafe8ea392aea98b", size = 1134295, upload-time = "2026-08-20T19:07:17.94Z" },
- { url = "https://files.pythonhosted.org/packages/f2/83/1c36270658d2ee56e23a3f9ef5fbcb94cbd2f9fe966a6641f2f38e697162/pyzmq-27.2.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:baa2ce3485145653194d6c8c5beedd1e9f0bf46a0919c9fa2fe2204fc35b74d9", size = 1167492, upload-time = "2026-08-20T19:07:19.476Z" },
- { url = "https://files.pythonhosted.org/packages/58/b2/f0ae223438d7faa991f6feefdc823815f11cc604f898738376b59fd96515/pyzmq-27.2.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:e1ed46048d1920cabc96d952a0d5cfe4127ad8db572c335aae4e3c57b9278d7f", size = 1465992, upload-time = "2026-08-20T19:07:20.941Z" },
- { url = "https://files.pythonhosted.org/packages/59/46/fb56f3f37a6a0937b0e1d2885e808b5eedc171320bac85573cfae78fa9bc/pyzmq-27.2.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e0fa0bc6b1a184aee59b32efcd1b7f0e6d5b8f9387799e4c16a4cb66a86747d6", size = 976118, upload-time = "2026-08-20T19:07:22.577Z" },
- { url = "https://files.pythonhosted.org/packages/21/82/a2c9bfd7c4d34eea1278493cd041bc000d41acb4463c89ceaad29dc813b6/pyzmq-27.2.0-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4bd6743e8bf854c3bfce892dd6578a514aabf128e37a4b2eafcf01856f7e44", size = 705968, upload-time = "2026-08-20T19:07:24.019Z" },
- { url = "https://files.pythonhosted.org/packages/d6/12/b906b269116b6591dc15c0acc5d04c043957c8a531d336999731f4b1d899/pyzmq-27.2.0-cp315-cp315t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95369ed6626afcfe2ac89832fb1b917c077fbeb905fbbe5d918349ce0222b89b", size = 879011, upload-time = "2026-08-20T19:07:25.428Z" },
- { url = "https://files.pythonhosted.org/packages/12/13/f96359534bfb77651c15f1fbfc4bfdd7ec3489d23f434706d39598dd0dcd/pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:40124779c3a56ad5d91902df1ff89159cb414b6c1a0ee697abcc66cf5e6db62d", size = 1697496, upload-time = "2026-08-20T19:07:26.821Z" },
- { url = "https://files.pythonhosted.org/packages/21/b4/2c007ae5f2fe5eca86cbfbc874ed86b5135f2f7812615dfd78606d3c93f6/pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:ec8a318dfc27c7d946651b3d9e8025d5734f30c168a822195601827207bac09b", size = 2064347, upload-time = "2026-08-20T19:07:28.315Z" },
- { url = "https://files.pythonhosted.org/packages/9b/88/767af3a6630c15215f3a66700ec79598a375edd1fdc9d75a3ad522178c01/pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:88c0fac061bac269076edeb3a209acefc96cd6167c239daf1c2b404ac48d7012", size = 1917360, upload-time = "2026-08-20T19:07:29.693Z" },
- { url = "https://files.pythonhosted.org/packages/35/c1/80dd2d20d6e57bc68e1dce1e84bf3e76c9577c1bf728199985c8b4ea0fd1/pyzmq-27.2.0-cp315-cp315t-win32.whl", hash = "sha256:ac126d48cf18aa955daabef43bf0009ff76ad4deee437d09ecf15388214b5beb", size = 591073, upload-time = "2026-08-20T19:07:31.341Z" },
- { url = "https://files.pythonhosted.org/packages/f8/b5/33b781666f3f52ae834bc9c8e38f4f0483a826c5a91cccc993292007bf10/pyzmq-27.2.0-cp315-cp315t-win_amd64.whl", hash = "sha256:edce90a1e588ec63adbf612cc0ad582de4169cd216c7ae53c15f42a2ee902f35", size = 670701, upload-time = "2026-08-20T19:07:32.895Z" },
- { url = "https://files.pythonhosted.org/packages/6e/97/bc4f0edefb992df4fdebcf9f0cc40f631cd4ed277e1ed59ef2cd99a5c8c5/pyzmq-27.2.0-cp315-cp315t-win_arm64.whl", hash = "sha256:a843094b4d3d633bc3623e47a2ff50742d6af02bc1f7606aa2e67e971e21878d", size = 581985, upload-time = "2026-08-20T19:07:34.19Z" },
- { url = "https://files.pythonhosted.org/packages/8c/26/1a7cd2d8e4e3c27d83a46960e22101b69f527843be140cb3375267aa8ca6/pyzmq-27.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:770a37f28ddfbe1d2c40a2e3ce37e5fd10831daa6ae9634105aa8a5d23507b00", size = 875015, upload-time = "2026-08-20T19:07:52.084Z" },
- { url = "https://files.pythonhosted.org/packages/b8/ed/c8daf770ca31eb293bef40f801f05148e0a96ad538d55ec3bceb226c91ec/pyzmq-27.2.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b398c5fe102b41e1559f7ffdae760aabd5f432d73b047b4ae0eac4e01cb594d2", size = 774400, upload-time = "2026-08-20T19:07:53.596Z" },
- { url = "https://files.pythonhosted.org/packages/64/6f/b958b0785eb15ab7a78913e3df8de705dbf226ce1d0004998fa4872bdd59/pyzmq-27.2.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0e1af01858d6dc0c09cea57f9cb1ddf4601f04897b6bb1efc3a2038123c87d79", size = 877993, upload-time = "2026-08-20T19:07:55.192Z" },
- { url = "https://files.pythonhosted.org/packages/2d/5a/7a070d0e9911441061402013f0c64e5772ee781704c5dae093e6b051c6f6/pyzmq-27.2.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:211350c3ccd4746bc5a85e8fe961bad1f7f2f274f67cf1f785fad7f96f562eea", size = 614493, upload-time = "2026-08-20T19:07:56.77Z" },
- { url = "https://files.pythonhosted.org/packages/b3/28/833485224e1bd8960cbf3539db465ab2bf23b50de885dc4225a637480539/pyzmq-27.2.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dde5e291548ca0f397623b5e523db5c90172b32aa4fd3ba464a79ea31a580b43", size = 780573, upload-time = "2026-08-20T19:07:58.18Z" },
- { url = "https://files.pythonhosted.org/packages/90/8e/52239b9b5fe4f9cf77e272e6a8c548c01ba407d08d3aeeb3220816669ef1/pyzmq-27.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:94242bd4de6af7e74665e14a88630bccd615057f6acfaf08a3a432551d604645", size = 553963, upload-time = "2026-08-20T19:08:00.015Z" },
- { url = "https://files.pythonhosted.org/packages/93/22/7187a1f0bf2b8bf8dc6b91762438fb9b472f684f2dc4cb74a24bf8957943/pyzmq-27.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a7c1144dc61777938e932a2c9011b980b89fd8ff3733033b34c44c299187a6e1", size = 875015, upload-time = "2026-08-20T19:08:01.692Z" },
- { url = "https://files.pythonhosted.org/packages/92/71/09b71620ad52bad4eb68b1516978ecaf52ef623c3fa16e0732a03cf3274c/pyzmq-27.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:c218b816220d05acf6ab1bafca58926d95cbcc5fec5024724666030466308f0c", size = 774395, upload-time = "2026-08-20T19:08:03.108Z" },
- { url = "https://files.pythonhosted.org/packages/9c/cf/5c8eb9994a14ff5ee5b0cada339421748746c95aee0280c8b656741e8749/pyzmq-27.2.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ae6ebbc0bfe5a21ce21e32ba567bf73df2d93888109c65acbd42506cf9395759", size = 877994, upload-time = "2026-08-20T19:08:04.724Z" },
- { url = "https://files.pythonhosted.org/packages/97/64/e22094c5555e550b6450ecfcceb6a1205d893d9a18ae27764c8c45acfb16/pyzmq-27.2.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:679b5b1dde326a921ea2c9ec1f9ea3115bfe1b4735779bbc6eb0473a0ed93f71", size = 614492, upload-time = "2026-08-20T19:08:06.487Z" },
- { url = "https://files.pythonhosted.org/packages/d2/28/5b1042899caed18278c56d54a502f5254d463afe8aea1acbecc98e053391/pyzmq-27.2.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5c6d8744d10b5e1eadd90a7c58f8546acf6bf680ee463f7e6ada09ad6c9f802", size = 780574, upload-time = "2026-08-20T19:08:07.987Z" },
- { url = "https://files.pythonhosted.org/packages/77/a3/f134603a671c114c6b56eb912bba890f09e2d43b8a28d243be5a5507cf2f/pyzmq-27.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3ee8dd7031d5e23f632e0e7eee67183ca7d2536e0de35dc1e5d69f3471a791e8", size = 553961, upload-time = "2026-08-20T19:08:09.651Z" },
-]
-
[[package]]
name = "questionary"
version = "2.1.1"
@@ -6307,15 +5742,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl", hash = "sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752", size = 104164, upload-time = "2026-06-03T00:56:38.614Z" },
]
-[[package]]
-name = "soupsieve"
-version = "2.9.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/69/99/a6ca3beb3ccacb41fb3321d8a60e5566f9e6467601ef8eba6a17e1b89778/soupsieve-2.9.2.tar.gz", hash = "sha256:4a55d8cf158a9c2e587fa4922f1bbb91d68ac829e2d6f25403a85747c71daf74", size = 122445, upload-time = "2026-08-07T00:57:24.801Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/eb/dc/ad025c1ee131eba60c69f4dd5779b18fcf1e6b21a343e2162a84d5d133c7/soupsieve-2.9.2-py3-none-any.whl", hash = "sha256:8089a26fd974ca7a1f30276d3d8492ab266ab15af581642dfe8aa162e0c1c823", size = 37370, upload-time = "2026-08-07T00:57:23.524Z" },
-]
-
[[package]]
name = "sphinx"
version = "8.1.3"
@@ -6566,22 +5992,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e", size = 13343, upload-time = "2023-04-14T08:10:20.844Z" },
]
-[[package]]
-name = "sphinx-datatables"
-version = "1.0.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "packaging" },
- { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
- { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
- { name = "sphinxcontrib-jquery" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/86/ee/7246d8b48187794bdeb7389d3bb1247850d3ae0015812e293182193715e1/sphinx_datatables-1.0.0.tar.gz", hash = "sha256:0d0aeccbcc3f4342e4f770848b00a074efb80f08e179a3330da57499cc47cc9d", size = 9548, upload-time = "2026-02-03T04:28:29.554Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/41/21/09a9e92d68e62642694cbd1bd76ba5a906e748c18904e95b7153e2af421e/sphinx_datatables-1.0.0-py3-none-any.whl", hash = "sha256:215a6245893605fe48c3e5a54dc5e66f29b3547e621bd5dd32aa748aac1f8c11", size = 8425, upload-time = "2026-02-03T04:28:28.33Z" },
-]
-
[[package]]
name = "sphinx-design"
version = "0.6.1"
@@ -6654,41 +6064,6 @@ dependencies = [
]
sdist = { url = "https://files.pythonhosted.org/packages/ae/21/62d3a58ff7bd02bbb9245a63d1f0d2e0455522a11a78951d16088569fca8/sphinx-paramlinks-0.6.0.tar.gz", hash = "sha256:746a0816860aa3fff5d8d746efcbec4deead421f152687411db1d613d29f915e", size = 12363, upload-time = "2023-08-11T16:09:28.604Z" }
-[[package]]
-name = "sphinx-tabs"
-version = "3.5.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
- { name = "pygments" },
- { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
- { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/ce/30/ca5b0de830f369968d8e3483dd45a8908fd10169c05cd9837f0bd075982e/sphinx_tabs-3.5.0.tar.gz", hash = "sha256:91dba1187e4c35fd37380a56ac228bbd54c6c649b2351829f3bf033718277537", size = 17006, upload-time = "2026-03-03T23:00:30.404Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2e/45/6adc5efeb19fd5fed4027e520b5c668ce58236a2b271ade5533c4c116276/sphinx_tabs-3.5.0-py3-none-any.whl", hash = "sha256:154be49de4d5c8249ea08c5d9bf88ca8f9c31e00a178305a93cbc33e000339e5", size = 9871, upload-time = "2026-03-03T23:00:28.89Z" },
-]
-
-[[package]]
-name = "sphinx-togglebutton"
-version = "0.4.5"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
- { name = "setuptools" },
- { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
- { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
- { name = "wheel" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/cc/be/169a0b0a8ad9588e8697c85e1d489aaaca7416073c2fc0267c360af5aae9/sphinx_togglebutton-0.4.5.tar.gz", hash = "sha256:c870dfbd3bc6e119b50ff9a37a64f8991902269e856728931c7d89877e8d4b3d", size = 18101, upload-time = "2026-03-27T13:50:41.984Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d8/2e/3dd55564928c5d61f92827d4b91307dde7911a40fbe0000645d73202eea9/sphinx_togglebutton-0.4.5-py3-none-any.whl", hash = "sha256:74eac6d2426110c3e1e6f989a98e07d7823141a335df1ad8a9d637bdf6a7af62", size = 44907, upload-time = "2026-03-27T13:50:40.94Z" },
-]
-
[[package]]
name = "sphinxcontrib-applehelp"
version = "2.0.0"
@@ -7055,11 +6430,9 @@ dev = [
{ name = "duckdb-engine" },
{ name = "fsspec", extra = ["s3"] },
{ name = "hatch-mypyc" },
- { name = "jupyter-sphinx" },
{ name = "mypy" },
{ name = "myst-parser", version = "4.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "myst-parser", version = "5.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
- { name = "nbsphinx" },
{ name = "numpydoc" },
{ name = "pandas-stubs" },
{ name = "pgvector" },
@@ -7093,13 +6466,10 @@ dev = [
{ name = "sphinx-autodoc-typehints", version = "3.13.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
{ name = "sphinx-click" },
{ name = "sphinx-copybutton" },
- { name = "sphinx-datatables" },
{ name = "sphinx-design", version = "0.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "sphinx-design", version = "0.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "sphinx-iconify" },
{ name = "sphinx-paramlinks" },
- { name = "sphinx-tabs" },
- { name = "sphinx-togglebutton" },
{ name = "sphinxcontrib-jquery" },
{ name = "sphinxcontrib-mermaid" },
{ name = "sqlalchemy", extra = ["asyncio"] },
@@ -7116,10 +6486,8 @@ dev = [
doc = [
{ name = "auto-pytabs", extra = ["sphinx"] },
{ name = "click-extra", extra = ["sphinx"] },
- { name = "jupyter-sphinx" },
{ name = "myst-parser", version = "4.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "myst-parser", version = "5.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
- { name = "nbsphinx" },
{ name = "numpydoc" },
{ name = "shibuya" },
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
@@ -7132,13 +6500,10 @@ doc = [
{ name = "sphinx-autodoc-typehints", version = "3.13.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
{ name = "sphinx-click" },
{ name = "sphinx-copybutton" },
- { name = "sphinx-datatables" },
{ name = "sphinx-design", version = "0.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "sphinx-design", version = "0.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "sphinx-iconify" },
{ name = "sphinx-paramlinks" },
- { name = "sphinx-tabs" },
- { name = "sphinx-togglebutton" },
{ name = "sphinxcontrib-jquery" },
{ name = "sphinxcontrib-mermaid" },
]
@@ -7281,10 +6646,8 @@ dev = [
{ name = "duckdb-engine", specifier = ">=0.17.0" },
{ name = "fsspec", extras = ["s3"] },
{ name = "hatch-mypyc" },
- { name = "jupyter-sphinx" },
{ name = "mypy", specifier = ">=2.0.0" },
{ name = "myst-parser" },
- { name = "nbsphinx" },
{ name = "numpydoc" },
{ name = "pandas-stubs", specifier = "<3" },
{ name = "pgvector" },
@@ -7313,12 +6676,9 @@ dev = [
{ name = "sphinx-autodoc-typehints" },
{ name = "sphinx-click", specifier = ">=6.0.0" },
{ name = "sphinx-copybutton", specifier = ">=0.5.2" },
- { name = "sphinx-datatables" },
{ name = "sphinx-design", specifier = ">=0.5.0" },
{ name = "sphinx-iconify" },
{ name = "sphinx-paramlinks", specifier = ">=0.6.0" },
- { name = "sphinx-tabs" },
- { name = "sphinx-togglebutton", specifier = ">=0.3.2" },
{ name = "sphinxcontrib-jquery" },
{ name = "sphinxcontrib-mermaid", specifier = ">=0.9.2" },
{ name = "sqlalchemy", extras = ["asyncio"] },
@@ -7335,9 +6695,7 @@ dev = [
doc = [
{ name = "auto-pytabs", extras = ["sphinx"], specifier = ">=0.5.0" },
{ name = "click-extra", extras = ["sphinx"] },
- { name = "jupyter-sphinx" },
{ name = "myst-parser" },
- { name = "nbsphinx" },
{ name = "numpydoc" },
{ name = "shibuya" },
{ name = "sphinx" },
@@ -7345,12 +6703,9 @@ doc = [
{ name = "sphinx-autodoc-typehints" },
{ name = "sphinx-click", specifier = ">=6.0.0" },
{ name = "sphinx-copybutton", specifier = ">=0.5.2" },
- { name = "sphinx-datatables" },
{ name = "sphinx-design", specifier = ">=0.5.0" },
{ name = "sphinx-iconify" },
{ name = "sphinx-paramlinks", specifier = ">=0.6.0" },
- { name = "sphinx-tabs" },
- { name = "sphinx-togglebutton", specifier = ">=0.3.2" },
{ name = "sphinxcontrib-jquery" },
{ name = "sphinxcontrib-mermaid", specifier = ">=0.9.2" },
]
@@ -7401,20 +6756,6 @@ test = [
{ name = "sniffio" },
]
-[[package]]
-name = "stack-data"
-version = "0.6.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "asttokens" },
- { name = "executing" },
- { name = "pure-eval" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" },
-]
-
[[package]]
name = "starlette"
version = "1.6.0"
@@ -7460,18 +6801,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" },
]
-[[package]]
-name = "tinycss2"
-version = "1.5.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "webencodings" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" },
-]
-
[[package]]
name = "tomli"
version = "2.4.1"
@@ -7535,23 +6864,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/13/bc/8c13eb66537dce1d2bd3a57132902f38d0e7f5bb46fa9f4daed9fe9d76ee/tomlkit-0.15.1-py3-none-any.whl", hash = "sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304", size = 49449, upload-time = "2026-07-17T01:48:05.728Z" },
]
-[[package]]
-name = "tornado"
-version = "6.5.8"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/10/d3/343e5bb989d6515b1646cf3d40135d73f3d5e45339bded401b56cdac24dd/tornado-6.5.8.tar.gz", hash = "sha256:9452e1b208a8bd771e2cb1f2ff564985b9b214bdebbe622793e1799e0a6bd23f", size = 520493, upload-time = "2026-08-07T02:12:42.971Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f2/d5/007086fd8df5489338e204f65adce33fd4f21a4999dbb2b9cff2f897b5f4/tornado-6.5.8-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:cc6aa787d7cfab7c3d35189dc7a56fbd2399a569624c730c6b55b3d6531d0403", size = 449487, upload-time = "2026-08-07T02:12:28.682Z" },
- { url = "https://files.pythonhosted.org/packages/70/c8/5a24a99495903f594f6a199dd7beead1cbc0a13e2cb9102727bcaaf2a997/tornado-6.5.8-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9715b5eb79735b2bcd454ce216a9275b7c0470e64ea1bf5742f78b2f72b26eeb", size = 447649, upload-time = "2026-08-07T02:12:30.306Z" },
- { url = "https://files.pythonhosted.org/packages/6e/de/f2e733f386b85962d1b1dc82cd63d169b5b4580062b35397eac9244a41fe/tornado-6.5.8-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:547d63f450d570c14fe0e8db2cfb14c9bbd1c2503b4a6612586267955aa47b58", size = 450707, upload-time = "2026-08-07T02:12:31.95Z" },
- { url = "https://files.pythonhosted.org/packages/0b/94/20efeee9a01c141e9ac47c397f81679dfda24b32768fc4fff24e76d36c2c/tornado-6.5.8-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e2360a0ffbe145eca8af0b19cb7203d79b1a98dd4cccdd6b368f6f49c2e3808", size = 451677, upload-time = "2026-08-07T02:12:33.512Z" },
- { url = "https://files.pythonhosted.org/packages/42/ec/a96ccb8ccf0de2b7bc2c5fa1608a4803735018242e90c4882365a9fd418f/tornado-6.5.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5d242290bdf7ab3151bc1065fdd75c0dcc21cbc7b49f22a4c56329c2d6566d22", size = 451510, upload-time = "2026-08-07T02:12:35.346Z" },
- { url = "https://files.pythonhosted.org/packages/29/b5/93185859245ad3f00e62175f29607346788b696369347f0146e0421286bb/tornado-6.5.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7b94ff0e128fe0542f3bd331fb44d06260fc4ac16881545159f34ef08aad4195", size = 450917, upload-time = "2026-08-07T02:12:36.963Z" },
- { url = "https://files.pythonhosted.org/packages/97/cf/fe33cf062834487d34d1559746a4a12521033c22645b6d74d4bca702e018/tornado-6.5.8-cp39-abi3-win32.whl", hash = "sha256:67832909c4779c64942380cb5f044a5c6163d00831472d80e25e115de9917836", size = 451952, upload-time = "2026-08-07T02:12:38.512Z" },
- { url = "https://files.pythonhosted.org/packages/cb/e1/468ad54333e92ccb62627e62cb88e5fc14a2171daa67ed47b1b8542d5b86/tornado-6.5.8-cp39-abi3-win_amd64.whl", hash = "sha256:11881db6b7c168494be2c2d12e65931451bdf7ee718535418ae1d8855dd5a0ee", size = 452391, upload-time = "2026-08-07T02:12:39.971Z" },
- { url = "https://files.pythonhosted.org/packages/ad/3e/cd5e4f06e34cde33b8ef66cf36aa2b5ad46354cc1af7d2136bbe365fee1d/tornado-6.5.8-cp39-abi3-win_arm64.whl", hash = "sha256:68a7468c7e289f8514d7d664101753903217eff1bb6822c6b5994a0b5f5bcb26", size = 451411, upload-time = "2026-08-07T02:12:41.469Z" },
-]
-
[[package]]
name = "tracerite"
version = "2.6.5"
@@ -7564,15 +6876,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7b/73/510b204e9543645031656e42b3e12b70ebc692facf0293700348ee27a35b/tracerite-2.6.5-py3-none-any.whl", hash = "sha256:0de23ea33cede6a905448bf0bd5fa869c4d950d06a3595693eebe55e44070f15", size = 112440, upload-time = "2026-08-18T03:53:47.073Z" },
]
-[[package]]
-name = "traitlets"
-version = "5.16.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/2c/2e/a7fbfe268c8a3b32546930c0297c101d65a4a14c304ad5790a9f478f0e4e/traitlets-5.16.1.tar.gz", hash = "sha256:ed900c2b631aa3a112811139fa97b8d2c3bad5e989656bba4b7e52c7852c18c1", size = 166137, upload-time = "2026-08-03T08:32:36.848Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ad/66/0d785f0bc5e4315a96c989bb476d0fc07ea4f85132550c7b156ca2035d52/traitlets-5.16.1-py3-none-any.whl", hash = "sha256:f775618166caa0396c8e337099240f2bd3e5e917d203b2e6fbe21a58d3cb1f6b", size = 86211, upload-time = "2026-08-03T08:32:34.48Z" },
-]
-
[[package]]
name = "trove-classifiers"
version = "2026.6.1.19"
@@ -8171,15 +7474,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c4/0e/57f6bb3024a597b2e8ec4aee710ffe62ddc95af2e2bb1ee7a7abdc22c68c/wcwidth-0.8.3-py3-none-any.whl", hash = "sha256:d5b73dba6158a595ec9370350e7f2637bcac8d6c5e4fde34f30fcffb6103a5e4", size = 331669, upload-time = "2026-08-28T18:10:04.909Z" },
]
-[[package]]
-name = "webencodings"
-version = "0.6.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d5/a0/8fd707bcb776a7be556bad06a2ea5fb9bd519df78ef8e26f70ccf0f38bff/webencodings-0.6.1.tar.gz", hash = "sha256:565f9ad031c702dae404e27a099e3e09186a3ab1b9520f06d215502b651fd910", size = 15001, upload-time = "2026-08-15T14:22:57.549Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/77/c6/040cbc72480d789a5f40d63fb484d3106554c4dfa2d2b70ad5022057750f/webencodings-0.6.1-py3-none-any.whl", hash = "sha256:7fab6269c8bf237c657876b52058ccb182e861518d1c695c1a9aaa8c1c105d5b", size = 8745, upload-time = "2026-08-15T14:22:56.31Z" },
-]
-
[[package]]
name = "websockets"
version = "15.0.1"
@@ -8251,27 +7545,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" },
]
-[[package]]
-name = "wheel"
-version = "0.48.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "packaging" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/d0/20/50ed6bdf27dec98b568a8ae25dc599f35baa3d9709f9e83fd1edb56b9a90/wheel-0.48.0.tar.gz", hash = "sha256:94800765601e9171bf5d58d066e640662842bcedcbab982b2c90787a2c987322", size = 66471, upload-time = "2026-08-11T22:02:27.327Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2e/29/69cfbb602cd91690c55d38ba9fe53e6a7e76a6fa647bf38f19c138d25449/wheel-0.48.0-py3-none-any.whl", hash = "sha256:3217dcc807155e45db462d7ef2431f5ddda0d7273b700d05a67b271ceb1287ab", size = 33320, upload-time = "2026-08-11T22:02:26.1Z" },
-]
-
-[[package]]
-name = "widgetsnbextension"
-version = "4.0.16"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/bf/60/bc7a980fc78837d6ef8f5940cca4cadc433364503a4c4d42e2a7a0de3231/widgetsnbextension-4.0.16.tar.gz", hash = "sha256:adeea0ae78f0856ee4945f413299801b82a0a01416303301f39a704282a37b73", size = 1111094, upload-time = "2026-08-18T08:52:55.859Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/34/95/40e17e20046b7bc820d29d09ae84ec157ec8dd6e6f6cd722626292c31b2e/widgetsnbextension-4.0.16-py3-none-any.whl", hash = "sha256:a31a8774885b96fe825462f5d6496166f0c7cae111195b6465c801d230eb5a4e", size = 2225148, upload-time = "2026-08-18T08:52:53.736Z" },
-]
-
[[package]]
name = "wrapt"
version = "2.4.0"