Skip to content

Commit 5534d28

Browse files
committed
cp dines
1 parent cee8ae7 commit 5534d28

10 files changed

Lines changed: 275 additions & 9 deletions

File tree

src/runloop_api_client/sdk/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from __future__ import annotations
77

8-
from .axon import Axon
8+
from .axon import Axon, AxonSqlOps
99
from .sync import (
1010
AxonOps,
1111
AgentOps,
@@ -48,7 +48,7 @@
4848
from .benchmark import Benchmark
4949
from .blueprint import Blueprint
5050
from .execution import Execution
51-
from .async_axon import AsyncAxon
51+
from .async_axon import AsyncAxon, AsyncAxonSqlOps
5252
from .mcp_config import McpConfig
5353
from .async_agent import AsyncAgent
5454
from .async_devbox import AsyncDevbox, AsyncNamedShell
@@ -111,6 +111,8 @@
111111
"AsyncAgent",
112112
"Axon",
113113
"AsyncAxon",
114+
"AxonSqlOps",
115+
"AsyncAxonSqlOps",
114116
"AsyncSecret",
115117
"Benchmark",
116118
"AsyncBenchmark",

src/runloop_api_client/sdk/_types.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,19 @@ class SDKAxonPublishParams(AxonPublishParams, LongRequestOptions):
196196
pass
197197

198198

199+
class SDKAxonSqlQueryParams(LongRequestOptions, total=False):
200+
sql: str
201+
"""SQL query with ?-style positional placeholders."""
202+
203+
params: list[object]
204+
"""Positional parameter bindings for ? placeholders."""
205+
206+
207+
class SDKAxonSqlBatchParams(LongRequestOptions, total=False):
208+
statements: list[dict[str, object]]
209+
"""The SQL statements to execute atomically within a transaction."""
210+
211+
199212
class SDKScenarioListParams(ScenarioListParams, BaseRequestOptions):
200213
pass
201214

src/runloop_api_client/sdk/async_axon.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,48 @@
77
from ._types import (
88
BaseRequestOptions,
99
SDKAxonPublishParams,
10+
SDKAxonSqlQueryParams,
11+
SDKAxonSqlBatchParams,
1012
)
1113
from .._client import AsyncRunloop
1214
from .._streaming import AsyncStream
1315
from ..types.axon_view import AxonView
1416
from ..types.axon_event_view import AxonEventView
1517
from ..types.publish_result_view import PublishResultView
18+
from ..types.axons.sql_query_result_view import SqlQueryResultView
19+
from ..types.axons.sql_batch_result_view import SqlBatchResultView
20+
21+
22+
class AsyncAxonSqlOps:
23+
"""[Beta] Async SQL operations for an axon's SQLite database.
24+
25+
Access via ``axon.sql``.
26+
27+
Example:
28+
>>> axon = await runloop.axon.create()
29+
>>> await axon.sql.query(sql="CREATE TABLE tasks (id INTEGER PRIMARY KEY, name TEXT)")
30+
>>> result = await axon.sql.query(sql="SELECT * FROM tasks WHERE id = ?", params=[1])
31+
"""
32+
33+
def __init__(self, client: AsyncRunloop, axon_id: str) -> None:
34+
self._client = client
35+
self._axon_id = axon_id
36+
37+
async def query(self, **params: Unpack[SDKAxonSqlQueryParams]) -> SqlQueryResultView:
38+
"""[Beta] Execute a single parameterized SQL statement against this axon's SQLite database."""
39+
return await self._client.axons.sql.query(self._axon_id, **params)
40+
41+
async def batch(self, **params: Unpack[SDKAxonSqlBatchParams]) -> SqlBatchResultView:
42+
"""[Beta] Execute multiple SQL statements atomically within a single transaction."""
43+
return await self._client.axons.sql.batch(self._axon_id, **params)
1644

1745

1846
class AsyncAxon:
1947
"""[Beta] Wrapper around asynchronous axon operations.
2048
21-
Axons are event communication channels that support publishing events
22-
and subscribing to event streams via server-sent events (SSE).
49+
Axons are event communication channels that support publishing events,
50+
subscribing to event streams via server-sent events (SSE), and executing
51+
SQL queries against an embedded SQLite database.
2352
Obtain instances via ``runloop.axon.create()`` or ``runloop.axon.from_id()``.
2453
2554
Example:
@@ -29,11 +58,13 @@ class AsyncAxon:
2958
>>> async with await axon.subscribe_sse() as stream:
3059
... async for event in stream:
3160
... print(event.event_type, event.payload)
61+
>>> await axon.sql.query(sql="CREATE TABLE tasks (id INTEGER PRIMARY KEY, name TEXT)")
3262
"""
3363

3464
def __init__(self, client: AsyncRunloop, axon_id: str) -> None:
3565
self._client = client
3666
self._id = axon_id
67+
self.sql = AsyncAxonSqlOps(client, axon_id)
3768

3869
@override
3970
def __repr__(self) -> str:

src/runloop_api_client/sdk/axon.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,48 @@
77
from ._types import (
88
BaseRequestOptions,
99
SDKAxonPublishParams,
10+
SDKAxonSqlQueryParams,
11+
SDKAxonSqlBatchParams,
1012
)
1113
from .._client import Runloop
1214
from .._streaming import Stream
1315
from ..types.axon_view import AxonView
1416
from ..types.axon_event_view import AxonEventView
1517
from ..types.publish_result_view import PublishResultView
18+
from ..types.axons.sql_query_result_view import SqlQueryResultView
19+
from ..types.axons.sql_batch_result_view import SqlBatchResultView
20+
21+
22+
class AxonSqlOps:
23+
"""[Beta] SQL operations for an axon's SQLite database.
24+
25+
Access via ``axon.sql``.
26+
27+
Example:
28+
>>> axon = runloop.axon.create()
29+
>>> axon.sql.query(sql="CREATE TABLE tasks (id INTEGER PRIMARY KEY, name TEXT)")
30+
>>> result = axon.sql.query(sql="SELECT * FROM tasks WHERE id = ?", params=[1])
31+
"""
32+
33+
def __init__(self, client: Runloop, axon_id: str) -> None:
34+
self._client = client
35+
self._axon_id = axon_id
36+
37+
def query(self, **params: Unpack[SDKAxonSqlQueryParams]) -> SqlQueryResultView:
38+
"""[Beta] Execute a single parameterized SQL statement against this axon's SQLite database."""
39+
return self._client.axons.sql.query(self._axon_id, **params)
40+
41+
def batch(self, **params: Unpack[SDKAxonSqlBatchParams]) -> SqlBatchResultView:
42+
"""[Beta] Execute multiple SQL statements atomically within a single transaction."""
43+
return self._client.axons.sql.batch(self._axon_id, **params)
1644

1745

1846
class Axon:
1947
"""[Beta] Wrapper around synchronous axon operations.
2048
21-
Axons are event communication channels that support publishing events
22-
and subscribing to event streams via server-sent events (SSE).
49+
Axons are event communication channels that support publishing events,
50+
subscribing to event streams via server-sent events (SSE), and executing
51+
SQL queries against an embedded SQLite database.
2352
Obtain instances via ``runloop.axon.create()`` or ``runloop.axon.from_id()``.
2453
2554
Example:
@@ -29,11 +58,13 @@ class Axon:
2958
>>> with axon.subscribe_sse() as stream:
3059
... for event in stream:
3160
... print(event.event_type, event.payload)
61+
>>> axon.sql.query(sql="CREATE TABLE tasks (id INTEGER PRIMARY KEY, name TEXT)")
3262
"""
3363

3464
def __init__(self, client: Runloop, axon_id: str) -> None:
3565
self._client = client
3666
self._id = axon_id
67+
self.sql = AxonSqlOps(client, axon_id)
3768

3869
@override
3970
def __repr__(self) -> str:

tests/sdk/conftest.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,47 @@ class MockPublishResultView:
133133
timestamp_ms: int = 1234567890000
134134

135135

136+
@dataclass
137+
class MockSqlColumnMetaView:
138+
"""Mock SqlColumnMetaView for testing."""
139+
140+
name: str = "id"
141+
type: str = "INTEGER"
142+
143+
144+
@dataclass
145+
class MockSqlResultMetaView:
146+
"""Mock SqlResultMetaView for testing."""
147+
148+
changes: int = 0
149+
duration_ms: float = 1.5
150+
rows_read_limit_reached: bool = False
151+
152+
153+
@dataclass
154+
class MockSqlQueryResultView:
155+
"""Mock SqlQueryResultView for testing."""
156+
157+
columns: list[Any] = field(default_factory=lambda: [MockSqlColumnMetaView()])
158+
meta: Any = field(default_factory=MockSqlResultMetaView)
159+
rows: list[Any] = field(default_factory=lambda: [[1, "hello"]])
160+
161+
162+
@dataclass
163+
class MockSqlStepResultView:
164+
"""Mock SqlStepResultView for testing."""
165+
166+
success: Any = field(default_factory=lambda: MockSqlQueryResultView())
167+
error: Any = None
168+
169+
170+
@dataclass
171+
class MockSqlBatchResultView:
172+
"""Mock SqlBatchResultView for testing."""
173+
174+
results: list[Any] = field(default_factory=lambda: [MockSqlStepResultView()])
175+
176+
136177
@dataclass
137178
class MockScenarioView:
138179
"""Mock ScenarioView for testing."""

tests/sdk/test_async_axon.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import pytest
88

9-
from tests.sdk.conftest import MockAxonView, MockPublishResultView
9+
from tests.sdk.conftest import MockAxonView, MockPublishResultView, MockSqlQueryResultView, MockSqlBatchResultView
1010
from runloop_api_client.sdk import AsyncAxon
1111

1212

@@ -75,3 +75,38 @@ async def test_subscribe_sse(self, mock_async_client: AsyncMock) -> None:
7575

7676
assert result == mock_stream
7777
mock_async_client.axons.subscribe_sse.assert_awaited_once_with("axn_123")
78+
79+
@pytest.mark.asyncio
80+
async def test_sql_query(self, mock_async_client: AsyncMock) -> None:
81+
"""Test sql.query method delegates to client.axons.sql.query."""
82+
mock_result = MockSqlQueryResultView()
83+
mock_async_client.axons.sql.query = AsyncMock(return_value=mock_result)
84+
85+
axon = AsyncAxon(mock_async_client, "axn_123")
86+
result = await axon.sql.query(sql="SELECT * FROM test WHERE id = ?", params=[1])
87+
88+
assert result == mock_result
89+
mock_async_client.axons.sql.query.assert_awaited_once_with(
90+
"axn_123",
91+
sql="SELECT * FROM test WHERE id = ?",
92+
params=[1],
93+
)
94+
95+
@pytest.mark.asyncio
96+
async def test_sql_batch(self, mock_async_client: AsyncMock) -> None:
97+
"""Test sql.batch method delegates to client.axons.sql.batch."""
98+
mock_result = MockSqlBatchResultView()
99+
mock_async_client.axons.sql.batch = AsyncMock(return_value=mock_result)
100+
101+
statements = [
102+
{"sql": "CREATE TABLE t (id INTEGER PRIMARY KEY)"},
103+
{"sql": "INSERT INTO t (id) VALUES (?)", "params": [1]},
104+
]
105+
axon = AsyncAxon(mock_async_client, "axn_123")
106+
result = await axon.sql.batch(statements=statements)
107+
108+
assert result == mock_result
109+
mock_async_client.axons.sql.batch.assert_awaited_once_with(
110+
"axn_123",
111+
statements=statements,
112+
)

tests/sdk/test_axon.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from unittest.mock import Mock
66

7-
from tests.sdk.conftest import MockAxonView, MockPublishResultView
7+
from tests.sdk.conftest import MockAxonView, MockPublishResultView, MockSqlQueryResultView, MockSqlBatchResultView
88
from runloop_api_client.sdk import Axon
99

1010

@@ -70,3 +70,36 @@ def test_subscribe_sse(self, mock_client: Mock) -> None:
7070

7171
assert result == mock_stream
7272
mock_client.axons.subscribe_sse.assert_called_once_with("axn_123")
73+
74+
def test_sql_query(self, mock_client: Mock) -> None:
75+
"""Test sql.query method delegates to client.axons.sql.query."""
76+
mock_result = MockSqlQueryResultView()
77+
mock_client.axons.sql.query.return_value = mock_result
78+
79+
axon = Axon(mock_client, "axn_123")
80+
result = axon.sql.query(sql="SELECT * FROM test WHERE id = ?", params=[1])
81+
82+
assert result == mock_result
83+
mock_client.axons.sql.query.assert_called_once_with(
84+
"axn_123",
85+
sql="SELECT * FROM test WHERE id = ?",
86+
params=[1],
87+
)
88+
89+
def test_sql_batch(self, mock_client: Mock) -> None:
90+
"""Test sql.batch method delegates to client.axons.sql.batch."""
91+
mock_result = MockSqlBatchResultView()
92+
mock_client.axons.sql.batch.return_value = mock_result
93+
94+
statements = [
95+
{"sql": "CREATE TABLE t (id INTEGER PRIMARY KEY)"},
96+
{"sql": "INSERT INTO t (id) VALUES (?)", "params": [1]},
97+
]
98+
axon = Axon(mock_client, "axn_123")
99+
result = axon.sql.batch(statements=statements)
100+
101+
assert result == mock_result
102+
mock_client.axons.sql.batch.assert_called_once_with(
103+
"axn_123",
104+
statements=statements,
105+
)

tests/smoketests/sdk/test_async_axon.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,46 @@ async def test_axon_publish(self, async_sdk_client: AsyncRunloopSDK) -> None:
6969
pass
7070

7171

72+
class TestAsyncAxonSql:
73+
"""Test async axon SQL operations."""
74+
75+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
76+
async def test_sql_query_create_and_select(self, async_sdk_client: AsyncRunloopSDK) -> None:
77+
"""Test creating a table and querying it via sql.query."""
78+
axon = await async_sdk_client.axon.create()
79+
80+
await axon.sql.query(sql="CREATE TABLE IF NOT EXISTS smoke_test (id INTEGER PRIMARY KEY, value TEXT)")
81+
82+
await axon.sql.query(sql="INSERT INTO smoke_test (id, value) VALUES (?, ?)", params=[1, "hello"])
83+
84+
result = await axon.sql.query(sql="SELECT * FROM smoke_test WHERE id = ?", params=[1])
85+
86+
assert result.columns is not None
87+
assert len(result.columns) > 0
88+
assert len(result.rows) == 1
89+
assert result.meta.duration_ms >= 0
90+
91+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
92+
async def test_sql_batch(self, async_sdk_client: AsyncRunloopSDK) -> None:
93+
"""Test executing multiple statements atomically via sql.batch."""
94+
axon = await async_sdk_client.axon.create()
95+
96+
result = await axon.sql.batch(
97+
statements=[
98+
{"sql": "CREATE TABLE IF NOT EXISTS batch_test (id INTEGER PRIMARY KEY, name TEXT)"},
99+
{"sql": "INSERT INTO batch_test (id, name) VALUES (?, ?)", "params": [1, "alice"]},
100+
{"sql": "INSERT INTO batch_test (id, name) VALUES (?, ?)", "params": [2, "bob"]},
101+
{"sql": "SELECT * FROM batch_test ORDER BY id"},
102+
],
103+
)
104+
105+
assert result.results is not None
106+
assert len(result.results) == 4
107+
select_result = result.results[3]
108+
assert select_result.success is not None
109+
assert len(select_result.success.rows) == 2
110+
111+
72112
class TestAsyncAxonListing:
73113
"""Test axon listing operations."""
74114

0 commit comments

Comments
 (0)