diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3113c53..61f8be8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,5 +33,23 @@ jobs: - name: Mypy run: uv run mypy packages/cockroachdb/src packages/cockroachdb-core/src packages/cockroachdb-continuum/src packages/cockroachdb-testing/src + - name: Start CockroachDB + # Many tests (DDL compiler, dialect, alembic, integration) connect to a + # live node at localhost:26257; without one they fail with "connection + # refused". Testcontainers-based tests use the same Docker daemon. + run: | + docker run -d --name crdb -p 26257:26257 \ + cockroachdb/cockroach:latest-v25.1 start-single-node --insecure + for i in $(seq 1 60); do + if docker exec crdb ./cockroach sql --insecure -e 'SELECT 1' >/dev/null 2>&1; then + echo "CockroachDB ready"; break + fi + sleep 2 + done + - name: Pytest + env: + CRDB_URL: postgresql://root@localhost:26257/defaultdb?sslmode=disable + DATABASE_URL: postgresql://root@localhost:26257/defaultdb?sslmode=disable + COCKROACH_URL: postgresql://root@localhost:26257/defaultdb?sslmode=disable run: uv run pytest tests/ -v --tb=short diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..32d017a --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,56 @@ +name: Pages + +# Builds all workspace packages, a PEP 503 install index, and pdoc API docs, +# then deploys them to GitHub Pages: +# https://cockroachlabs.github.io/cockroachdb-python-library/ +# Install packages with: +# pip install --extra-index-url \ +# https://cockroachlabs.github.io/cockroachdb-python-library/simple/ cockroachdb-core +on: + push: + branches: [main] + release: + types: [published] + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: pages-${{ github.ref }} + cancel-in-progress: true + +jobs: + pages: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: astral-sh/setup-uv@v4 + + - name: Install Python + run: uv python install 3.12 + + - name: Sync workspace + run: uv sync --all-packages --all-extras + + - name: Build all packages + run: | + set -euo pipefail + for pkg in packages/*/; do + echo "Building $pkg" + ( cd "$pkg" && uv build ) + done + + - name: Generate API docs (pdoc) + run: uv run --with pdoc pdoc cockroachdb -o _site/apidocs + + - name: Assemble Pages site (landing + PEP 503 index) + run: python3 scripts/build_pages.py + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: _site + publish_branch: gh-pages diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index b90a38c..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Publish to PyPI - -on: - release: - types: [created] - -permissions: - id-token: write - -jobs: - publish: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: astral-sh/setup-uv@v4 - - - name: Build all packages - run: | - for pkg in packages/*/; do - echo "Building $pkg" - (cd "$pkg" && uv build) - done - - - name: Collect dist artifacts - run: | - mkdir -p dist - cp packages/*/dist/* dist/ - - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/README.md b/README.md index 7880e8a..2828388 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # CockroachDB Python Library -[![PyPI](https://img.shields.io/pypi/v/cockroachdb.svg)](https://pypi.org/project/cockroachdb/) -[![Python](https://img.shields.io/pypi/pyversions/cockroachdb.svg)](https://pypi.org/project/cockroachdb/) -[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) -[![CI](https://img.shields.io/github/actions/workflow/status/cockroachdb/cockroachdb-python-library/ci.yml?branch=main)](https://github.com/cockroachdb/cockroachdb-python-library/actions) +[![CI](https://github.com/cockroachlabs/cockroachdb-python-library/actions/workflows/ci.yml/badge.svg)](https://github.com/cockroachlabs/cockroachdb-python-library/actions/workflows/ci.yml) +[![Release](https://img.shields.io/github/v/release/cockroachlabs/cockroachdb-python-library?sort=semver)](https://github.com/cockroachlabs/cockroachdb-python-library/releases) +[![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/) +[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE) -A production-grade Python library for [CockroachDB](https://www.cockroachlabs.com/) that provides automatic transaction retry handling, multi-region support, follower reads, CDC webhook handlers, vector search, and RAG pipelines. Part of the CockroachDB language-specific library family alongside the [Java Spring Boot Starter](https://github.com/cockroachdb/cockroachdb-java-library), [.NET Library](https://github.com/cockroachdb/cockroachdb-dotnet-library), and [Go Library](https://github.com/cockroachdb/cockroachdb-go-library). +A production-grade Python library for [CockroachDB](https://www.cockroachlabs.com/) that provides automatic transaction retry handling, multi-region support, follower reads, CDC webhook handlers, vector search, and RAG pipelines. Part of the CockroachDB language-specific library family alongside the [Java Spring Boot Starter](https://github.com/cockroachlabs/cockroachdb-java-library), [.NET Library](https://github.com/cockroachlabs/cockroachdb-dotnet-library), and [Go Library](https://github.com/cockroachlabs/cockroachdb-go-library). ## Architecture @@ -21,6 +21,10 @@ The library is structured as a layered monorepo of six packages with a clear dep ## Quick Install +> Packages are published to this repository's GitHub Pages index (not PyPI). +> Add `--extra-index-url https://cockroachlabs.github.io/cockroachdb-python-library/simple/` +> to each `pip install` below, or set it once in your `pip.conf`. + ```bash # Base package (decorators, types, config, exceptions) pip install cockroachdb @@ -482,9 +486,16 @@ Optional surfaces for [Cockroach Continuum](https://www.cockroachlabs.com/docs/c ```python from cockroachdb.continuum import ( - CloudConfig, make_cloud, datadog_export, health, - VirtualClusterManager, routing_conn_string, - McpConfig, make_mcp, McpAegisClient, McpToolNames, + CloudConfig, + make_cloud, + datadog_export, + health, + VirtualClusterManager, + routing_conn_string, + McpConfig, + make_mcp, + McpAegisClient, + McpToolNames, ) # --- Cloud control plane (simulated until api_key is set) --- @@ -494,14 +505,16 @@ cloud.configure_metric_export(cluster_id, datadog_export(dd_key, "US1")) # type ready = health(cloud) # bool # --- Virtual clusters + routing --- -vm = VirtualClusterManager(conn) # any DB-API connection (e.g. psycopg) +vm = VirtualClusterManager(conn) # any DB-API connection (e.g. psycopg) vm.create("analytics") dsn = routing_conn_string(base_dsn, "analytics") # adds -ccluster option # --- MCP client (real 2025-06-18 wire protocol) + Aegis over MCP --- mcp = make_mcp(McpConfig(enabled=True, endpoint="https://cockroachlabs.cloud/mcp", token=key)) tools = mcp.list_tools() -aegis = McpAegisClient(mcp, McpToolNames(findings="list_findings")) # discover names via list_tools +aegis = McpAegisClient( + mcp, McpToolNames(findings="list_findings") +) # discover names via list_tools findings = aegis.findings() ``` @@ -511,7 +524,12 @@ Also in the `continuum` package: a standalone **Aegis** client (findings/recomme ```python from cockroachdb.continuum import ( - AegisConfig, make_aegis, AegisExhaust, storage_summary, vcluster, Priority, + AegisConfig, + make_aegis, + AegisExhaust, + storage_summary, + vcluster, + Priority, ) # Aegis — simulated until an endpoint is set: diff --git a/packages/cockroachdb-continuum/src/cockroachdb/continuum/mcp.py b/packages/cockroachdb-continuum/src/cockroachdb/continuum/mcp.py index 8994e3a..0bcbb49 100644 --- a/packages/cockroachdb-continuum/src/cockroachdb/continuum/mcp.py +++ b/packages/cockroachdb-continuum/src/cockroachdb/continuum/mcp.py @@ -17,19 +17,20 @@ import json import urllib.error import urllib.request +from collections.abc import Callable, Mapping from dataclasses import dataclass, field from itertools import count from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable if TYPE_CHECKING: - from collections.abc import Mapping + from typing import TypeAlias PROTOCOL_VERSION = "2025-06-18" # A transport maps (method, url, headers, body) -> (status, response_headers, text). -McpTransport = ( - "Callable[[str, str, Mapping[str, str], str | None], tuple[int, Mapping[str, str], str]]" -) +McpTransport: TypeAlias = Callable[ + [str, str, Mapping[str, str], str | None], tuple[int, Mapping[str, str], str] +] @dataclass(frozen=True) diff --git a/packages/cockroachdb-continuum/tests/test_cloud.py b/packages/cockroachdb-continuum/tests/test_cloud.py index ddfe3fc..ab08aed 100644 --- a/packages/cockroachdb-continuum/tests/test_cloud.py +++ b/packages/cockroachdb-continuum/tests/test_cloud.py @@ -54,8 +54,18 @@ def __call__(self, method, url, headers, body): def test_http_list_parses_and_sends_bearer(): body = json.dumps( - {"clusters": [{"id": "c1", "name": "prod", "plan": "STANDARD", - "state": "CREATED", "cloud_provider": "AWS", "regions": ["us-east-1"]}]} + { + "clusters": [ + { + "id": "c1", + "name": "prod", + "plan": "STANDARD", + "state": "CREATED", + "cloud_provider": "AWS", + "regions": ["us-east-1"], + } + ] + } ) t = _RecordingTransport({("GET", "/clusters"): (200, body)}) c = HttpCloudClient(CloudConfig(enabled=True, api_key="sk-123"), transport=t) diff --git a/pyproject.toml b/pyproject.toml index 3321c66..7a0e622 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,6 +57,8 @@ known-first-party = ["cockroachdb"] # Tests: names encode PostgreSQL error codes (e.g. 40P01) and fake exception # classes; these are intentional and not part of any public API. "tests/**" = ["N802", "N818", "E501", "SIM117"] +# scripts: build helpers with embedded HTML/CSS template strings (long lines). +"scripts/**" = ["E501"] [tool.mypy] python_version = "3.11" diff --git a/scripts/build_pages.py b/scripts/build_pages.py new file mode 100644 index 0000000..7ebba98 --- /dev/null +++ b/scripts/build_pages.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +"""Assemble the GitHub Pages site for the CockroachDB Python Library. + +Produces, under ``_site/``: + +* ``index.html`` — CU-themed landing (matches the developer-libraries site) + with a pip-install snippet and one card per package. +* ``apidocs/`` — generated by ``pdoc`` *before* this script runs. +* ``packages/`` — every built wheel / sdist. +* ``simple/`` — a PEP 503 "simple" index pip can install from via + ``--extra-index-url .../simple/``. + +Run after ``uv build`` has produced ``packages/*/dist/*`` and ``pdoc`` has +written ``_site/apidocs/``. Deterministic (no network, no timestamps). +""" + +from __future__ import annotations + +import html +import re +import shutil +import sys +import tomllib +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +SITE = ROOT / "_site" +REPO_SLUG = "cockroachlabs/cockroachdb-python-library" +PAGES_URL = "https://cockroachlabs.github.io/cockroachdb-python-library" + +# One-line blurbs for the landing cards (keyed by distribution name). +BLURBS = { + "cockroachdb": "Umbrella package — pulls in the full CockroachDB Python stack.", + "cockroachdb-core": "Transaction retry, pooling, follower reads, multi-region.", + "cockroachdb-continuum": "Plenum storage insights, vCluster tagging, Aegis, Cloud, MCP.", + "cockroachdb-sqlalchemy": "SQLAlchemy dialect and CockroachDB helpers.", + "cockroachdb-django": "Django database backend for CockroachDB.", + "cockroachdb-cdc": "Changefeed (CDC) webhook handlers.", + "cockroachdb-ai": "pgvector similarity search and RAG pipelines.", + "cockroachdb-testing": "pytest + Testcontainers helpers.", +} + + +def normalize(name: str) -> str: + """PEP 503 name normalization.""" + return re.sub(r"[-_.]+", "-", name).lower() + + +def discover() -> list[dict]: + """Read every workspace package: name, version, and its built dist files.""" + out = [] + for pyproject in sorted((ROOT / "packages").glob("*/pyproject.toml")): + data = tomllib.loads(pyproject.read_text())["project"] + pkgdir = pyproject.parent + dists = sorted((pkgdir / "dist").glob("*")) if (pkgdir / "dist").is_dir() else [] + out.append({"name": data["name"], "version": data["version"], "dists": dists}) + return out + + +def build_pep503(pkgs: list[dict]) -> None: + (SITE / "packages").mkdir(parents=True, exist_ok=True) + (SITE / "simple").mkdir(parents=True, exist_ok=True) + root_links = [] + for pkg in pkgs: + norm = normalize(pkg["name"]) + proj_dir = SITE / "simple" / norm + proj_dir.mkdir(exist_ok=True) + file_links = [] + for dist in pkg["dists"]: + shutil.copy2(dist, SITE / "packages" / dist.name) + # ../../packages/ relative to simple//index.html + file_links.append( + f'{html.escape(dist.name)}
' + ) + (proj_dir / "index.html").write_text( + f"" + f"Links for {html.escape(pkg['name'])}" + f"

