Skip to content

Commit d683fd1

Browse files
authored
fix: adding axon SQL to sdk (#767)
1 parent cee8ae7 commit d683fd1

File tree

10 files changed

+298
-9
lines changed

10 files changed

+298
-9
lines changed

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: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
from ..types.devboxes import DiskSnapshotListParams, DiskSnapshotUpdateParams
4343
from ..types.scenarios import ScorerListParams, ScorerCreateParams, ScorerUpdateParams
4444
from ..types.devbox_create_params import DevboxBaseCreateParams
45+
from ..types.axons.sql_batch_params import SqlBatchParams
46+
from ..types.axons.sql_query_params import SqlQueryParams
4547
from ..types.scenario_start_run_params import ScenarioStartRunBaseParams
4648
from ..types.benchmark_start_run_params import BenchmarkSelfStartRunParams
4749
from ..types.devbox_execute_async_params import DevboxNiceExecuteAsyncParams
@@ -196,6 +198,14 @@ class SDKAxonPublishParams(AxonPublishParams, LongRequestOptions):
196198
pass
197199

198200

201+
class SDKAxonSqlQueryParams(SqlQueryParams, LongRequestOptions):
202+
pass
203+
204+
205+
class SDKAxonSqlBatchParams(SqlBatchParams, LongRequestOptions):
206+
pass
207+
208+
199209
class SDKScenarioListParams(ScenarioListParams, BaseRequestOptions):
200210
pass
201211

src/runloop_api_client/sdk/async_axon.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,48 @@
77
from ._types import (
88
BaseRequestOptions,
99
SDKAxonPublishParams,
10+
SDKAxonSqlBatchParams,
11+
SDKAxonSqlQueryParams,
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_batch_result_view import SqlBatchResultView
19+
from ..types.axons.sql_query_result_view import SqlQueryResultView
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,17 @@ 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)
68+
69+
@property
70+
def sql(self) -> AsyncAxonSqlOps:
71+
return self._sql
3772

3873
@override
3974
def __repr__(self) -> str:

src/runloop_api_client/sdk/axon.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,48 @@
77
from ._types import (
88
BaseRequestOptions,
99
SDKAxonPublishParams,
10+
SDKAxonSqlBatchParams,
11+
SDKAxonSqlQueryParams,
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_batch_result_view import SqlBatchResultView
19+
from ..types.axons.sql_query_result_view import SqlQueryResultView
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,17 @@ 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)
68+
69+
@property
70+
def sql(self) -> AxonSqlOps:
71+
return self._sql
3772

3873
@override
3974
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: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66

77
import pytest
88

9-
from tests.sdk.conftest import MockAxonView, MockPublishResultView
9+
from tests.sdk.conftest import MockAxonView, MockPublishResultView, MockSqlBatchResultView, MockSqlQueryResultView
1010
from runloop_api_client.sdk import AsyncAxon
11+
from runloop_api_client.types.axons.sql_statement_params import SqlStatementParams
1112

1213

1314
class TestAsyncAxon:
@@ -75,3 +76,38 @@ async def test_subscribe_sse(self, mock_async_client: AsyncMock) -> None:
7576

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

tests/sdk/test_axon.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44

55
from unittest.mock import Mock
66

7-
from tests.sdk.conftest import MockAxonView, MockPublishResultView
7+
from tests.sdk.conftest import MockAxonView, MockPublishResultView, MockSqlBatchResultView, MockSqlQueryResultView
88
from runloop_api_client.sdk import Axon
9+
from runloop_api_client.types.axons.sql_statement_params import SqlStatementParams
910

1011

1112
class TestAxon:
@@ -70,3 +71,36 @@ def test_subscribe_sse(self, mock_client: Mock) -> None:
7071

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

0 commit comments

Comments
 (0)