pydantic-claude-code 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.3.0}/PKG-INFO +12 -10
  2. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.3.0}/README.md +11 -9
  3. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.3.0}/examples/basic.py +2 -3
  4. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.3.0}/pyproject.toml +1 -1
  5. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.3.0}/src/pydantic_ai_claude_code/__init__.py +2 -3
  6. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.3.0}/src/pydantic_ai_claude_code/model.py +23 -0
  7. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.3.0}/src/pydantic_ai_claude_code/provider.py +0 -10
  8. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.3.0}/tests/test_integration.py +2 -1
  9. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.3.0}/tests/test_provider.py +11 -2
  10. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.3.0}/.gitignore +0 -0
  11. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.3.0}/LICENSE +0 -0
  12. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.3.0}/src/pydantic_ai_claude_code/__main__.py +0 -0
  13. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.3.0}/src/pydantic_ai_claude_code/auth.py +0 -0
  14. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.3.0}/src/pydantic_ai_claude_code/config.py +0 -0
  15. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.3.0}/src/pydantic_ai_claude_code/credentials.py +0 -0
  16. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.3.0}/src/pydantic_ai_claude_code/flow.py +0 -0
  17. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.3.0}/src/pydantic_ai_claude_code/py.typed +0 -0
  18. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.3.0}/src/pydantic_ai_claude_code/storage.py +0 -0
  19. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.3.0}/tests/test_callback_server.py +0 -0
  20. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.3.0}/tests/test_credentials.py +0 -0
  21. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.3.0}/tests/test_flow.py +0 -0
  22. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.3.0}/tests/test_keyring_store.py +0 -0
  23. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.3.0}/tests/test_model.py +0 -0
  24. {pydantic_claude_code-0.2.0 → pydantic_claude_code-0.3.0}/tests/test_storage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pydantic-claude-code
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Use your Claude Code subscription from a Pydantic AI Agent, with full tool support
5
5
  Project-URL: Repository, https://github.com/mpfaffenberger/pydantic-ai-claude-code
6
6
  Project-URL: Homepage, https://pypi.org/project/pydantic-claude-code/
@@ -47,8 +47,9 @@ Two deliberate design choices distinguish this from a fork of pydantic-ai:
47
47
  executes your tools, and validates structured output. The wheel is a model
48
48
  + provider, not a second agent fighting for control.
49
49
  2. **Object-only resolution.** Instead of a `claude-code:` model-name string
