pydantic-ai-slim 0.4.6__py3-none-any.whl → 0.4.8__py3-none-any.whl
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.
- pydantic_ai/_parts_manager.py +31 -5
- pydantic_ai/ag_ui.py +67 -77
- pydantic_ai/agent.py +5 -3
- pydantic_ai/mcp.py +97 -37
- pydantic_ai/messages.py +84 -21
- pydantic_ai/models/__init__.py +11 -0
- pydantic_ai/models/anthropic.py +11 -3
- pydantic_ai/models/bedrock.py +4 -2
- pydantic_ai/models/cohere.py +6 -6
- pydantic_ai/models/function.py +4 -2
- pydantic_ai/models/gemini.py +5 -1
- pydantic_ai/models/google.py +9 -2
- pydantic_ai/models/groq.py +6 -2
- pydantic_ai/models/huggingface.py +6 -2
- pydantic_ai/models/mistral.py +3 -1
- pydantic_ai/models/openai.py +34 -7
- pydantic_ai/models/test.py +6 -2
- pydantic_ai/profiles/openai.py +8 -0
- pydantic_ai/providers/__init__.py +8 -0
- pydantic_ai/providers/moonshotai.py +97 -0
- pydantic_ai/providers/vercel.py +107 -0
- pydantic_ai/retries.py +249 -0
- pydantic_ai/toolsets/combined.py +4 -3
- {pydantic_ai_slim-0.4.6.dist-info → pydantic_ai_slim-0.4.8.dist-info}/METADATA +9 -6
- {pydantic_ai_slim-0.4.6.dist-info → pydantic_ai_slim-0.4.8.dist-info}/RECORD +28 -25
- {pydantic_ai_slim-0.4.6.dist-info → pydantic_ai_slim-0.4.8.dist-info}/WHEEL +0 -0
- {pydantic_ai_slim-0.4.6.dist-info → pydantic_ai_slim-0.4.8.dist-info}/entry_points.txt +0 -0
- {pydantic_ai_slim-0.4.6.dist-info → pydantic_ai_slim-0.4.8.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations as _annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Literal, overload
|
|
5
|
+
|
|
6
|
+
from httpx import AsyncClient as AsyncHTTPClient
|
|
7
|
+
from openai import AsyncOpenAI
|
|
8
|
+
|
|
9
|
+
from pydantic_ai.exceptions import UserError
|
|
10
|
+
from pydantic_ai.models import cached_async_http_client
|
|
11
|
+
from pydantic_ai.profiles import ModelProfile
|
|
12
|
+
from pydantic_ai.profiles.moonshotai import moonshotai_model_profile
|
|
13
|
+
from pydantic_ai.profiles.openai import (
|
|
14
|
+
OpenAIJsonSchemaTransformer,
|
|
15
|
+
OpenAIModelProfile,
|
|
16
|
+
)
|
|
17
|
+
from pydantic_ai.providers import Provider
|
|
18
|
+
|
|
19
|
+
MoonshotAIModelName = Literal[
|
|
20
|
+
'moonshot-v1-8k',
|
|
21
|
+
'moonshot-v1-32k',
|
|
22
|
+
'moonshot-v1-128k',
|
|
23
|
+
'moonshot-v1-8k-vision-preview',
|
|
24
|
+
'moonshot-v1-32k-vision-preview',
|
|
25
|
+
'moonshot-v1-128k-vision-preview',
|
|
26
|
+
'kimi-latest',
|
|
27
|
+
'kimi-thinking-preview',
|
|
28
|
+
'kimi-k2-0711-preview',
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MoonshotAIProvider(Provider[AsyncOpenAI]):
|
|
33
|
+
"""Provider for MoonshotAI platform (Kimi models)."""
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def name(self) -> str:
|
|
37
|
+
return 'moonshotai'
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def base_url(self) -> str:
|
|
41
|
+
# OpenAI-compatible endpoint, see MoonshotAI docs
|
|
42
|
+
return 'https://api.moonshot.ai/v1'
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def client(self) -> AsyncOpenAI:
|
|
46
|
+
return self._client
|
|
47
|
+
|
|
48
|
+
def model_profile(self, model_name: str) -> ModelProfile | None:
|
|
49
|
+
profile = moonshotai_model_profile(model_name)
|
|
50
|
+
|
|
51
|
+
# As the MoonshotAI API is OpenAI-compatible, let's assume we also need OpenAIJsonSchemaTransformer,
|
|
52
|
+
# unless json_schema_transformer is set explicitly.
|
|
53
|
+
# Also, MoonshotAI does not support strict tool definitions
|
|
54
|
+
# https://platform.moonshot.ai/docs/guide/migrating-from-openai-to-kimi#about-tool_choice
|
|
55
|
+
# "Please note that the current version of Kimi API does not support the tool_choice=required parameter."
|
|
56
|
+
return OpenAIModelProfile(
|
|
57
|
+
json_schema_transformer=OpenAIJsonSchemaTransformer,
|
|
58
|
+
openai_supports_tool_choice_required=False,
|
|
59
|
+
supports_json_object_output=True,
|
|
60
|
+
).update(profile)
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------
|
|
63
|
+
# Construction helpers
|
|
64
|
+
# ---------------------------------------------------------------------
|
|
65
|
+
@overload
|
|
66
|
+
def __init__(self) -> None: ...
|
|
67
|
+
|
|
68
|
+
@overload
|
|
69
|
+
def __init__(self, *, api_key: str) -> None: ...
|
|
70
|
+
|
|
71
|
+
@overload
|
|
72
|
+
def __init__(self, *, api_key: str, http_client: AsyncHTTPClient) -> None: ...
|
|
73
|
+
|
|
74
|
+
@overload
|
|
75
|
+
def __init__(self, *, openai_client: AsyncOpenAI | None = None) -> None: ...
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
*,
|
|
80
|
+
api_key: str | None = None,
|
|
81
|
+
openai_client: AsyncOpenAI | None = None,
|
|
82
|
+
http_client: AsyncHTTPClient | None = None,
|
|
83
|
+
) -> None:
|
|
84
|
+
api_key = api_key or os.getenv('MOONSHOTAI_API_KEY')
|
|
85
|
+
if not api_key and openai_client is None:
|
|
86
|
+
raise UserError(
|
|
87
|
+
'Set the `MOONSHOTAI_API_KEY` environment variable or pass it via '
|
|
88
|
+
'`MoonshotAIProvider(api_key=...)` to use the MoonshotAI provider.'
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if openai_client is not None:
|
|
92
|
+
self._client = openai_client
|
|
93
|
+
elif http_client is not None:
|
|
94
|
+
self._client = AsyncOpenAI(base_url=self.base_url, api_key=api_key, http_client=http_client)
|
|
95
|
+
else:
|
|
96
|
+
http_client = cached_async_http_client(provider='moonshotai')
|
|
97
|
+
self._client = AsyncOpenAI(base_url=self.base_url, api_key=api_key, http_client=http_client)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations as _annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import overload
|
|
5
|
+
|
|
6
|
+
from httpx import AsyncClient as AsyncHTTPClient
|
|
7
|
+
|
|
8
|
+
from pydantic_ai.exceptions import UserError
|
|
9
|
+
from pydantic_ai.models import cached_async_http_client
|
|
10
|
+
from pydantic_ai.profiles import ModelProfile
|
|
11
|
+
from pydantic_ai.profiles.amazon import amazon_model_profile
|
|
12
|
+
from pydantic_ai.profiles.anthropic import anthropic_model_profile
|
|
13
|
+
from pydantic_ai.profiles.cohere import cohere_model_profile
|
|
14
|
+
from pydantic_ai.profiles.deepseek import deepseek_model_profile
|
|
15
|
+
from pydantic_ai.profiles.google import google_model_profile
|
|
16
|
+
from pydantic_ai.profiles.grok import grok_model_profile
|
|
17
|
+
from pydantic_ai.profiles.mistral import mistral_model_profile
|
|
18
|
+
from pydantic_ai.profiles.openai import OpenAIJsonSchemaTransformer, OpenAIModelProfile, openai_model_profile
|
|
19
|
+
from pydantic_ai.providers import Provider
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
from openai import AsyncOpenAI
|
|
23
|
+
except ImportError as _import_error: # pragma: no cover
|
|
24
|
+
raise ImportError(
|
|
25
|
+
'Please install the `openai` package to use the Vercel provider, '
|
|
26
|
+
'you can use the `openai` optional group — `pip install "pydantic-ai-slim[openai]"`'
|
|
27
|
+
) from _import_error
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class VercelProvider(Provider[AsyncOpenAI]):
|
|
31
|
+
"""Provider for Vercel AI Gateway API."""
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def name(self) -> str:
|
|
35
|
+
return 'vercel'
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def base_url(self) -> str:
|
|
39
|
+
return 'https://ai-gateway.vercel.sh/v1'
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def client(self) -> AsyncOpenAI:
|
|
43
|
+
return self._client
|
|
44
|
+
|
|
45
|
+
def model_profile(self, model_name: str) -> ModelProfile | None:
|
|
46
|
+
provider_to_profile = {
|
|
47
|
+
'anthropic': anthropic_model_profile,
|
|
48
|
+
'bedrock': amazon_model_profile,
|
|
49
|
+
'cohere': cohere_model_profile,
|
|
50
|
+
'deepseek': deepseek_model_profile,
|
|
51
|
+
'mistral': mistral_model_profile,
|
|
52
|
+
'openai': openai_model_profile,
|
|
53
|
+
'vertex': google_model_profile,
|
|
54
|
+
'xai': grok_model_profile,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
profile = None
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
provider, model_name = model_name.split('/', 1)
|
|
61
|
+
except ValueError:
|
|
62
|
+
raise UserError(f"Model name must be in 'provider/model' format, got: {model_name!r}")
|
|
63
|
+
|
|
64
|
+
if provider in provider_to_profile:
|
|
65
|
+
profile = provider_to_profile[provider](model_name)
|
|
66
|
+
|
|
67
|
+
# As VercelProvider is always used with OpenAIModel, which used to unconditionally use OpenAIJsonSchemaTransformer,
|
|
68
|
+
# we need to maintain that behavior unless json_schema_transformer is set explicitly
|
|
69
|
+
return OpenAIModelProfile(
|
|
70
|
+
json_schema_transformer=OpenAIJsonSchemaTransformer,
|
|
71
|
+
).update(profile)
|
|
72
|
+
|
|
73
|
+
@overload
|
|
74
|
+
def __init__(self) -> None: ...
|
|
75
|
+
|
|
76
|
+
@overload
|
|
77
|
+
def __init__(self, *, api_key: str) -> None: ...
|
|
78
|
+
|
|
79
|
+
@overload
|
|
80
|
+
def __init__(self, *, api_key: str, http_client: AsyncHTTPClient) -> None: ...
|
|
81
|
+
|
|
82
|
+
@overload
|
|
83
|
+
def __init__(self, *, openai_client: AsyncOpenAI | None = None) -> None: ...
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
*,
|
|
88
|
+
api_key: str | None = None,
|
|
89
|
+
openai_client: AsyncOpenAI | None = None,
|
|
90
|
+
http_client: AsyncHTTPClient | None = None,
|
|
91
|
+
) -> None:
|
|
92
|
+
# Support Vercel AI Gateway's standard environment variables
|
|
93
|
+
api_key = api_key or os.getenv('VERCEL_AI_GATEWAY_API_KEY') or os.getenv('VERCEL_OIDC_TOKEN')
|
|
94
|
+
|
|
95
|
+
if not api_key and openai_client is None:
|
|
96
|
+
raise UserError(
|
|
97
|
+
'Set the `VERCEL_AI_GATEWAY_API_KEY` or `VERCEL_OIDC_TOKEN` environment variable '
|
|
98
|
+
'or pass the API key via `VercelProvider(api_key=...)` to use the Vercel provider.'
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if openai_client is not None:
|
|
102
|
+
self._client = openai_client
|
|
103
|
+
elif http_client is not None:
|
|
104
|
+
self._client = AsyncOpenAI(base_url=self.base_url, api_key=api_key, http_client=http_client)
|
|
105
|
+
else:
|
|
106
|
+
http_client = cached_async_http_client(provider='vercel')
|
|
107
|
+
self._client = AsyncOpenAI(base_url=self.base_url, api_key=api_key, http_client=http_client)
|
pydantic_ai/retries.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Retries utilities based on tenacity, especially for HTTP requests.
|
|
2
|
+
|
|
3
|
+
This module provides HTTP transport wrappers and wait strategies that integrate with
|
|
4
|
+
the tenacity library to add retry capabilities to HTTP requests. The transports can be
|
|
5
|
+
used with HTTP clients that support custom transports (such as httpx), while the wait
|
|
6
|
+
strategies can be used with any tenacity retry decorator.
|
|
7
|
+
|
|
8
|
+
The module includes:
|
|
9
|
+
- TenacityTransport: Synchronous HTTP transport with retry capabilities
|
|
10
|
+
- AsyncTenacityTransport: Asynchronous HTTP transport with retry capabilities
|
|
11
|
+
- wait_retry_after: Wait strategy that respects HTTP Retry-After headers
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from httpx import AsyncBaseTransport, AsyncHTTPTransport, BaseTransport, HTTPTransport, Request, Response
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
from tenacity import AsyncRetrying, Retrying
|
|
20
|
+
except ImportError as _import_error:
|
|
21
|
+
raise ImportError(
|
|
22
|
+
'Please install `tenacity` to use the retries utilities, '
|
|
23
|
+
'you can use the `retries` optional group — `pip install "pydantic-ai-slim[retries]"`'
|
|
24
|
+
) from _import_error
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
__all__ = ['TenacityTransport', 'AsyncTenacityTransport', 'wait_retry_after']
|
|
28
|
+
|
|
29
|
+
from datetime import datetime, timezone
|
|
30
|
+
from email.utils import parsedate_to_datetime
|
|
31
|
+
from typing import Callable, cast
|
|
32
|
+
|
|
33
|
+
from httpx import HTTPStatusError
|
|
34
|
+
from tenacity import RetryCallState, wait_exponential
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TenacityTransport(BaseTransport):
|
|
38
|
+
"""Synchronous HTTP transport with tenacity-based retry functionality.
|
|
39
|
+
|
|
40
|
+
This transport wraps another BaseTransport and adds retry capabilities using the tenacity library.
|
|
41
|
+
It can be configured to retry requests based on various conditions such as specific exception types,
|
|
42
|
+
response status codes, or custom validation logic.
|
|
43
|
+
|
|
44
|
+
The transport works by intercepting HTTP requests and responses, allowing the tenacity controller
|
|
45
|
+
to determine when and how to retry failed requests. The validate_response function can be used
|
|
46
|
+
to convert HTTP responses into exceptions that trigger retries.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
wrapped: The underlying transport to wrap and add retry functionality to.
|
|
50
|
+
controller: The tenacity Retrying instance that defines the retry behavior
|
|
51
|
+
(retry conditions, wait strategy, stop conditions, etc.).
|
|
52
|
+
validate_response: Optional callable that takes a Response and can raise an exception
|
|
53
|
+
to be handled by the controller if the response should trigger a retry.
|
|
54
|
+
Common use case is to raise exceptions for certain HTTP status codes.
|
|
55
|
+
If None, no response validation is performed.
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
```python
|
|
59
|
+
from httpx import Client, HTTPTransport, HTTPStatusError
|
|
60
|
+
from tenacity import Retrying, stop_after_attempt, retry_if_exception_type
|
|
61
|
+
from pydantic_ai.retries import TenacityTransport, wait_retry_after
|
|
62
|
+
|
|
63
|
+
transport = TenacityTransport(
|
|
64
|
+
HTTPTransport(),
|
|
65
|
+
Retrying(
|
|
66
|
+
retry=retry_if_exception_type(HTTPStatusError),
|
|
67
|
+
wait=wait_retry_after(max_wait=300),
|
|
68
|
+
stop=stop_after_attempt(5),
|
|
69
|
+
reraise=True
|
|
70
|
+
),
|
|
71
|
+
validate_response=lambda r: r.raise_for_status()
|
|
72
|
+
)
|
|
73
|
+
client = Client(transport=transport)
|
|
74
|
+
```
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
controller: Retrying,
|
|
80
|
+
wrapped: BaseTransport | None = None,
|
|
81
|
+
validate_response: Callable[[Response], None] | None = None,
|
|
82
|
+
):
|
|
83
|
+
self.controller = controller
|
|
84
|
+
self.wrapped = wrapped or HTTPTransport()
|
|
85
|
+
self.validate_response = validate_response
|
|
86
|
+
|
|
87
|
+
def handle_request(self, request: Request) -> Response:
|
|
88
|
+
"""Handle an HTTP request with retry logic.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
request: The HTTP request to handle.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
The HTTP response.
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
RuntimeError: If the retry controller did not make any attempts.
|
|
98
|
+
Exception: Any exception raised by the wrapped transport or validation function.
|
|
99
|
+
"""
|
|
100
|
+
for attempt in self.controller:
|
|
101
|
+
with attempt:
|
|
102
|
+
response = self.wrapped.handle_request(request)
|
|
103
|
+
if self.validate_response:
|
|
104
|
+
self.validate_response(response)
|
|
105
|
+
return response
|
|
106
|
+
raise RuntimeError('The retry controller did not make any attempts') # pragma: no cover
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class AsyncTenacityTransport(AsyncBaseTransport):
|
|
110
|
+
"""Asynchronous HTTP transport with tenacity-based retry functionality.
|
|
111
|
+
|
|
112
|
+
This transport wraps another AsyncBaseTransport and adds retry capabilities using the tenacity library.
|
|
113
|
+
It can be configured to retry requests based on various conditions such as specific exception types,
|
|
114
|
+
response status codes, or custom validation logic.
|
|
115
|
+
|
|
116
|
+
The transport works by intercepting HTTP requests and responses, allowing the tenacity controller
|
|
117
|
+
to determine when and how to retry failed requests. The validate_response function can be used
|
|
118
|
+
to convert HTTP responses into exceptions that trigger retries.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
wrapped: The underlying async transport to wrap and add retry functionality to.
|
|
122
|
+
controller: The tenacity AsyncRetrying instance that defines the retry behavior
|
|
123
|
+
(retry conditions, wait strategy, stop conditions, etc.).
|
|
124
|
+
validate_response: Optional callable that takes a Response and can raise an exception
|
|
125
|
+
to be handled by the controller if the response should trigger a retry.
|
|
126
|
+
Common use case is to raise exceptions for certain HTTP status codes.
|
|
127
|
+
If None, no response validation is performed.
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
```python
|
|
131
|
+
from httpx import AsyncClient, HTTPStatusError
|
|
132
|
+
from tenacity import AsyncRetrying, stop_after_attempt, retry_if_exception_type
|
|
133
|
+
from pydantic_ai.retries import AsyncTenacityTransport, wait_retry_after
|
|
134
|
+
|
|
135
|
+
transport = AsyncTenacityTransport(
|
|
136
|
+
AsyncRetrying(
|
|
137
|
+
retry=retry_if_exception_type(HTTPStatusError),
|
|
138
|
+
wait=wait_retry_after(max_wait=300),
|
|
139
|
+
stop=stop_after_attempt(5),
|
|
140
|
+
reraise=True
|
|
141
|
+
),
|
|
142
|
+
validate_response=lambda r: r.raise_for_status()
|
|
143
|
+
)
|
|
144
|
+
client = AsyncClient(transport=transport)
|
|
145
|
+
```
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def __init__(
|
|
149
|
+
self,
|
|
150
|
+
controller: AsyncRetrying,
|
|
151
|
+
wrapped: AsyncBaseTransport | None = None,
|
|
152
|
+
validate_response: Callable[[Response], None] | None = None,
|
|
153
|
+
):
|
|
154
|
+
self.controller = controller
|
|
155
|
+
self.wrapped = wrapped or AsyncHTTPTransport()
|
|
156
|
+
self.validate_response = validate_response
|
|
157
|
+
|
|
158
|
+
async def handle_async_request(self, request: Request) -> Response:
|
|
159
|
+
"""Handle an async HTTP request with retry logic.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
request: The HTTP request to handle.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
The HTTP response.
|
|
166
|
+
|
|
167
|
+
Raises:
|
|
168
|
+
RuntimeError: If the retry controller did not make any attempts.
|
|
169
|
+
Exception: Any exception raised by the wrapped transport or validation function.
|
|
170
|
+
"""
|
|
171
|
+
async for attempt in self.controller:
|
|
172
|
+
with attempt:
|
|
173
|
+
response = await self.wrapped.handle_async_request(request)
|
|
174
|
+
if self.validate_response:
|
|
175
|
+
self.validate_response(response)
|
|
176
|
+
return response
|
|
177
|
+
raise RuntimeError('The retry controller did not make any attempts') # pragma: no cover
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def wait_retry_after(
|
|
181
|
+
fallback_strategy: Callable[[RetryCallState], float] | None = None, max_wait: float = 300
|
|
182
|
+
) -> Callable[[RetryCallState], float]:
|
|
183
|
+
"""Create a tenacity-compatible wait strategy that respects HTTP Retry-After headers.
|
|
184
|
+
|
|
185
|
+
This wait strategy checks if the exception contains an HTTPStatusError with a
|
|
186
|
+
Retry-After header, and if so, waits for the time specified in the header.
|
|
187
|
+
If no header is present or parsing fails, it falls back to the provided strategy.
|
|
188
|
+
|
|
189
|
+
The Retry-After header can be in two formats:
|
|
190
|
+
- An integer representing seconds to wait
|
|
191
|
+
- An HTTP date string representing when to retry
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
fallback_strategy: Wait strategy to use when no Retry-After header is present
|
|
195
|
+
or parsing fails. Defaults to exponential backoff with max 60s.
|
|
196
|
+
max_wait: Maximum time to wait in seconds, regardless of header value.
|
|
197
|
+
Defaults to 300 (5 minutes).
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
A wait function that can be used with tenacity retry decorators.
|
|
201
|
+
|
|
202
|
+
Example:
|
|
203
|
+
```python
|
|
204
|
+
from httpx import AsyncClient, HTTPStatusError
|
|
205
|
+
from tenacity import AsyncRetrying, stop_after_attempt, retry_if_exception_type
|
|
206
|
+
from pydantic_ai.retries import AsyncTenacityTransport, wait_retry_after
|
|
207
|
+
|
|
208
|
+
transport = AsyncTenacityTransport(
|
|
209
|
+
AsyncRetrying(
|
|
210
|
+
retry=retry_if_exception_type(HTTPStatusError),
|
|
211
|
+
wait=wait_retry_after(max_wait=120),
|
|
212
|
+
stop=stop_after_attempt(5),
|
|
213
|
+
reraise=True
|
|
214
|
+
),
|
|
215
|
+
validate_response=lambda r: r.raise_for_status()
|
|
216
|
+
)
|
|
217
|
+
client = AsyncClient(transport=transport)
|
|
218
|
+
```
|
|
219
|
+
"""
|
|
220
|
+
if fallback_strategy is None:
|
|
221
|
+
fallback_strategy = wait_exponential(multiplier=1, max=60)
|
|
222
|
+
|
|
223
|
+
def wait_func(state: RetryCallState) -> float:
|
|
224
|
+
exc = state.outcome.exception() if state.outcome else None
|
|
225
|
+
if isinstance(exc, HTTPStatusError):
|
|
226
|
+
retry_after = exc.response.headers.get('retry-after')
|
|
227
|
+
if retry_after:
|
|
228
|
+
try:
|
|
229
|
+
# Try parsing as seconds first
|
|
230
|
+
wait_seconds = int(retry_after)
|
|
231
|
+
return min(float(wait_seconds), max_wait)
|
|
232
|
+
except ValueError:
|
|
233
|
+
# Try parsing as HTTP date
|
|
234
|
+
try:
|
|
235
|
+
retry_time = cast(datetime, parsedate_to_datetime(retry_after))
|
|
236
|
+
assert isinstance(retry_time, datetime)
|
|
237
|
+
now = datetime.now(timezone.utc)
|
|
238
|
+
wait_seconds = (retry_time - now).total_seconds()
|
|
239
|
+
|
|
240
|
+
if wait_seconds > 0:
|
|
241
|
+
return min(wait_seconds, max_wait)
|
|
242
|
+
except (ValueError, TypeError, AssertionError):
|
|
243
|
+
# If date parsing fails, fall back to fallback strategy
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
# Use fallback strategy
|
|
247
|
+
return fallback_strategy(state)
|
|
248
|
+
|
|
249
|
+
return wait_func
|
pydantic_ai/toolsets/combined.py
CHANGED
|
@@ -43,9 +43,10 @@ class CombinedToolset(AbstractToolset[AgentDepsT]):
|
|
|
43
43
|
async def __aenter__(self) -> Self:
|
|
44
44
|
async with self._enter_lock:
|
|
45
45
|
if self._entered_count == 0:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
async with AsyncExitStack() as exit_stack:
|
|
47
|
+
for toolset in self.toolsets:
|
|
48
|
+
await exit_stack.enter_async_context(toolset)
|
|
49
|
+
self._exit_stack = exit_stack.pop_all()
|
|
49
50
|
self._entered_count += 1
|
|
50
51
|
return self
|
|
51
52
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pydantic-ai-slim
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.8
|
|
4
4
|
Summary: Agent Framework / shim to use Pydantic with LLMs, slim package
|
|
5
5
|
Author-email: Samuel Colvin <samuel@pydantic.dev>, Marcelo Trylesinski <marcelotryle@gmail.com>, David Montague <david@pydantic.dev>, Alex Hall <alex@pydantic.dev>, Douwe Maan <douwe@pydantic.dev>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -30,7 +30,7 @@ Requires-Dist: exceptiongroup; python_version < '3.11'
|
|
|
30
30
|
Requires-Dist: griffe>=1.3.2
|
|
31
31
|
Requires-Dist: httpx>=0.27
|
|
32
32
|
Requires-Dist: opentelemetry-api>=1.28.0
|
|
33
|
-
Requires-Dist: pydantic-graph==0.4.
|
|
33
|
+
Requires-Dist: pydantic-graph==0.4.8
|
|
34
34
|
Requires-Dist: pydantic>=2.10
|
|
35
35
|
Requires-Dist: typing-inspection>=0.4.0
|
|
36
36
|
Provides-Extra: a2a
|
|
@@ -47,25 +47,28 @@ Requires-Dist: argcomplete>=3.5.0; extra == 'cli'
|
|
|
47
47
|
Requires-Dist: prompt-toolkit>=3; extra == 'cli'
|
|
48
48
|
Requires-Dist: rich>=13; extra == 'cli'
|
|
49
49
|
Provides-Extra: cohere
|
|
50
|
-
Requires-Dist: cohere>=5.
|
|
50
|
+
Requires-Dist: cohere>=5.16.0; (platform_system != 'Emscripten') and extra == 'cohere'
|
|
51
|
+
Requires-Dist: tokenizers<=0.21.2; extra == 'cohere'
|
|
51
52
|
Provides-Extra: duckduckgo
|
|
52
53
|
Requires-Dist: ddgs>=9.0.0; extra == 'duckduckgo'
|
|
53
54
|
Provides-Extra: evals
|
|
54
|
-
Requires-Dist: pydantic-evals==0.4.
|
|
55
|
+
Requires-Dist: pydantic-evals==0.4.8; extra == 'evals'
|
|
55
56
|
Provides-Extra: google
|
|
56
57
|
Requires-Dist: google-genai>=1.24.0; extra == 'google'
|
|
57
58
|
Provides-Extra: groq
|
|
58
59
|
Requires-Dist: groq>=0.19.0; extra == 'groq'
|
|
59
60
|
Provides-Extra: huggingface
|
|
60
|
-
Requires-Dist: huggingface-hub[inference]>=0.33.
|
|
61
|
+
Requires-Dist: huggingface-hub[inference]>=0.33.5; extra == 'huggingface'
|
|
61
62
|
Provides-Extra: logfire
|
|
62
63
|
Requires-Dist: logfire>=3.11.0; extra == 'logfire'
|
|
63
64
|
Provides-Extra: mcp
|
|
64
|
-
Requires-Dist: mcp>=1.
|
|
65
|
+
Requires-Dist: mcp>=1.10.0; (python_version >= '3.10') and extra == 'mcp'
|
|
65
66
|
Provides-Extra: mistral
|
|
66
67
|
Requires-Dist: mistralai>=1.9.2; extra == 'mistral'
|
|
67
68
|
Provides-Extra: openai
|
|
68
69
|
Requires-Dist: openai>=1.92.0; extra == 'openai'
|
|
70
|
+
Provides-Extra: retries
|
|
71
|
+
Requires-Dist: tenacity>=8.2.3; extra == 'retries'
|
|
69
72
|
Provides-Extra: tavily
|
|
70
73
|
Requires-Dist: tavily-python>=0.5.0; extra == 'tavily'
|
|
71
74
|
Provides-Extra: vertexai
|
|
@@ -7,23 +7,24 @@ pydantic_ai/_function_schema.py,sha256=6Xuash0DVpfPF0rWQce0bhtgti8YRyk3B1-OK_n6d
|
|
|
7
7
|
pydantic_ai/_griffe.py,sha256=Ugft16ZHw9CN_6-lW0Svn6jESK9zHXO_x4utkGBkbBI,5253
|
|
8
8
|
pydantic_ai/_mcp.py,sha256=PuvwnlLjv7YYOa9AZJCrklevBug99zGMhwJCBGG7BHQ,5626
|
|
9
9
|
pydantic_ai/_output.py,sha256=2k-nxfPNLJEb-wjnPhQo63lh-yQH1XsIhNG1hjsrim0,37462
|
|
10
|
-
pydantic_ai/_parts_manager.py,sha256=
|
|
10
|
+
pydantic_ai/_parts_manager.py,sha256=T4nlxaS697KeikJoqc1I9kRoIN5-_t5TEv-ovpMlzZg,17856
|
|
11
11
|
pydantic_ai/_run_context.py,sha256=pqb_HPXytE1Z9zZRRuBboRYes_tVTC75WGTpZgnb2Ko,1691
|
|
12
12
|
pydantic_ai/_system_prompt.py,sha256=lUSq-gDZjlYTGtd6BUm54yEvTIvgdwBmJ8mLsNZZtYU,1142
|
|
13
13
|
pydantic_ai/_thinking_part.py,sha256=mzx2RZSfiQxAKpljEflrcXRXmFKxtp6bKVyorY3UYZk,1554
|
|
14
14
|
pydantic_ai/_tool_manager.py,sha256=ptVj2oJm7Qm5MlDQHDNj8BPIEPY0HfkrzqeeD_ZuVbQ,8180
|
|
15
15
|
pydantic_ai/_utils.py,sha256=0Pte4mjir4YFZJTa6i-H6Cra9NbVwSKjOKegArzUggk,16283
|
|
16
|
-
pydantic_ai/ag_ui.py,sha256=
|
|
17
|
-
pydantic_ai/agent.py,sha256=
|
|
16
|
+
pydantic_ai/ag_ui.py,sha256=KW9B8ZrG2IgOGdsTcDbprSOGV3jIC-nWxf-gYBRHJzg,25411
|
|
17
|
+
pydantic_ai/agent.py,sha256=IaO8MQapAKwDTyMCdufLZM5dDsPnNBBiQ5JA-CCFHW8,106528
|
|
18
18
|
pydantic_ai/direct.py,sha256=WRfgke3zm-eeR39LTuh9XI2TrdHXAqO81eDvFwih4Ko,14803
|
|
19
19
|
pydantic_ai/exceptions.py,sha256=o0l6fBrWI5UhosICVZ2yaT-JEJF05eqBlKlQCW8i9UM,3462
|
|
20
20
|
pydantic_ai/format_as_xml.py,sha256=IINfh1evWDphGahqHNLBArB5dQ4NIqS3S-kru35ztGg,372
|
|
21
21
|
pydantic_ai/format_prompt.py,sha256=Or-Ytq55RQb1UJqy2HKIyPpZ-knWXfdDP3Z6tNc6Orw,4244
|
|
22
|
-
pydantic_ai/mcp.py,sha256=
|
|
23
|
-
pydantic_ai/messages.py,sha256=
|
|
22
|
+
pydantic_ai/mcp.py,sha256=v_f4CRzJ399uPC96aqTiEzpaYvuo6vIQyLIpXQBudsY,26271
|
|
23
|
+
pydantic_ai/messages.py,sha256=QutKuCXC6aglsbpyI5b3FTzCh7n7uVhJ47KwpEfCtpU,42548
|
|
24
24
|
pydantic_ai/output.py,sha256=54Cwd1RruXlA5hucZ1h-SxFrzKHJuLvYvLtH9iyg2GI,11988
|
|
25
25
|
pydantic_ai/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
26
|
pydantic_ai/result.py,sha256=6RYFUGMC_YXJ_v57DFc0swVFkYEmJxByBw5J3aWtXfw,25310
|
|
27
|
+
pydantic_ai/retries.py,sha256=Xkj-gZAd3wc12CVsIErVYx2EIdIwD5yJOL4Ou6jDQ2s,10498
|
|
27
28
|
pydantic_ai/settings.py,sha256=yuUZ7-GkdPB-Gbx71kSdh8dSr6gwM9gEwk84qNxPO_I,3552
|
|
28
29
|
pydantic_ai/tools.py,sha256=PQm1yWbocdrhyLdMf_J8dJMTRJTWzyS2BEF24t4jgqw,14205
|
|
29
30
|
pydantic_ai/usage.py,sha256=ceC9HHflyM_rkLBJqtaWPc-M8bEoq5rZF4XwGblPQoU,5830
|
|
@@ -33,21 +34,21 @@ pydantic_ai/common_tools/tavily.py,sha256=Q1xxSF5HtXAaZ10Pp-OaDOHXwJf2mco9wScGEQ
|
|
|
33
34
|
pydantic_ai/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
35
|
pydantic_ai/ext/aci.py,sha256=vUaNIj6pRM52x6RkPW_DohSYxJPm75pPUfOMw2i5Xx0,2515
|
|
35
36
|
pydantic_ai/ext/langchain.py,sha256=GemxfhpyG1JPxj69PbRiSJANnY8Q5s4hSB7wqt-uTbo,2266
|
|
36
|
-
pydantic_ai/models/__init__.py,sha256=
|
|
37
|
-
pydantic_ai/models/anthropic.py,sha256=
|
|
38
|
-
pydantic_ai/models/bedrock.py,sha256=
|
|
39
|
-
pydantic_ai/models/cohere.py,sha256=
|
|
37
|
+
pydantic_ai/models/__init__.py,sha256=GZ2YE5qcqI8tNpovlO0_6Ryx92bo8sQTsLmRKiYnSU4,30912
|
|
38
|
+
pydantic_ai/models/anthropic.py,sha256=dMPFqIeYCIhoeU_4uk9PmZYQWL1NbkSVmVrBKXplTiI,24167
|
|
39
|
+
pydantic_ai/models/bedrock.py,sha256=O6wKZDu4L18L1L2Nsa-XMW4ch073FjcLKRA5t_NXcHU,29511
|
|
40
|
+
pydantic_ai/models/cohere.py,sha256=lKUXEPqTMqxIJfouDj-Fr1bnfkrPu-JK3Xth7CL03kU,12800
|
|
40
41
|
pydantic_ai/models/fallback.py,sha256=URaV-dTQWkg99xrlkmknue5lXZWDcEt7cJ1Vsky4oB4,5130
|
|
41
|
-
pydantic_ai/models/function.py,sha256=
|
|
42
|
-
pydantic_ai/models/gemini.py,sha256=
|
|
43
|
-
pydantic_ai/models/google.py,sha256=
|
|
44
|
-
pydantic_ai/models/groq.py,sha256=
|
|
45
|
-
pydantic_ai/models/huggingface.py,sha256=
|
|
42
|
+
pydantic_ai/models/function.py,sha256=iHhG6GYN14XDo3_qbdliv_umY10B7-k11aoDoVF4xP8,13563
|
|
43
|
+
pydantic_ai/models/gemini.py,sha256=BMFEiDJXbB0DPj5DKK4kCwXuQHifT2WU-WuthJOqPsI,38138
|
|
44
|
+
pydantic_ai/models/google.py,sha256=NNcr2jJlK3eFlSRyaTgDPuSjG2GxOOj0vYDrAfD6rbo,24394
|
|
45
|
+
pydantic_ai/models/groq.py,sha256=8-sh8h2sJNZE6TiNUoiTKjmWghtCncxa3BX_xN979XQ,18854
|
|
46
|
+
pydantic_ai/models/huggingface.py,sha256=06Rh0Q-p_2LPuny5RIooMx-NWD1rbbhLWP2wL4E6aq0,19019
|
|
46
47
|
pydantic_ai/models/instrumented.py,sha256=aqvzspcGexn1Molbu6Mn4EEPRBSoQCCCS_yknJvJJ-8,16205
|
|
47
48
|
pydantic_ai/models/mcp_sampling.py,sha256=q9nnjNEAAbhrfRc_Qw5z9TtCHMG_SwlCWW9FvKWjh8k,3395
|
|
48
|
-
pydantic_ai/models/mistral.py,sha256=
|
|
49
|
-
pydantic_ai/models/openai.py,sha256=
|
|
50
|
-
pydantic_ai/models/test.py,sha256=
|
|
49
|
+
pydantic_ai/models/mistral.py,sha256=u3LcPVqvdI2WHckffbj7zRT5oQn9yYdTRbtEN20Gqpw,31427
|
|
50
|
+
pydantic_ai/models/openai.py,sha256=CJC5nU-b8HeKSa_4EbvTG_cmRjEnBjpoVbdfg_4ttyA,55834
|
|
51
|
+
pydantic_ai/models/test.py,sha256=tkm6K0-G5Mc_iSqVzVIpU9VLil9dfkE1-5az8GGWwTI,18457
|
|
51
52
|
pydantic_ai/models/wrapper.py,sha256=A5-ncYhPF8c9S_czGoXkd55s2KOQb65p3jbVpwZiFPA,2043
|
|
52
53
|
pydantic_ai/profiles/__init__.py,sha256=BXMqUpgRfosmYgcxjKAI9ESCj47JTSa30DhKXEgVLzM,2419
|
|
53
54
|
pydantic_ai/profiles/_json_schema.py,sha256=sTNHkaK0kbwmbldZp9JRGQNax0f5Qvwy0HkWuu_nGxU,7179
|
|
@@ -60,9 +61,9 @@ pydantic_ai/profiles/grok.py,sha256=nBOxOCYCK9aiLmz2Q-esqYhotNbbBC1boAoOYIk1tVw,
|
|
|
60
61
|
pydantic_ai/profiles/meta.py,sha256=IAGPoUrLWd-g9ajAgpWp9fIeOrP-7dBlZ2HEFjIhUbY,334
|
|
61
62
|
pydantic_ai/profiles/mistral.py,sha256=ll01PmcK3szwlTfbaJLQmfd0TADN8lqjov9HpPJzCMQ,217
|
|
62
63
|
pydantic_ai/profiles/moonshotai.py,sha256=LL5RacKHKn6rdvhoKjpGgZ8aVriv5NMeL6HCWEANAiU,223
|
|
63
|
-
pydantic_ai/profiles/openai.py,sha256=
|
|
64
|
+
pydantic_ai/profiles/openai.py,sha256=2s1DILf4fetSA5e0vUpB-KbNp-nCfSFqlJslbxXLCA8,7157
|
|
64
65
|
pydantic_ai/profiles/qwen.py,sha256=u7pL8uomoQTVl45g5wDrHx0P_oFDLaN6ALswuwmkWc0,334
|
|
65
|
-
pydantic_ai/providers/__init__.py,sha256=
|
|
66
|
+
pydantic_ai/providers/__init__.py,sha256=6Jm4ioGiI5jcwKUC2Yxv-GHdrK3ZTJmb-9_eHBZgfdw,4005
|
|
66
67
|
pydantic_ai/providers/anthropic.py,sha256=D35UXxCPXv8yIbD0fj9Zg2FvNyoMoJMeDUtVM8Sn78I,3046
|
|
67
68
|
pydantic_ai/providers/azure.py,sha256=y77IHGiSQ9Ttx9f4SGMgdpin2Daq6eYyzUdM9ET22RQ,5819
|
|
68
69
|
pydantic_ai/providers/bedrock.py,sha256=ycdTXnkj_WNqPMA7DNDPeYia0C37FP0_l0CygSQmWYI,5694
|
|
@@ -78,12 +79,14 @@ pydantic_ai/providers/groq.py,sha256=hqcR-RFHHHeemYP3K16IFeQTJKywNEU2wNZOSGTz6fE
|
|
|
78
79
|
pydantic_ai/providers/heroku.py,sha256=NmDIkAdxtWsvCjlX-bKI5FgI4HW1zO9-e0mrNQNGMCk,2990
|
|
79
80
|
pydantic_ai/providers/huggingface.py,sha256=LRmJcJpQRRYvam3IAPkYs2fMUJf70GgE3aDgQltGRCU,3821
|
|
80
81
|
pydantic_ai/providers/mistral.py,sha256=EIUSENjFuGzBhvbdrarUTM4VPkesIMnZrzfnEKHOsc4,3120
|
|
82
|
+
pydantic_ai/providers/moonshotai.py,sha256=3BAE9eC9QaD3kblVwxtQWEln0-PhgK7bRvrYTCEYXbY,3471
|
|
81
83
|
pydantic_ai/providers/openai.py,sha256=7iGij0EaFylab7dTZAZDgXr78tr-HsZrn9EI9AkWBNQ,3091
|
|
82
84
|
pydantic_ai/providers/openrouter.py,sha256=NXjNdnlXIBrBMMqbzcWQnowXOuZh4NHikXenBn5h3mc,4061
|
|
83
85
|
pydantic_ai/providers/together.py,sha256=zFVSMSm5jXbpkNouvBOTjWrPmlPpCp6sQS5LMSyVjrQ,3482
|
|
86
|
+
pydantic_ai/providers/vercel.py,sha256=JM1NmqLR64RvEmVWI8t2zGQdxOHJ-CguibOI0uSv-_Q,4061
|
|
84
87
|
pydantic_ai/toolsets/__init__.py,sha256=JCnqqAFeuHhmVW4XK0LM6Op_9B1cvsQUJ3vTmQ9Z5cQ,590
|
|
85
88
|
pydantic_ai/toolsets/abstract.py,sha256=LqunlpwUsoHVn4VMtdjEoLY5kiMUF74PZ4KW539K8tQ,6027
|
|
86
|
-
pydantic_ai/toolsets/combined.py,sha256=
|
|
89
|
+
pydantic_ai/toolsets/combined.py,sha256=UBSZ5quJAm4aITkI4enOTJ6sEDndkJ2sKvT1sTeX5LU,3440
|
|
87
90
|
pydantic_ai/toolsets/deferred.py,sha256=j_CO2KesBXFPyVYiIk3sO2bimJ1bssnShdaGx4UNuPQ,1444
|
|
88
91
|
pydantic_ai/toolsets/filtered.py,sha256=qmPeQDTZoWa_yyk6VXKHcpV9NFgdnLN48sBf7WItjBs,855
|
|
89
92
|
pydantic_ai/toolsets/function.py,sha256=Pgc8q6vh3qkBzI3xL0k-CQaETGG63zfy1PyWtqqzXwc,10186
|
|
@@ -91,8 +94,8 @@ pydantic_ai/toolsets/prefixed.py,sha256=MIStkzUdiU0rk2Y6P19IrTBxspH5pTstGxsqCBt-
|
|
|
91
94
|
pydantic_ai/toolsets/prepared.py,sha256=Zjfz6S8In6PBVxoKFN9sKPN984zO6t0awB7Lnq5KODw,1431
|
|
92
95
|
pydantic_ai/toolsets/renamed.py,sha256=JuLHpi-hYPiSPlaTpN8WiXLiGsywYK0axi2lW2Qs75k,1637
|
|
93
96
|
pydantic_ai/toolsets/wrapper.py,sha256=WjLoiM1WDuffSJ4mDS6pZrEZGHgZ421fjrqFcB66W94,1205
|
|
94
|
-
pydantic_ai_slim-0.4.
|
|
95
|
-
pydantic_ai_slim-0.4.
|
|
96
|
-
pydantic_ai_slim-0.4.
|
|
97
|
-
pydantic_ai_slim-0.4.
|
|
98
|
-
pydantic_ai_slim-0.4.
|
|
97
|
+
pydantic_ai_slim-0.4.8.dist-info/METADATA,sha256=nliQXFsj6CUu4vC4pLcAWNSvekw8aZMJEkSo5tNOLfw,4226
|
|
98
|
+
pydantic_ai_slim-0.4.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
99
|
+
pydantic_ai_slim-0.4.8.dist-info/entry_points.txt,sha256=kbKxe2VtDCYS06hsI7P3uZGxcVC08-FPt1rxeiMpIps,50
|
|
100
|
+
pydantic_ai_slim-0.4.8.dist-info/licenses/LICENSE,sha256=vA6Jc482lEyBBuGUfD1pYx-cM7jxvLYOxPidZ30t_PQ,1100
|
|
101
|
+
pydantic_ai_slim-0.4.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|