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.
pytest_adk-0.0.1/LICENSE
ADDED
|
@@ -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,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
|