50
- (which would require patching pydantic-ai's internals), pass the model
51
- object you build from the provider.
50
+ (which would require patching pydantic-ai's internals), construct a
51
+ `ClaudeCodeModel('claude-fable-5-1')` directly; it wires itself up to a
52
+ `ClaudeCodeProvider` by default.
52
53
 
53
54
  ## Quick start
54
55
 
@@ -57,15 +58,14 @@ import asyncio
57
58
 
58
59
  from pydantic_ai import Agent
59
60
 
60
- from pydantic_ai_claude_code import ClaudeCodeProvider, login
61
+ from pydantic_ai_claude_code import ClaudeCodeModel, login
61
62
 
62
63
  async def main() -> None:
63
64
  # One-time: opens your browser, mints tokens, stores them
64
65
  # (only needs to run again when tokens are revoked).
65
66
  await login()
66
67
 
67
- provider = ClaudeCodeProvider() # loads the stored tokens
68
- agent = Agent(provider.model('claude-fable-5-1'))
68
+ agent = Agent(ClaudeCodeModel('claude-fable-5-1'))
69
69
 
70
70
  result = await agent.run('Say hi in three words.')
71
71
  print(result.data)
@@ -77,9 +77,9 @@ asyncio.run(main())
77
77
 
78
78
  ```python
79
79
  from pydantic_ai import Agent
80
+ from pydantic_ai_claude_code import ClaudeCodeModel
80
81
 
81
- provider = ClaudeCodeProvider()
82
- agent = Agent(provider.model('claude-fable-5-1'))
82
+ agent = Agent(ClaudeCodeModel('claude-fable-5-1'))
83
83
 
84
84
  @agent.tool_plain
85
85
  def add(a: int, b: int) -> int:
@@ -96,12 +96,13 @@ definitions, and structured output works the same way as with the built-in
96
96
  ```python
97
97
  from pydantic import BaseModel
98
98
  from pydantic_ai import Agent
99
+ from pydantic_ai_claude_code import ClaudeCodeModel
99
100
 
100
101
  class Weather(BaseModel):
101
102
  city: str
102
103
  temperature_c: float
103
104
 
104
- agent = Agent(ClaudeCodeProvider().model('claude-fable-5-1'), output_type=Weather)
105
+ agent = Agent(ClaudeCodeModel('claude-fable-5-1'), output_type=Weather)
105
106
  result = await agent.run('Weather in Paris right now?')
106
107
  assert result.output.city == 'Paris'
107
108
  ```
@@ -110,8 +111,9 @@ assert result.output.city == 'Paris'
110
111
 
111
112
  ```python
112
113
  from pydantic_ai import Agent
114
+ from pydantic_ai_claude_code import ClaudeCodeModel
113
115
 
114
- agent = Agent(ClaudeCodeProvider().model('claude-fable-5-1'))
116
+ agent = Agent(ClaudeCodeModel('claude-fable-5-1'))
115
117
  async with agent.run_stream('Count from 1 to 3.') as stream:
116
118
  async for chunk in stream.stream_text():
117
119
  print(chunk, end='', flush=True)
@@ -23,8 +23,9 @@ Two deliberate design choices distinguish this from a fork of pydantic-ai:
23
23
  executes your tools, and validates structured output. The wheel is a model
24
24
  + provider, not a second agent fighting for control.
25
25
  2. **Object-only resolution.** Instead of a `claude-code:` model-name string
26
- (which would require patching pydantic-ai's internals), pass the model
27
- object you build from the provider.
26
+ (which would require patching pydantic-ai's internals), construct a
27
+ `ClaudeCodeModel('claude-fable-5-1')` directly; it wires itself up to a
28
+ `ClaudeCodeProvider` by default.
28
29
 
29
30
  ## Quick start
30
31
 
@@ -33,15 +34,14 @@ import asyncio
33
34
 
34
35
  from pydantic_ai import Agent
35
36
 
36
- from pydantic_ai_claude_code import ClaudeCodeProvider, login
37
+ from pydantic_ai_claude_code import ClaudeCodeModel, login
37
38
 
38
39
  async def main() -> None:
39
40
  # One-time: opens your browser, mints tokens, stores them
40
41
  # (only needs to run again when tokens are revoked).
41
42
  await login()
42
43
 
43
- provider = ClaudeCodeProvider() # loads the stored tokens
44
- agent = Agent(provider.model('claude-fable-5-1'))
44
+ agent = Agent(ClaudeCodeModel('claude-fable-5-1'))
45
45
 
46
46
  result = await agent.run('Say hi in three words.')
47
47
  print(result.data)
@@ -53,9 +53,9 @@ asyncio.run(main())
53
53
 
54
54
  ```python
55
55
  from pydantic_ai import Agent
56
+ from pydantic_ai_claude_code import ClaudeCodeModel
56
57
 
57
- provider = ClaudeCodeProvider()
58
- agent = Agent(provider.model('claude-fable-5-1'))
58
+ agent = Agent(ClaudeCodeModel('claude-fable-5-1'))
59
59
 
60
60
  @agent.tool_plain
61
61
  def add(a: int, b: int) -> int:
@@ -72,12 +72,13 @@ definitions, and structured output works the same way as with the built-in
72
72
  ```python
73
73
  from pydantic import BaseModel
74
74
  from pydantic_ai import Agent
75
+ from pydantic_ai_claude_code import ClaudeCodeModel
75
76
 
76
77
  class Weather(BaseModel):
77
78
  city: str
78
79
  temperature_c: float
79
80
 
80
- agent = Agent(ClaudeCodeProvider().model('claude-fable-5-1'), output_type=Weather)
81
+ agent = Agent(ClaudeCodeModel('claude-fable-5-1'), output_type=Weather)
81
82
  result = await agent.run('Weather in Paris right now?')
82
83
  assert result.output.city == 'Paris'
83
84
  ```
@@ -86,8 +87,9 @@ assert result.output.city == 'Paris'
86
87
 
87
88
  ```python
88
89
  from pydantic_ai import Agent
90
+ from pydantic_ai_claude_code import ClaudeCodeModel
89
91
 
90
- agent = Agent(ClaudeCodeProvider().model('claude-fable-5-1'))
92
+ agent = Agent(ClaudeCodeModel('claude-fable-5-1'))
91
93
  async with agent.run_stream('Count from 1 to 3.') as stream:
92
94
  async for chunk in stream.stream_text():
93
95
  print(chunk, end='', flush=True)
@@ -11,7 +11,7 @@ import asyncio
11
11
 
12
12
  from pydantic_ai import Agent
13
13
 
14
- from pydantic_ai_claude_code import ClaudeCodeProvider, default_store, login
14
+ from pydantic_ai_claude_code import ClaudeCodeModel, default_store, login
15
15
 
16
16
 
17
17
  async def main() -> None:
@@ -19,8 +19,7 @@ async def main() -> None:
19
19
  if default_store().load() is None:
20
20
  await login()
21
21
 
22
- provider = ClaudeCodeProvider()
23
- agent = Agent(provider.model("claude-fable-5-1"))
22
+ agent = Agent(ClaudeCodeModel("claude-fable-5-1"))
24
23
 
25
24
  result = await agent.run("Say hi in exactly three words.")
26
25
  print("Agent said:", result.output)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pydantic-claude-code"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Use your Claude Code subscription from a Pydantic AI Agent, with full tool support"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11,<3.15"
@@ -3,10 +3,9 @@
3
3
  Quick start:
4
4
 
5
5
  from pydantic_ai import Agent
6
- from pydantic_ai_claude_code import ClaudeCodeProvider
6
+ from pydantic_ai_claude_code import ClaudeCodeModel
7
7
 
8
- provider = ClaudeCodeProvider()
9
- agent = Agent(provider.model('claude-fable-5-1'))
8
+ agent = Agent(ClaudeCodeModel('claude-fable-5-1'))
10
9
  """
11
10
 
12
11
  from .credentials import ClaudeCodeCredentials
@@ -2,8 +2,11 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from typing import Any
6
+
5
7
  from pydantic_ai.messages import ModelMessage, ModelRequest, SystemPromptPart
6
8
  from pydantic_ai.models.anthropic import AnthropicModel
9
+ from pydantic_ai.providers import Provider
7
10
 
8
11
  from . import config
9
12
 
@@ -17,6 +20,26 @@ class ClaudeCodeModel(AnthropicModel):
17
20
  duplicate it.
18
21
  """
19
22
 
23
+ def __init__(
24
+ self,
25
+ model_name: str,
26
+ *,
27
+ provider: Provider[Any] | None = None,
28
+ **kwargs: Any,
29
+ ) -> None:
30
+ """Initialize a Claude Code model.
31
+
32
+ Args:
33
+ model_name: The model name to use, e.g. `'claude-fable-5-1'`.
34
+ provider: The provider to authenticate with. Defaults to a
35
+ `ClaudeCodeProvider` backed by the default token store.
36
+ """
37
+ if provider is None:
38
+ from .provider import ClaudeCodeProvider
39
+
40
+ provider = ClaudeCodeProvider()
41
+ super().__init__(model_name, provider=provider, **kwargs)
42
+
20
43
  def prepare_messages(
21
44
  self,
22
45
  messages: list[ModelMessage],
@@ -12,7 +12,6 @@ from pydantic_ai.exceptions import UserError
12
12
  from pydantic_ai.providers.anthropic import AnthropicProvider
13
13
 
14
14
  from . import config
15
- from .model import ClaudeCodeModel
16
15
  from .auth import _ClaudeCodeAuth
17
16
  from .credentials import ClaudeCodeCredentials
18
17
  from .storage import TokenStore, default_store
@@ -85,15 +84,6 @@ class ClaudeCodeProvider(AnthropicProvider):
85
84
  """The current credentials; refreshed in place by the auth shim."""
86
85
  return self._claude_code_auth.credentials
87
86
 
88
- def model(self, model_name: str) -> ClaudeCodeModel:
89
- """Build a `ClaudeCodeModel` bound to this provider.
90
-
91
- This is the object-only entry point agreed for the wheel: pass its result
92
- to `Agent(model=...)` without modifying pydantic-ai. The persona is
93
- prepended to the system context automatically.
94
- """
95
- return ClaudeCodeModel(model_name, provider=self)
96
-
97
87
  @classmethod
98
88
  def from_token_store(
99
89
  cls,
@@ -16,6 +16,7 @@ import pytest
16
16
  from pydantic_ai import Agent
17
17
 
18
18
  from pydantic_ai_claude_code.credentials import ClaudeCodeCredentials
19
+ from pydantic_ai_claude_code.model import ClaudeCodeModel
19
20
  from pydantic_ai_claude_code.provider import ClaudeCodeProvider
20
21
 
21
22
 
@@ -58,7 +59,7 @@ def messages_stub(monkeypatch) -> ThreadingHTTPServer:
58
59
  def test_agent_round_trip(messages_stub, model_name: str) -> None:
59
60
  creds = ClaudeCodeCredentials(access_token="sub-token", refresh_token="refresh")
60
61
  provider = ClaudeCodeProvider(credentials=creds, base_url=f"http://127.0.0.1:{messages_stub.server_address[1]}")
61
- agent = Agent(provider.model(model_name))
62
+ agent = Agent(ClaudeCodeModel(model_name, provider=provider))
62
63
 
63
64
  result = asyncio_run(agent.run("Say hi."))
64
65
 
@@ -8,6 +8,7 @@ import httpx2
8
8
  from pydantic_ai.exceptions import UserError
9
9
 
10
10
  from pydantic_ai_claude_code.credentials import ClaudeCodeCredentials
11
+ from pydantic_ai_claude_code.model import ClaudeCodeModel
11
12
  from pydantic_ai_claude_code.provider import ClaudeCodeProvider
12
13
  from pydantic_ai_claude_code.storage import ClaudeCodeTokenStore
13
14
 
@@ -23,13 +24,21 @@ def test_credentials_property() -> None:
23
24
  assert provider.credentials.token == "a"
24
25
 
25
26
 
26
- def test_model_binding() -> None:
27
+ def test_model_binds_explicit_provider() -> None:
27
28
  provider = ClaudeCodeProvider(credentials=ClaudeCodeCredentials(access_token="a", refresh_token="b"))
28
- model = provider.model("claude-sonnet-4-5")
29
+ model = ClaudeCodeModel("claude-sonnet-4-5", provider=provider)
29
30
  assert model.model_name == "claude-sonnet-4-5"
30
31
  assert model._provider is provider # type: ignore[attr-defined]
31
32
 
32
33
 
34
+ def test_model_defaults_provider(tmp_path, monkeypatch) -> None:
35
+ monkeypatch.setenv("CLAUDE_CODE_CREDENTIALS", "file")
36
+ monkeypatch.setenv("CLAUDE_CODE_AUTH_FILE", str(tmp_path / "auth.json"))
37
+ ClaudeCodeTokenStore().save(ClaudeCodeCredentials(access_token="a", refresh_token="b"))
38
+ model = ClaudeCodeModel("claude-sonnet-4-5")
39
+ assert isinstance(model._provider, ClaudeCodeProvider) # type: ignore[attr-defined]
40
+
41
+
33
42
  def test_missing_credentials_raises(tmp_path) -> None:
34
43
  store = ClaudeCodeTokenStore(path=tmp_path / "missing.json")
35
44
  with pytest.raises(UserError):