Links for {html.escape(pkg['name'])}

\n" + + "\n".join(file_links) + + "\n" + ) + root_links.append(f'{html.escape(pkg["name"])}
') + (SITE / "simple" / "index.html").write_text( + "Simple index\n" + + "\n".join(root_links) + + "\n" + ) + + +def build_landing(pkgs: list[dict]) -> None: + version = next( + (p["version"] for p in pkgs if p["name"] == "cockroachdb-core"), pkgs[0]["version"] + ) + cards = [] + for pkg in pkgs: + blurb = BLURBS.get(pkg["name"], "CockroachDB Python package.") + cards.append(f"""
{html.escape(pkg["name"])} API reference + {html.escape(pkg["name"])} +

{html.escape(pkg["name"])}

+

{html.escape(blurb)}

+
API reference →
""") + cards_html = "\n".join(cards) + install = ( + f"pip install --extra-index-url {PAGES_URL}/simple/ \\\n" + " cockroachdb-core cockroachdb-continuum" + ) + SITE.mkdir(parents=True, exist_ok=True) + ( + SITE / "index.html" + ).write_text(f""" + +CockroachDB Python Library — API Reference + + + + + +
+ API · v{html.escape(version)} +

API Reference

+

Generated API documentation for the CockroachDB Python Library — a uv workspace of focused packages sharing the cockroachdb namespace. Install from the GitHub Pages package index below.

