build: promote anthropic, postgres, console, tls to core dependencies

The Anthropic SDK provider was the lone first-class provider gated behind
an optional extra, while OpenAI ships in core and Google rides the
OpenAI-compatible path. Fold anthropic, psycopg (postgres), croniter
(console), and lacme (tls) into the base dependency set so a default
`pip install turnstone` yields a complete single- or multi-node
deployment; only the Discord/Slack channel gateways stay optional.

- pyproject: four extras → base deps; `all` is now discord+slack; drop the
  redundant croniter from the `test` extra; regenerate uv.lock.
- ci: the postgres test job installs `.[test]` (psycopg is base now).
- providers: `_ensure_anthropic` becomes a thin SDK accessor for
  `create_client`; drop the now-redundant eager import-guard calls from
  the streaming/completion hot path (anthropic is always present).
- bootstrap: import anthropic directly.
- tests/docs: drop the anthropic importorskips and stale extra-install hints.
This commit is contained in:
Patrick Buckley
2026-06-04 00:09:45 -07:00
parent b2273089bd
commit b3c1b9c9e0
8 changed files with 29 additions and 59 deletions
+1 -1
View File
@@ -82,7 +82,7 @@ jobs:
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: "24"
- run: pip install -e ".[test,postgres]"
- run: pip install -e ".[test]"
- run: pytest tests/ -m "not live" --storage-backend=postgresql -q
env:
TURNSTONE_TEST_PG_URL: postgresql+psycopg://postgres:postgres@localhost:5432/turnstone_test
+1 -3
View File
@@ -51,14 +51,12 @@ turnstone --base-url http://localhost:8000/v1
turnstone-server --port 8080 --base-url http://localhost:8000/v1
# Cluster dashboard
pip install turnstone[console]
turnstone-console --port 8090
```
For PostgreSQL (recommended for production):
```bash
pip install turnstone[postgres]
export TURNSTONE_DB_BACKEND=postgresql
export TURNSTONE_DB_URL="postgresql+psycopg://user:pass@localhost:5432/turnstone"
turnstone-server --port 8080 --base-url http://localhost:8000/v1
@@ -161,7 +159,7 @@ UML diagrams in [`docs/diagrams/`](docs/diagrams/):
- Python 3.11+
- An OpenAI-compatible API endpoint, Anthropic API key, or Google Gemini API key
- Optional: PostgreSQL (`pip install turnstone[postgres]`), Anthropic (`pip install turnstone[anthropic]`)
- Optional: Discord / Slack channel integrations (`pip install turnstone[discord,slack]`)
- [Git LFS](https://git-lfs.com/) for cloning (diagram PNGs)
## Community
+2 -3
View File
@@ -652,9 +652,8 @@ display). Automatic prompt caching is enabled via top-level `cache_control:
cacheable block and advances it as conversations grow (90% input cost
reduction on cache hits, 1.25x write on first turn). Cache metrics
(`cache_creation_input_tokens`, `cache_read_input_tokens`) are extracted from
both streaming and non-streaming responses. The `anthropic` SDK is imported
lazily so it remains an optional dependency (`pip install
turnstone[anthropic]`).
both streaming and non-streaming responses. The `anthropic` SDK is a core
dependency — the Anthropic provider is first-class alongside OpenAI.
**GoogleProvider** (`_google.py`): extends `OpenAIChatCompletionsProvider` for
the Gemini `/v1beta/openai/` endpoint. Uses a single default
+6 -6
View File
@@ -23,6 +23,7 @@ classifiers = [
]
dependencies = [
"openai>=2.37",
"anthropic>=0.39",
"httpx>=0.28",
"mcp>=1.27",
"starlette>=1.0.1", # PYSEC-2026-161: host-header path-injection in URL reconstruction (auth-bypass on apps comparing reconstructed URL paths)
@@ -32,10 +33,13 @@ dependencies = [
"pydantic>=2.0",
"sqlalchemy>=2.0",
"alembic>=1.14",
"psycopg[binary]>=3.2",
"croniter>=3.0",
"structlog>=24.1",
"PyJWT>=2.8",
"bcrypt>=4.0",
"cryptography>=42",
"lacme>=1.0.5",
"python-frontmatter>=1.0",
]
@@ -45,15 +49,11 @@ Repository = "https://github.com/turnstonelabs/turnstone"
Issues = "https://github.com/turnstonelabs/turnstone/issues"
[project.optional-dependencies]
test = ["pytest>=9.0", "pytest-cov>=6.0", "croniter>=3.0", "slack-bolt>=1.18", "aiohttp>=3.9"]
test = ["pytest>=9.0", "pytest-cov>=6.0", "slack-bolt>=1.18", "aiohttp>=3.9"]
dev = ["ruff>=0.9", "mypy>=1.14"]
console = ["croniter>=3.0"]
anthropic = ["anthropic>=0.39"]
postgres = ["psycopg[binary]>=3.2"]
discord = ["discord.py>=2.4"]
tls = ["lacme>=1.0.5"]
slack = ["slack-bolt>=1.18", "aiohttp>=3.9"]
all = ["turnstone[console,anthropic,postgres,discord,tls,slack]"]
all = ["turnstone[discord,slack]"]
[project.scripts]
turnstone = "turnstone.cli:main"
+1 -5
View File
@@ -26,8 +26,6 @@ from types import SimpleNamespace
from typing import Any
from unittest.mock import MagicMock, patch
import pytest
from tests._session_helpers import make_session as _make_session
@@ -300,7 +298,6 @@ class TestSessionToWireBoundaryIntegration:
the resolver pre-set to *replay_flag*. Returns the kwargs
dict that reached the (mocked) Anthropic SDK boundary.
"""
pytest.importorskip("anthropic")
from turnstone.core.providers._anthropic import AnthropicProvider
session = _make_session()
@@ -322,7 +319,7 @@ class TestSessionToWireBoundaryIntegration:
model_alias="claude-opus-4-7",
)
# Iterate the stream to drain the (empty) generator and ensure
# _ensure_anthropic / convert / build_kwargs all ran.
# convert / build_kwargs all ran.
list(stream)
return captured
@@ -386,7 +383,6 @@ class TestSessionToWireBoundaryIntegration:
# supports_reasoning_replay=False. AND-gate at the resolver
# blocks replay, so the strip predicate fires at the wire and
# the thinking block does NOT reach the SDK boundary.
pytest.importorskip("anthropic")
from turnstone.core.providers._anthropic import AnthropicProvider
from turnstone.core.providers._protocol import ModelCapabilities
+1 -6
View File
@@ -976,12 +976,7 @@ def _setup_openai() -> tuple[str, Any, str]:
def _setup_anthropic() -> tuple[str, Any, str]:
try:
import anthropic
except ImportError:
print(f"\n {RED}The 'anthropic' package is not installed.{RESET}")
print(f" Install it with: {CYAN}pip install turnstone[anthropic]{RESET}")
sys.exit(1)
import anthropic
api_key = _prompt_api_key("ANTHROPIC_API_KEY", "Enter your Anthropic API key:")
model = _prompt_model("anthropic")
+7 -11
View File
@@ -29,16 +29,14 @@ log = logging.getLogger(__name__)
def _ensure_anthropic() -> Any:
"""Lazy import anthropic SDK, raising helpful error if not installed."""
try:
import anthropic # noqa: PLC0415
"""Return the ``anthropic`` SDK module (a core dependency).
return anthropic
except ImportError:
raise ImportError(
"The 'anthropic' package is required for Anthropic provider. "
"Install it with: pip install 'turnstone[anthropic]'"
) from None
Kept as a thin accessor so ``create_client`` (and tests) can obtain the
SDK via a single seam without a module-top import.
"""
import anthropic # noqa: PLC0415
return anthropic
# -- message format helpers --------------------------------------------------
@@ -700,7 +698,6 @@ class AnthropicProvider:
resolve_attachments: Callable[[list[str]], dict[str, dict[str, Any]]] | None = None,
) -> Iterator[StreamChunk]:
messages = materialize_attachments(messages, resolve_attachments)
_ensure_anthropic()
caps = capabilities or self.get_capabilities(model)
system_prompt, converted_msgs = self._convert_messages(
messages,
@@ -914,7 +911,6 @@ class AnthropicProvider:
resolve_attachments: Callable[[list[str]], dict[str, dict[str, Any]]] | None = None,
) -> CompletionResult:
messages = materialize_attachments(messages, resolve_attachments)
_ensure_anthropic()
caps = capabilities or self.get_capabilities(model)
system_prompt, converted_msgs = self._convert_messages(
messages,
Generated
+10 -24
View File
@@ -2311,12 +2311,16 @@ version = "1.6.0a10"
source = { editable = "." }
dependencies = [
{ name = "alembic" },
{ name = "anthropic" },
{ name = "bcrypt" },
{ name = "croniter" },
{ name = "cryptography" },
{ name = "httpx" },
{ name = "httpx-sse" },
{ name = "lacme" },
{ name = "mcp" },
{ name = "openai" },
{ name = "psycopg", extra = ["binary"] },
{ name = "pydantic" },
{ name = "pyjwt" },
{ name = "python-frontmatter" },
@@ -2330,19 +2334,9 @@ dependencies = [
[package.optional-dependencies]
all = [
{ name = "aiohttp" },
{ name = "anthropic" },
{ name = "croniter" },
{ name = "discord-py" },
{ name = "lacme" },
{ name = "psycopg", extra = ["binary"] },
{ name = "slack-bolt" },
]
anthropic = [
{ name = "anthropic" },
]
console = [
{ name = "croniter" },
]
dev = [
{ name = "mypy" },
{ name = "ruff" },
@@ -2350,42 +2344,34 @@ dev = [
discord = [
{ name = "discord-py" },
]
postgres = [
{ name = "psycopg", extra = ["binary"] },
]
slack = [
{ name = "aiohttp" },
{ name = "slack-bolt" },
]
test = [
{ name = "aiohttp" },
{ name = "croniter" },
{ name = "pytest" },
{ name = "pytest-cov" },
{ name = "slack-bolt" },
]
tls = [
{ name = "lacme" },
]
[package.metadata]
requires-dist = [
{ name = "aiohttp", marker = "extra == 'slack'", specifier = ">=3.9" },
{ name = "aiohttp", marker = "extra == 'test'", specifier = ">=3.9" },
{ name = "alembic", specifier = ">=1.14" },
{ name = "anthropic", marker = "extra == 'anthropic'", specifier = ">=0.39" },
{ name = "anthropic", specifier = ">=0.39" },
{ name = "bcrypt", specifier = ">=4.0" },
{ name = "croniter", marker = "extra == 'console'", specifier = ">=3.0" },
{ name = "croniter", marker = "extra == 'test'", specifier = ">=3.0" },
{ name = "croniter", specifier = ">=3.0" },
{ name = "cryptography", specifier = ">=42" },
{ name = "discord-py", marker = "extra == 'discord'", specifier = ">=2.4" },
{ name = "httpx", specifier = ">=0.28" },
{ name = "httpx-sse", specifier = ">=0.4" },
{ name = "lacme", marker = "extra == 'tls'", specifier = ">=1.0.5" },
{ name = "lacme", specifier = ">=1.0.5" },
{ name = "mcp", specifier = ">=1.27" },
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.14" },
{ name = "openai", specifier = ">=2.37" },
{ name = "psycopg", extras = ["binary"], marker = "extra == 'postgres'", specifier = ">=3.2" },
{ name = "psycopg", extras = ["binary"], specifier = ">=3.2" },
{ name = "pydantic", specifier = ">=2.0" },
{ name = "pyjwt", specifier = ">=2.8" },
{ name = "pytest", marker = "extra == 'test'", specifier = ">=9.0" },
@@ -2398,10 +2384,10 @@ requires-dist = [
{ name = "sse-starlette", specifier = ">=2.0" },
{ name = "starlette", specifier = ">=1.0.1" },
{ name = "structlog", specifier = ">=24.1" },
{ name = "turnstone", extras = ["console", "anthropic", "postgres", "discord", "tls", "slack"], marker = "extra == 'all'" },
{ name = "turnstone", extras = ["discord", "slack"], marker = "extra == 'all'" },
{ name = "uvicorn", specifier = ">=0.34" },
]
provides-extras = ["test", "dev", "console", "anthropic", "postgres", "discord", "tls", "slack", "all"]
provides-extras = ["test", "dev", "discord", "slack", "all"]
[[package]]
name = "typing-extensions"