pydantic-ai 0.0.39__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.

Files changed (95) hide show
  1. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/Makefile +5 -5
  2. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/PKG-INFO +3 -3
  3. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/pyproject.toml +3 -3
  4. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/conftest.py +19 -11
  5. pydantic_ai-0.0.40/tests/graph/test_file_persistence.py +204 -0
  6. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/graph/test_graph.py +209 -63
  7. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/graph/test_mermaid.py +22 -7
  8. pydantic_ai-0.0.40/tests/graph/test_persistence.py +347 -0
  9. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/graph/test_state.py +23 -12
  10. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/json_body_serializer.py +1 -1
  11. pydantic_ai-0.0.40/tests/models/cassettes/test_anthropic/test_document_binary_content_input.yaml +61 -0
  12. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/test_anthropic.py +14 -1
  13. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/test_fallback.py +4 -6
  14. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/test_model.py +1 -1
  15. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/test_vertexai.py +2 -2
  16. pydantic_ai-0.0.40/tests/providers/cassettes/test_azure/test_azure_provider_call.yaml +107 -0
  17. pydantic_ai-0.0.40/tests/providers/test_azure.py +72 -0
  18. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/test_examples.py +2 -0
  19. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/typed_graph.py +32 -2
  20. pydantic_ai-0.0.39/tests/graph/test_history.py +0 -147
  21. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/.gitignore +0 -0
  22. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/LICENSE +0 -0
  23. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/README.md +0 -0
  24. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/__init__.py +0 -0
  25. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/assets/dummy.pdf +0 -0
  26. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/assets/kiwi.png +0 -0
  27. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/assets/marcelo.mp3 +0 -0
  28. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/example_modules/README.md +0 -0
  29. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/example_modules/bank_database.py +0 -0
  30. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/example_modules/fake_database.py +0 -0
  31. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/example_modules/weather_service.py +0 -0
  32. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/graph/__init__.py +0 -0
  33. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/graph/test_utils.py +0 -0
  34. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/import_examples.py +0 -0
  35. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/__init__.py +0 -0
  36. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/cassettes/test_anthropic/test_document_url_input.yaml +0 -0
  37. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/cassettes/test_anthropic/test_image_url_input.yaml +0 -0
  38. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/cassettes/test_anthropic/test_image_url_input_invalid_mime_type.yaml +0 -0
  39. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/cassettes/test_anthropic/test_multiple_parallel_tool_calls.yaml +0 -0
  40. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/cassettes/test_anthropic/test_text_document_url_input.yaml +0 -0
  41. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/cassettes/test_bedrock/test_bedrock_model.yaml +0 -0
  42. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/cassettes/test_bedrock/test_bedrock_model_anthropic_model_without_tools.yaml +0 -0
  43. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/cassettes/test_bedrock/test_bedrock_model_iter_stream.yaml +0 -0
  44. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/cassettes/test_bedrock/test_bedrock_model_max_tokens.yaml +0 -0
  45. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/cassettes/test_bedrock/test_bedrock_model_retry.yaml +0 -0
  46. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/cassettes/test_bedrock/test_bedrock_model_stream.yaml +0 -0
  47. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/cassettes/test_bedrock/test_bedrock_model_structured_response.yaml +0 -0
  48. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/cassettes/test_bedrock/test_bedrock_model_top_p.yaml +0 -0
  49. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/cassettes/test_bedrock/test_document_url_input.yaml +0 -0
  50. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/cassettes/test_bedrock/test_image_as_binary_content_input.yaml +0 -0
  51. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/cassettes/test_bedrock/test_image_url_input.yaml +0 -0
  52. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/cassettes/test_bedrock/test_text_as_binary_content_input.yaml +0 -0
  53. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/cassettes/test_bedrock/test_text_document_url_input.yaml +0 -0
  54. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/cassettes/test_gemini/test_document_url_input.yaml +0 -0
  55. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/cassettes/test_gemini/test_image_as_binary_content_input.yaml +0 -0
  56. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/cassettes/test_gemini/test_image_url_input.yaml +0 -0
  57. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/cassettes/test_groq/test_image_as_binary_content_input.yaml +0 -0
  58. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/cassettes/test_groq/test_image_url_input.yaml +0 -0
  59. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/cassettes/test_openai/test_audio_as_binary_content_input.yaml +0 -0
  60. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/cassettes/test_openai/test_document_url_input.yaml +0 -0
  61. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/cassettes/test_openai/test_image_as_binary_content_input.yaml +0 -0
  62. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/cassettes/test_openai/test_openai_o1_mini_system_role[developer].yaml +0 -0
  63. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/cassettes/test_openai/test_openai_o1_mini_system_role[system].yaml +0 -0
  64. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/mock_async_stream.py +0 -0
  65. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/test_bedrock.py +0 -0
  66. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/test_cohere.py +0 -0
  67. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/test_gemini.py +0 -0
  68. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/test_groq.py +0 -0
  69. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/test_instrumented.py +0 -0
  70. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/test_mistral.py +0 -0
  71. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/test_model_function.py +0 -0
  72. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/test_model_names.py +0 -0
  73. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/test_model_test.py +0 -0
  74. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/models/test_openai.py +0 -0
  75. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/providers/__init__.py +0 -0
  76. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/providers/test_bedrock.py +0 -0
  77. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/providers/test_deepseek.py +0 -0
  78. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/providers/test_google_gla.py +0 -0
  79. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/providers/test_google_vertex.py +0 -0
  80. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/providers/test_groq.py +0 -0
  81. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/providers/test_provider_names.py +0 -0
  82. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/test_agent.py +0 -0
  83. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/test_cli.py +0 -0
  84. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/test_deps.py +0 -0
  85. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/test_format_as_xml.py +0 -0
  86. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/test_json_body_serializer.py +0 -0
  87. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/test_live.py +0 -0
  88. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/test_logfire.py +0 -0
  89. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/test_messages.py +0 -0
  90. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/test_parts_manager.py +0 -0
  91. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/test_streaming.py +0 -0
  92. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/test_tools.py +0 -0
  93. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/test_usage_limits.py +0 -0
  94. {pydantic_ai-0.0.39 → pydantic_ai-0.0.40}/tests/test_utils.py +0 -0
  95. {pydantic_ai-0.0.39 → 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.39
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.39
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.39; extra == 'examples'
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pydantic-ai"
7
- version = "0.0.39"
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",
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.39"]
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