diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b47f6b34..299988b03 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -378,27 +378,60 @@ jobs: env: PYTHONFAULTHANDLER: "1" PYTEST_ADDOPTS: "--max-worker-restart=0" - # At 22min, broadcast SIGABRT to ALL python processes (controller + xdist - # workers). Faulthandler in each process dumps every thread to stderr - # before exit, so the test step log captures the actual hang site. + # At 22min, signal the pytest process group with SIGABRT so faulthandler + # dumps thread stacks to stderr, with bounded escalation to SIGTERM and SIGKILL. + # Background watchdog is trapped and reaped reliably on any exit. run: | set -m - ( - sleep 1320 - echo "::group::Hang detected - SIGABRT to all python" - pkill -ABRT -f python || true - sleep 10 - echo "::endgroup::" - ) & - aborter_pid=$! + pytest_pgid="" + aborter_pid="" + + cleanup() { + local exit_code=$? + trap - EXIT INT TERM + if [ -n "$aborter_pid" ] && kill -0 -"$aborter_pid" 2>/dev/null; then + kill -TERM -"$aborter_pid" 2>/dev/null || true + wait "$aborter_pid" 2>/dev/null || true + fi + if [ -n "$pytest_pgid" ] && kill -0 -"$pytest_pgid" 2>/dev/null; then + kill -TERM -"$pytest_pgid" 2>/dev/null || true + sleep 2 + kill -KILL -"$pytest_pgid" 2>/dev/null || true + fi + exit "$exit_code" + } + trap cleanup EXIT + trap 'exit 130' INT + trap 'exit 143' TERM + # covdefaults sets fail_under=100 globally; per-tier coverage # only covers a slice, so override to 0. Codecov merges the # tiers server-side. - uv run pytest tests/integration docs/examples -n 2 --dist=loadgroup \ + uv run pytest tests/integration -n 2 --dist=loadgroup \ --cov=sqlspec --cov-report=xml:coverage-integration.xml --cov-report=term-missing \ - --cov-fail-under=0 + --cov-fail-under=0 & + pytest_pid=$! + # Job control makes the background job leader its process-group leader. + pytest_pgid=$pytest_pid + + ( + sleep 1320 + if kill -0 -"$pytest_pgid" 2>/dev/null; then + echo "::group::Hang detected after 22 minutes - SIGABRT to pytest process group $pytest_pgid" + kill -ABRT -"$pytest_pgid" 2>/dev/null || true + sleep 15 + if kill -0 -"$pytest_pgid" 2>/dev/null; then + kill -TERM -"$pytest_pgid" 2>/dev/null || true + sleep 5 + kill -KILL -"$pytest_pgid" 2>/dev/null || true + fi + echo "::endgroup::" + fi + ) & + aborter_pid=$! + + wait "$pytest_pid" rc=$? - kill "$aborter_pid" 2>/dev/null || true exit $rc - name: Upload coverage to Codecov @@ -427,64 +460,6 @@ jobs: docker logs --tail 100 "$c" 2>&1 || true done - # test-linux-freethreaded: - # # Disabled: cp314t ecosystem not mature enough yet (missing binary wheels, long build times) - # runs-on: ubuntu-latest - # continue-on-error: true - # timeout-minutes: 30 - # steps: - # - uses: actions/checkout@v4 - # - uses: astral-sh/setup-uv@v3 - # - run: uv python install 3.14t - # - run: uv sync --extra aiosqlite --extra duckdb --dev - # - run: uv run pytest tests/unit -n 2 --dist=loadgroup - - # test-windows: - # runs-on: windows-latest - # strategy: - # fail-fast: true - # matrix: - # python-version: ["3.12", "3.13"] - # timeout-minutes: 30 - # steps: - # - name: Check out repository - # uses: actions/checkout@v4 - - # - name: Install uv - # uses: astral-sh/setup-uv@v3 - - # - name: Set up Python - # run: uv python install ${{ matrix.python-version }} - - # - name: Install dependencies - # run: uv sync --all-extras --dev - - # - name: Test - # run: uv run pytest -m "" - - # test-osx: - # runs-on: macos-latest - # strategy: - # fail-fast: true - # matrix: - # python-version: ["3.11", "3.12", "3.13"] - # timeout-minutes: 30 - # steps: - # - name: Check out repository - # uses: actions/checkout@v4 - - # - name: Install uv - # uses: astral-sh/setup-uv@v3 - - # - name: Set up Python - # run: uv python install ${{ matrix.python-version }} - - # - name: Install dependencies - # run: uv sync --all-extras --dev - - # - name: Test - # run: uv run pytest -m "" - build-docs: needs: - ci-scope diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e4afe6b83..d8328df2e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -40,12 +40,12 @@ jobs: run: uv sync --all-extras --dev - name: Build documentation - run: uv run python tools/build_docs.py docs-build + run: make docs - name: Upload Pages artifact uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5 with: - path: docs-build + path: docs/_build/html deploy: needs: build diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 980153620..316f43129 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,13 +1,6 @@ default_language_version: python: "3" repos: - - repo: local - hooks: - - id: no-future-annotations - name: Disallow future annotations in package code - entry: python tools/hooks/no_future_annotations.py - language: python - files: ^sqlspec/.*\.py$ - repo: https://github.com/compilerla/conventional-pre-commit rev: v4.4.0 hooks: @@ -52,10 +45,3 @@ repos: hooks: - id: sphinx-lint args: ["--jobs", "1"] - - repo: local - hooks: - - id: pypi-readme - name: pypi-readme - language: python - entry: python tools/pypi_readme.py - types: [markdown] diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 2f9f46ed7..ee8e9c3f5 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -34,7 +34,7 @@ Guidelines for writing code - All code must be fully typed (enforced via `mypy `_ and `pyright `_). Avoid ``Any`` whenever possible. - Use PEP 585 built-in collection types (``dict``, ``list``, ``tuple``, ``set``) rather than their ``typing`` module equivalents. - Use PEP 604 union types (``T | None``), never ``Optional[T]`` or ``Union[T, None]``. - - Package modules must **not** import ``from __future__ import annotations``. Prek checks this using an AST-based local hook. + - Package modules must **not** import ``from __future__ import annotations``. This is enforced via Ruff rule ``TID251`` (banned API). - **Comments & Documentation**: - **Never use in-line comments in Python code**. If code requires explanation, document the rationale in the class or function docstring. @@ -72,8 +72,7 @@ Writing and running tests - Use unit tests (`tests/unit/`) for code that does not need a database (parsing, config normalization, parameter formatting). - Review the `test placement guide `_ 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.** - -[![PyPI](https://img.shields.io/pypi/v/sqlspec)](https://pypi.org/project/sqlspec/) -[![Python](https://img.shields.io/pypi/pyversions/sqlspec)](https://pypi.org/project/sqlspec/) -[![License](https://img.shields.io/pypi/l/sqlspec)](https://github.com/litestar-org/sqlspec/blob/main/LICENSE) -[![Docs](https://img.shields.io/badge/docs-sqlspec.dev-blue)](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 = 'Litestar Logo - Light' - - -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"