+
+
+

Install (GitHub Pages index)

+
{html.escape(install)}
+
+
+{cards_html} +
+ + +""") + + +def main() -> int: + pkgs = discover() + if not pkgs: + print("no packages found under packages/*/pyproject.toml", file=sys.stderr) + return 1 + build_pep503(pkgs) + build_landing(pkgs) + nfiles = sum(len(p["dists"]) for p in pkgs) + print(f"built _site: {len(pkgs)} packages, {nfiles} dist files, landing + simple index") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_continuum_mcp.py b/tests/test_continuum_mcp.py index f4d664c..a4251a7 100644 --- a/tests/test_continuum_mcp.py +++ b/tests/test_continuum_mcp.py @@ -47,7 +47,13 @@ def __call__(self, method, url, headers, body): return ( 200, {"Content-Type": "application/json", "Mcp-Session-Id": "sess-123"}, - json.dumps({"jsonrpc": "2.0", "id": req["id"], "result": {"protocolVersion": "2025-06-18"}}), + json.dumps( + { + "jsonrpc": "2.0", + "id": req["id"], + "result": {"protocolVersion": "2025-06-18"}, + } + ), ) if req["method"] == "tools/list": result = {"tools": [{"name": "list_findings", "description": "d"}]} @@ -89,11 +95,21 @@ def transport(method, url, headers, body): if "id" not in req: return 202, {}, "" if req["method"] == "initialize": - return 200, {"Content-Type": "application/json"}, json.dumps( - {"jsonrpc": "2.0", "id": req["id"], "result": {}} + return ( + 200, + {"Content-Type": "application/json"}, + json.dumps({"jsonrpc": "2.0", "id": req["id"], "result": {}}), ) - return 200, {"Content-Type": "application/json"}, json.dumps( - {"jsonrpc": "2.0", "id": req["id"], "error": {"code": -32601, "message": "method not found"}} + return ( + 200, + {"Content-Type": "application/json"}, + json.dumps( + { + "jsonrpc": "2.0", + "id": req["id"], + "error": {"code": -32601, "message": "method not found"}, + } + ), ) c = HttpMcpClient(McpConfig(endpoint="https://x/mcp"), transport)