pydantic-ai 0.0.38__tar.gz → 0.0.40__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.
Potentially problematic release.
This version of pydantic-ai might be problematic. Click here for more details.
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/Makefile +5 -5
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/PKG-INFO +3 -8
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/README.md +0 -5
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/pyproject.toml +3 -3
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/conftest.py +19 -11
- pydantic_ai-0.0.40/tests/graph/test_file_persistence.py +204 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/graph/test_graph.py +209 -63
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/graph/test_mermaid.py +22 -7
- pydantic_ai-0.0.40/tests/graph/test_persistence.py +347 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/graph/test_state.py +23 -12
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/json_body_serializer.py +1 -1
- pydantic_ai-0.0.40/tests/models/cassettes/test_anthropic/test_document_binary_content_input.yaml +61 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/test_anthropic.py +14 -1
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/test_fallback.py +4 -6
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/test_groq.py +33 -16
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/test_model.py +1 -1
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/test_vertexai.py +2 -2
- pydantic_ai-0.0.40/tests/providers/cassettes/test_azure/test_azure_provider_call.yaml +107 -0
- pydantic_ai-0.0.40/tests/providers/test_azure.py +72 -0
- pydantic_ai-0.0.40/tests/providers/test_groq.py +66 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/providers/test_provider_names.py +2 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/test_examples.py +2 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/test_live.py +2 -1
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/typed_graph.py +32 -2
- pydantic_ai-0.0.38/tests/graph/test_history.py +0 -147
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/.gitignore +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/LICENSE +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/__init__.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/assets/dummy.pdf +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/assets/kiwi.png +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/assets/marcelo.mp3 +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/example_modules/README.md +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/example_modules/bank_database.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/example_modules/fake_database.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/example_modules/weather_service.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/graph/__init__.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/graph/test_utils.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/import_examples.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/__init__.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/cassettes/test_anthropic/test_document_url_input.yaml +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/cassettes/test_anthropic/test_image_url_input.yaml +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/cassettes/test_anthropic/test_image_url_input_invalid_mime_type.yaml +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/cassettes/test_anthropic/test_multiple_parallel_tool_calls.yaml +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/cassettes/test_anthropic/test_text_document_url_input.yaml +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/cassettes/test_bedrock/test_bedrock_model.yaml +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/cassettes/test_bedrock/test_bedrock_model_anthropic_model_without_tools.yaml +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/cassettes/test_bedrock/test_bedrock_model_iter_stream.yaml +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/cassettes/test_bedrock/test_bedrock_model_max_tokens.yaml +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/cassettes/test_bedrock/test_bedrock_model_retry.yaml +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/cassettes/test_bedrock/test_bedrock_model_stream.yaml +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/cassettes/test_bedrock/test_bedrock_model_structured_response.yaml +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/cassettes/test_bedrock/test_bedrock_model_top_p.yaml +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/cassettes/test_bedrock/test_document_url_input.yaml +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/cassettes/test_bedrock/test_image_as_binary_content_input.yaml +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/cassettes/test_bedrock/test_image_url_input.yaml +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/cassettes/test_bedrock/test_text_as_binary_content_input.yaml +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/cassettes/test_bedrock/test_text_document_url_input.yaml +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/cassettes/test_gemini/test_document_url_input.yaml +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/cassettes/test_gemini/test_image_as_binary_content_input.yaml +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/cassettes/test_gemini/test_image_url_input.yaml +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/cassettes/test_groq/test_image_as_binary_content_input.yaml +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/cassettes/test_groq/test_image_url_input.yaml +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/cassettes/test_openai/test_audio_as_binary_content_input.yaml +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/cassettes/test_openai/test_document_url_input.yaml +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/cassettes/test_openai/test_image_as_binary_content_input.yaml +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/cassettes/test_openai/test_openai_o1_mini_system_role[developer].yaml +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/cassettes/test_openai/test_openai_o1_mini_system_role[system].yaml +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/mock_async_stream.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/test_bedrock.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/test_cohere.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/test_gemini.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/test_instrumented.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/test_mistral.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/test_model_function.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/test_model_names.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/test_model_test.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/models/test_openai.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/providers/__init__.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/providers/test_bedrock.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/providers/test_deepseek.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/providers/test_google_gla.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/providers/test_google_vertex.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/test_agent.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/test_cli.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/test_deps.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/test_format_as_xml.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/test_json_body_serializer.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/test_logfire.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/test_messages.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/test_parts_manager.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/test_streaming.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/test_tools.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/test_usage_limits.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/test_utils.py +0 -0
- {pydantic_ai-0.0.38 → pydantic_ai-0.0.40}/tests/typed_agent.py +0 -0
|
@@ -49,11 +49,11 @@ test: ## Run tests and collect coverage data
|
|
|
49
49
|
|
|
50
50
|
.PHONY: test-all-python
|
|
51
51
|
test-all-python: ## Run tests on Python 3.9 to 3.13
|
|
52
|
-
UV_PROJECT_ENVIRONMENT=.venv39 uv run --python 3.9 --all-extras coverage run -p -m pytest
|
|
53
|
-
UV_PROJECT_ENVIRONMENT=.venv310 uv run --python 3.10 --all-extras coverage run -p -m pytest
|
|
54
|
-
UV_PROJECT_ENVIRONMENT=.venv311 uv run --python 3.11 --all-extras coverage run -p -m pytest
|
|
55
|
-
UV_PROJECT_ENVIRONMENT=.venv312 uv run --python 3.12 --all-extras coverage run -p -m pytest
|
|
56
|
-
UV_PROJECT_ENVIRONMENT=.venv313 uv run --python 3.13 --all-extras coverage run -p -m pytest
|
|
52
|
+
UV_PROJECT_ENVIRONMENT=.venv39 uv run --python 3.9 --all-extras --all-packages coverage run -p -m pytest
|
|
53
|
+
UV_PROJECT_ENVIRONMENT=.venv310 uv run --python 3.10 --all-extras --all-packages coverage run -p -m pytest
|
|
54
|
+
UV_PROJECT_ENVIRONMENT=.venv311 uv run --python 3.11 --all-extras --all-packages coverage run -p -m pytest
|
|
55
|
+
UV_PROJECT_ENVIRONMENT=.venv312 uv run --python 3.12 --all-extras --all-packages coverage run -p -m pytest
|
|
56
|
+
UV_PROJECT_ENVIRONMENT=.venv313 uv run --python 3.13 --all-extras --all-packages coverage run -p -m pytest
|
|
57
57
|
@uv run coverage combine
|
|
58
58
|
@uv run coverage report
|
|
59
59
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pydantic-ai
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.40
|
|
4
4
|
Summary: Agent Framework / shim to use Pydantic with LLMs
|
|
5
5
|
Project-URL: Homepage, https://ai.pydantic.dev
|
|
6
6
|
Project-URL: Source, https://github.com/pydantic/pydantic-ai
|
|
@@ -28,9 +28,9 @@ Classifier: Topic :: Internet
|
|
|
28
28
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
29
29
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
30
30
|
Requires-Python: >=3.9
|
|
31
|
-
Requires-Dist: pydantic-ai-slim[anthropic,bedrock,cli,cohere,groq,mistral,openai,vertexai]==0.0.
|
|
31
|
+
Requires-Dist: pydantic-ai-slim[anthropic,bedrock,cli,cohere,groq,mistral,openai,vertexai]==0.0.40
|
|
32
32
|
Provides-Extra: examples
|
|
33
|
-
Requires-Dist: pydantic-ai-examples==0.0.
|
|
33
|
+
Requires-Dist: pydantic-ai-examples==0.0.40; extra == 'examples'
|
|
34
34
|
Provides-Extra: logfire
|
|
35
35
|
Requires-Dist: logfire>=2.3; extra == 'logfire'
|
|
36
36
|
Description-Content-Type: text/markdown
|
|
@@ -98,11 +98,6 @@ Provides the ability to [stream](https://ai.pydantic.dev/results/#streamed-resul
|
|
|
98
98
|
* __Graph Support__
|
|
99
99
|
[Pydantic Graph](https://ai.pydantic.dev/graph) provides a powerful way to define graphs using typing hints, this is useful in complex applications where standard control flow can degrade to spaghetti code.
|
|
100
100
|
|
|
101
|
-
## In Beta!
|
|
102
|
-
|
|
103
|
-
PydanticAI is in early beta, the API is still subject to change and there's a lot more to do.
|
|
104
|
-
[Feedback](https://github.com/pydantic/pydantic-ai/issues) is very welcome!
|
|
105
|
-
|
|
106
101
|
## Hello World Example
|
|
107
102
|
|
|
108
103
|
Here's a minimal example of PydanticAI:
|
|
@@ -61,11 +61,6 @@ Provides the ability to [stream](https://ai.pydantic.dev/results/#streamed-resul
|
|
|
61
61
|
* __Graph Support__
|
|
62
62
|
[Pydantic Graph](https://ai.pydantic.dev/graph) provides a powerful way to define graphs using typing hints, this is useful in complex applications where standard control flow can degrade to spaghetti code.
|
|
63
63
|
|
|
64
|
-
## In Beta!
|
|
65
|
-
|
|
66
|
-
PydanticAI is in early beta, the API is still subject to change and there's a lot more to do.
|
|
67
|
-
[Feedback](https://github.com/pydantic/pydantic-ai/issues) is very welcome!
|
|
68
|
-
|
|
69
64
|
## Hello World Example
|
|
70
65
|
|
|
71
66
|
Here's a minimal example of PydanticAI:
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pydantic-ai"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.40"
|
|
8
8
|
description = "Agent Framework / shim to use Pydantic with LLMs"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Samuel Colvin", email = "samuel@pydantic.dev" },
|
|
@@ -36,7 +36,7 @@ classifiers = [
|
|
|
36
36
|
]
|
|
37
37
|
requires-python = ">=3.9"
|
|
38
38
|
dependencies = [
|
|
39
|
-
"pydantic-ai-slim[openai,vertexai,groq,anthropic,mistral,cohere,bedrock,cli]==0.0.
|
|
39
|
+
"pydantic-ai-slim[openai,vertexai,groq,anthropic,mistral,cohere,bedrock,cli]==0.0.40",
|
|
40
40
|
]
|
|
41
41
|
|
|
42
42
|
[project.urls]
|
|
@@ -46,7 +46,7 @@ Documentation = "https://ai.pydantic.dev"
|
|
|
46
46
|
Changelog = "https://github.com/pydantic/pydantic-ai/releases"
|
|
47
47
|
|
|
48
48
|
[project.optional-dependencies]
|
|
49
|
-
examples = ["pydantic-ai-examples==0.0.
|
|
49
|
+
examples = ["pydantic-ai-examples==0.0.40"]
|
|
50
50
|
logfire = ["logfire>=2.3"]
|
|
51
51
|
|
|
52
52
|
[tool.uv.sources]
|
|
@@ -16,6 +16,7 @@ from typing import TYPE_CHECKING, Any, Callable
|
|
|
16
16
|
import httpx
|
|
17
17
|
import pytest
|
|
18
18
|
from _pytest.assertion.rewrite import AssertionRewritingHook
|
|
19
|
+
from pytest_mock import MockerFixture
|
|
19
20
|
from typing_extensions import TypeAlias
|
|
20
21
|
from vcr import VCR
|
|
21
22
|
|
|
@@ -44,17 +45,6 @@ else:
|
|
|
44
45
|
return _IsNow(*args, **kwargs)
|
|
45
46
|
|
|
46
47
|
|
|
47
|
-
try:
|
|
48
|
-
from logfire.testing import CaptureLogfire
|
|
49
|
-
except ImportError:
|
|
50
|
-
pass
|
|
51
|
-
else:
|
|
52
|
-
|
|
53
|
-
@pytest.fixture(autouse=True)
|
|
54
|
-
def logfire_disable(capfire: CaptureLogfire):
|
|
55
|
-
pass
|
|
56
|
-
|
|
57
|
-
|
|
58
48
|
class TestEnv:
|
|
59
49
|
__test__ = False
|
|
60
50
|
|
|
@@ -230,6 +220,12 @@ def image_content(assets_path: Path) -> BinaryContent:
|
|
|
230
220
|
return BinaryContent(data=image_bytes, media_type='image/png')
|
|
231
221
|
|
|
232
222
|
|
|
223
|
+
@pytest.fixture(scope='session')
|
|
224
|
+
def document_content(assets_path: Path) -> BinaryContent:
|
|
225
|
+
pdf_bytes = assets_path.joinpath('dummy.pdf').read_bytes()
|
|
226
|
+
return BinaryContent(data=pdf_bytes, media_type='application/pdf')
|
|
227
|
+
|
|
228
|
+
|
|
233
229
|
@pytest.fixture(scope='session')
|
|
234
230
|
def openai_api_key() -> str:
|
|
235
231
|
return os.getenv('OPENAI_API_KEY', 'mock-api-key')
|
|
@@ -248,3 +244,15 @@ def groq_api_key() -> str:
|
|
|
248
244
|
@pytest.fixture(scope='session')
|
|
249
245
|
def anthropic_api_key() -> str:
|
|
250
246
|
return os.getenv('ANTHROPIC_API_KEY', 'mock-api-key')
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@pytest.fixture
|
|
250
|
+
def mock_snapshot_id(mocker: MockerFixture):
|
|
251
|
+
i = 0
|
|
252
|
+
|
|
253
|
+
def generate_snapshot_id(node_id: str) -> str:
|
|
254
|
+
nonlocal i
|
|
255
|
+
i += 1
|
|
256
|
+
return f'{node_id}:{i}'
|
|
257
|
+
|
|
258
|
+
return mocker.patch('pydantic_graph.nodes.generate_snapshot_id', side_effect=generate_snapshot_id)
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
from __future__ import annotations as _annotations
|
|
2
|
+
|
|
3
|
+
from asyncio.exceptions import TimeoutError
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Union
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
from inline_snapshot import snapshot
|
|
11
|
+
|
|
12
|
+
from pydantic_graph import (
|
|
13
|
+
BaseNode,
|
|
14
|
+
End,
|
|
15
|
+
EndSnapshot,
|
|
16
|
+
Graph,
|
|
17
|
+
GraphRunContext,
|
|
18
|
+
NodeSnapshot,
|
|
19
|
+
)
|
|
20
|
+
from pydantic_graph.persistence.file import FileStatePersistence
|
|
21
|
+
|
|
22
|
+
from ..conftest import IsFloat, IsNow
|
|
23
|
+
|
|
24
|
+
pytestmark = pytest.mark.anyio
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class Float2String(BaseNode):
|
|
29
|
+
input_data: float
|
|
30
|
+
|
|
31
|
+
async def run(self, ctx: GraphRunContext) -> String2Length:
|
|
32
|
+
return String2Length(str(self.input_data))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class String2Length(BaseNode):
|
|
37
|
+
input_data: str
|
|
38
|
+
|
|
39
|
+
async def run(self, ctx: GraphRunContext) -> Double:
|
|
40
|
+
return Double(len(self.input_data))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class Double(BaseNode[None, None, int]):
|
|
45
|
+
input_data: int
|
|
46
|
+
|
|
47
|
+
async def run(self, ctx: GraphRunContext) -> Union[String2Length, End[int]]: # noqa: UP007
|
|
48
|
+
if self.input_data == 7: # pragma: no cover
|
|
49
|
+
return String2Length('x' * 21)
|
|
50
|
+
else:
|
|
51
|
+
return End(self.input_data * 2)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def test_run(tmp_path: Path, mock_snapshot_id: object):
|
|
55
|
+
my_graph = Graph(nodes=(Float2String, String2Length, Double))
|
|
56
|
+
p = tmp_path / 'test_graph.json'
|
|
57
|
+
persistence = FileStatePersistence(p)
|
|
58
|
+
result = await my_graph.run(Float2String(3.14), persistence=persistence)
|
|
59
|
+
# len('3.14') * 2 == 8
|
|
60
|
+
assert result.output == 8
|
|
61
|
+
assert my_graph.name == 'my_graph'
|
|
62
|
+
assert await persistence.load_all() == snapshot(
|
|
63
|
+
[
|
|
64
|
+
NodeSnapshot(
|
|
65
|
+
state=None,
|
|
66
|
+
node=Float2String(input_data=3.14),
|
|
67
|
+
start_ts=IsNow(tz=timezone.utc),
|
|
68
|
+
duration=IsFloat(),
|
|
69
|
+
status='success',
|
|
70
|
+
id='Float2String:1',
|
|
71
|
+
),
|
|
72
|
+
NodeSnapshot(
|
|
73
|
+
state=None,
|
|
74
|
+
node=String2Length(input_data='3.14'),
|
|
75
|
+
start_ts=IsNow(tz=timezone.utc),
|
|
76
|
+
duration=IsFloat(),
|
|
77
|
+
status='success',
|
|
78
|
+
id='String2Length:2',
|
|
79
|
+
),
|
|
80
|
+
NodeSnapshot(
|
|
81
|
+
state=None,
|
|
82
|
+
node=Double(input_data=4),
|
|
83
|
+
start_ts=IsNow(tz=timezone.utc),
|
|
84
|
+
duration=IsFloat(),
|
|
85
|
+
status='success',
|
|
86
|
+
id='Double:3',
|
|
87
|
+
),
|
|
88
|
+
EndSnapshot(state=None, result=End(data=8), ts=IsNow(tz=timezone.utc), id='end:4'),
|
|
89
|
+
]
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def test_next_from_persistence(tmp_path: Path, mock_snapshot_id: object):
|
|
94
|
+
my_graph = Graph(nodes=(Float2String, String2Length, Double))
|
|
95
|
+
p = tmp_path / 'test_graph.json'
|
|
96
|
+
persistence = FileStatePersistence(p)
|
|
97
|
+
|
|
98
|
+
async with my_graph.iter(Float2String(3.14), persistence=persistence) as run:
|
|
99
|
+
node = await run.next()
|
|
100
|
+
assert node == snapshot(String2Length(input_data='3.14'))
|
|
101
|
+
assert node.get_snapshot_id() == snapshot('String2Length:2')
|
|
102
|
+
assert my_graph.name == 'my_graph'
|
|
103
|
+
|
|
104
|
+
async with my_graph.iter_from_persistence(persistence) as run:
|
|
105
|
+
node = await run.next()
|
|
106
|
+
assert node == snapshot(Double(input_data=4))
|
|
107
|
+
assert node.get_snapshot_id() == snapshot('Double:3')
|
|
108
|
+
|
|
109
|
+
node = await run.next()
|
|
110
|
+
assert node == snapshot(End(data=8))
|
|
111
|
+
assert node.get_snapshot_id() == snapshot('end:4')
|
|
112
|
+
|
|
113
|
+
assert await persistence.load_all() == snapshot(
|
|
114
|
+
[
|
|
115
|
+
NodeSnapshot(
|
|
116
|
+
state=None,
|
|
117
|
+
node=Float2String(input_data=3.14),
|
|
118
|
+
start_ts=IsNow(tz=timezone.utc),
|
|
119
|
+
duration=IsFloat(),
|
|
120
|
+
status='success',
|
|
121
|
+
id='Float2String:1',
|
|
122
|
+
),
|
|
123
|
+
NodeSnapshot(
|
|
124
|
+
state=None,
|
|
125
|
+
node=String2Length(input_data='3.14'),
|
|
126
|
+
start_ts=IsNow(tz=timezone.utc),
|
|
127
|
+
duration=IsFloat(),
|
|
128
|
+
status='success',
|
|
129
|
+
id='String2Length:2',
|
|
130
|
+
),
|
|
131
|
+
NodeSnapshot(
|
|
132
|
+
state=None,
|
|
133
|
+
node=Double(input_data=4),
|
|
134
|
+
start_ts=IsNow(tz=timezone.utc),
|
|
135
|
+
duration=IsFloat(),
|
|
136
|
+
status='success',
|
|
137
|
+
id='Double:3',
|
|
138
|
+
),
|
|
139
|
+
EndSnapshot(state=None, result=End(data=8), ts=IsNow(tz=timezone.utc), id='end:4'),
|
|
140
|
+
]
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
async def test_node_error(tmp_path: Path, mock_snapshot_id: object):
|
|
145
|
+
@dataclass
|
|
146
|
+
class Foo(BaseNode):
|
|
147
|
+
async def run(self, ctx: GraphRunContext) -> Bar:
|
|
148
|
+
return Bar()
|
|
149
|
+
|
|
150
|
+
@dataclass
|
|
151
|
+
class Bar(BaseNode[None, None, None]):
|
|
152
|
+
async def run(self, ctx: GraphRunContext) -> End[None]:
|
|
153
|
+
raise RuntimeError('test error')
|
|
154
|
+
|
|
155
|
+
g = Graph(nodes=(Foo, Bar))
|
|
156
|
+
p = tmp_path / 'test_graph.json'
|
|
157
|
+
persistence = FileStatePersistence(p)
|
|
158
|
+
with pytest.raises(RuntimeError, match='test error'):
|
|
159
|
+
await g.run(Foo(), persistence=persistence)
|
|
160
|
+
|
|
161
|
+
assert await persistence.load_all() == snapshot(
|
|
162
|
+
[
|
|
163
|
+
NodeSnapshot(
|
|
164
|
+
state=None,
|
|
165
|
+
node=Foo(),
|
|
166
|
+
start_ts=IsNow(tz=timezone.utc),
|
|
167
|
+
duration=IsFloat(),
|
|
168
|
+
status='success',
|
|
169
|
+
id='Foo:1',
|
|
170
|
+
),
|
|
171
|
+
NodeSnapshot(
|
|
172
|
+
state=None,
|
|
173
|
+
node=Bar(),
|
|
174
|
+
start_ts=IsNow(tz=timezone.utc),
|
|
175
|
+
duration=IsFloat(),
|
|
176
|
+
status='error',
|
|
177
|
+
id='Bar:2',
|
|
178
|
+
),
|
|
179
|
+
]
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
async def test_lock_timeout(tmp_path: Path):
|
|
184
|
+
p = tmp_path / 'test_graph.json'
|
|
185
|
+
persistence = FileStatePersistence(p)
|
|
186
|
+
async with persistence._lock(): # type: ignore[reportPrivateUsage]
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
async with persistence._lock(): # type: ignore[reportPrivateUsage]
|
|
190
|
+
with pytest.raises(TimeoutError):
|
|
191
|
+
async with persistence._lock(timeout=0.1): # type: ignore[reportPrivateUsage]
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
async def test_record_lookup_error(tmp_path: Path):
|
|
196
|
+
p = tmp_path / 'test_graph.json'
|
|
197
|
+
persistence = FileStatePersistence(p)
|
|
198
|
+
my_graph = Graph(nodes=(Float2String, String2Length, Double))
|
|
199
|
+
persistence.set_graph_types(my_graph)
|
|
200
|
+
persistence.set_graph_types(my_graph)
|
|
201
|
+
|
|
202
|
+
with pytest.raises(LookupError, match="No snapshot found with id='foobar'"):
|
|
203
|
+
async with persistence.record_run('foobar'):
|
|
204
|
+
pass
|