pytest-adk 0.0.1__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nikkie
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,42 @@
1
+ Metadata-Version: 2.3
2
+ Name: pytest-adk
3
+ Version: 0.0.1
4
+ Summary: Helpers for testing agents with Google's adk-python
5
+ Author: ftnext
6
+ Author-email: ftnext <takuyafjp+develop@gmail.com>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2026 nikkie
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+ Classifier: Development Status :: 1 - Planning
29
+ Classifier: Intended Audience :: Developers
30
+ Classifier: Programming Language :: Python
31
+ Classifier: Programming Language :: Python :: 3 :: Only
32
+ Classifier: Programming Language :: Python :: 3.10
33
+ Classifier: Programming Language :: Python :: 3.11
34
+ Classifier: Programming Language :: Python :: 3.12
35
+ Classifier: Programming Language :: Python :: 3.13
36
+ Classifier: Programming Language :: Python :: 3.14
37
+ Requires-Dist: google-adk>=1.30.0
38
+ Requires-Dist: pytest>=8 ; extra == 'dev'
39
+ Requires-Dist: pytest-asyncio>=0.24 ; extra == 'dev'
40
+ Requires-Python: >=3.10
41
+ Project-URL: Repository, https://github.com/ftnext/pytest-adk
42
+ Provides-Extra: dev
@@ -0,0 +1,41 @@
1
+ [project]
2
+ name = "pytest-adk"
3
+ version = "0.0.1"
4
+ description = "Helpers for testing agents with Google's adk-python"
5
+ authors = [
6
+ { name = "ftnext", email = "takuyafjp+develop@gmail.com" }
7
+ ]
8
+ license = { file = "LICENSE" }
9
+ requires-python = ">=3.10"
10
+ classifiers = [
11
+ "Development Status :: 1 - Planning",
12
+ "Intended Audience :: Developers",
13
+ "Programming Language :: Python",
14
+ "Programming Language :: Python :: 3 :: Only",
15
+ "Programming Language :: Python :: 3.10",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Programming Language :: Python :: 3.14",
20
+ ]
21
+ dependencies = [
22
+ "google-adk>=1.30.0",
23
+ ]
24
+
25
+ [project.optional-dependencies]
26
+ dev = [
27
+ "pytest>=8",
28
+ "pytest-asyncio>=0.24",
29
+ ]
30
+
31
+ [tool.pytest]
32
+ testpaths = ["tests"]
33
+ asyncio_mode = "auto"
34
+ asyncio_default_fixture_loop_scope = "function"
35
+
36
+ [build-system]
37
+ requires = ["uv_build>=0.11.2,<0.12.0"]
38
+ build-backend = "uv_build"
39
+
40
+ [project.urls]
41
+ Repository = "https://github.com/ftnext/pytest-adk"
@@ -0,0 +1,9 @@
1
+ # Copyright 2026 pytest-adk contributors
2
+
3
+ from .resume import load_session_from_json
4
+ from .resume import runner_from_exported_session
5
+
6
+ __all__ = [
7
+ 'load_session_from_json',
8
+ 'runner_from_exported_session',
9
+ ]
@@ -0,0 +1,126 @@
1
+ # Copyright 2026 pytest-adk contributors
2
+ """Helpers to resume an exported ADK session JSON with an in-memory Runner."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ from google.adk.agents.base_agent import BaseAgent
10
+ from google.adk.artifacts.base_artifact_service import BaseArtifactService
11
+ from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
12
+ from google.adk.auth.credential_service.base_credential_service import BaseCredentialService
13
+ from google.adk.auth.credential_service.in_memory_credential_service import InMemoryCredentialService
14
+ from google.adk.memory.base_memory_service import BaseMemoryService
15
+ from google.adk.memory.in_memory_memory_service import InMemoryMemoryService
16
+ from google.adk.runners import Runner
17
+ from google.adk.sessions.in_memory_session_service import InMemorySessionService
18
+ from google.adk.sessions.session import Session
19
+
20
+
21
+ def load_session_from_json(path_or_str: str | Path) -> Session:
22
+ """Load an exported session from a file path or raw JSON string.
23
+
24
+ Args:
25
+ path_or_str: Path to a ``.session.json``-style file, or the JSON text.
26
+ A ``str`` whose (stripped) first character is ``{`` or ``[`` is
27
+ treated as JSON content; otherwise it is treated as a file path and
28
+ must exist.
29
+
30
+ Returns:
31
+ Parsed :class:`google.adk.sessions.session.Session`.
32
+
33
+ Raises:
34
+ FileNotFoundError: If ``path_or_str`` refers to a path that does not
35
+ exist on disk.
36
+ """
37
+ if isinstance(path_or_str, Path):
38
+ if not path_or_str.is_file():
39
+ raise FileNotFoundError(path_or_str)
40
+ data = path_or_str.read_text(encoding='utf-8')
41
+ else:
42
+ stripped = path_or_str.lstrip()
43
+ # Avoid Path(...).is_file() on large JSON strings (long path errors).
44
+ if stripped.startswith('{') or stripped.startswith('['):
45
+ data = path_or_str
46
+ else:
47
+ candidate = Path(path_or_str)
48
+ try:
49
+ is_file = candidate.is_file()
50
+ except OSError as exc:
51
+ raise FileNotFoundError(path_or_str) from exc
52
+ if not is_file:
53
+ raise FileNotFoundError(path_or_str)
54
+ data = candidate.read_text(encoding='utf-8')
55
+
56
+ return Session.model_validate_json(data)
57
+
58
+
59
+ async def runner_from_exported_session(
60
+ agent: BaseAgent,
61
+ exported: Session | str | Path,
62
+ *,
63
+ app_name: Optional[str] = None,
64
+ user_id: Optional[str] = None,
65
+ session_id: Optional[str] = None,
66
+ artifact_service: Optional[BaseArtifactService] = None,
67
+ memory_service: Optional[BaseMemoryService] = None,
68
+ credential_service: Optional[BaseCredentialService] = None,
69
+ ) -> tuple[Runner, Session]:
70
+ """Build an in-memory :class:`Runner` whose session service holds the export.
71
+
72
+ This mirrors the ``saved_session_file`` flow in ``google.adk.cli``: create a
73
+ fresh session with copied ``state``, then :meth:`append_event` for each export
74
+ event. Call :meth:`Runner.run_async` with the same ``user_id`` and
75
+ ``session.id`` to continue the conversation.
76
+
77
+ Args:
78
+ agent: Root agent for the runner (must match the exported app logically).
79
+ exported: Parsed session, or a path / JSON string for
80
+ :func:`load_session_from_json`.
81
+ app_name: Override runner app name (default: from export).
82
+ user_id: Override user id (default: from export).
83
+ session_id: Override session id (default: from export; keeps ids aligned).
84
+ artifact_service: Optional; defaults to :class:`InMemoryArtifactService`.
85
+ memory_service: Optional; defaults to :class:`InMemoryMemoryService`.
86
+ credential_service: Optional; defaults to
87
+ :class:`InMemoryCredentialService`.
88
+
89
+ Returns:
90
+ ``(runner, session)`` where ``session`` is the restored in-memory session
91
+ (also reachable via ``runner.session_service.get_session``).
92
+ """
93
+ if not isinstance(exported, Session):
94
+ exported = load_session_from_json(exported)
95
+
96
+ resolved_app_name = app_name or exported.app_name
97
+ resolved_user_id = user_id or exported.user_id
98
+ resolved_session_id = session_id or exported.id
99
+
100
+ session_service = InMemorySessionService()
101
+ session = await session_service.create_session(
102
+ app_name=resolved_app_name,
103
+ user_id=resolved_user_id,
104
+ state=exported.state,
105
+ session_id=resolved_session_id,
106
+ )
107
+
108
+ for event in exported.events:
109
+ await session_service.append_event(session, event)
110
+
111
+ runner = Runner(
112
+ app_name=resolved_app_name,
113
+ agent=agent,
114
+ session_service=session_service,
115
+ artifact_service=artifact_service
116
+ if artifact_service is not None
117
+ else InMemoryArtifactService(),
118
+ memory_service=memory_service
119
+ if memory_service is not None
120
+ else InMemoryMemoryService(),
121
+ credential_service=credential_service
122
+ if credential_service is not None
123
+ else InMemoryCredentialService(),
124
+ )
125
+
126
+ return runner, session