From b3c1b9c9e0ed1717485fac5e4c328205cfe0535c Mon Sep 17 00:00:00 2001 From: Patrick Buckley Date: Thu, 4 Jun 2026 00:09:45 -0700 Subject: [PATCH] build: promote anthropic, postgres, console, tls to core dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .github/workflows/ci.yml | 2 +- README.md | 4 +-- docs/architecture.md | 5 ++-- pyproject.toml | 12 ++++----- tests/test_session_replay_reasoning.py | 6 +---- turnstone/bootstrap.py | 7 +----- turnstone/core/providers/_anthropic.py | 18 ++++++-------- uv.lock | 34 ++++++++------------------ 8 files changed, 29 insertions(+), 59 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 461b9556..b26223a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/README.md b/README.md index f83e055c..2916cb3f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/architecture.md b/docs/architecture.md index 24e40e5b..06bf0114 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 diff --git a/pyproject.toml b/pyproject.toml index a3529c72..521b611c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/tests/test_session_replay_reasoning.py b/tests/test_session_replay_reasoning.py index 948f0bce..bbfe70d9 100644 --- a/tests/test_session_replay_reasoning.py +++ b/tests/test_session_replay_reasoning.py @@ -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 diff --git a/turnstone/bootstrap.py b/turnstone/bootstrap.py index b1ca4a93..89ba03a9 100644 --- a/turnstone/bootstrap.py +++ b/turnstone/bootstrap.py @@ -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") diff --git a/turnstone/core/providers/_anthropic.py b/turnstone/core/providers/_anthropic.py index 14c5590f..52a236ef 100644 --- a/turnstone/core/providers/_anthropic.py +++ b/turnstone/core/providers/_anthropic.py @@ -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, diff --git a/uv.lock b/uv.lock index de57141c..0697f419 100644 --- a/uv.lock +++ b/uv.lock @@ -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"