From 5534d287a6ed554988fb84082b4dcc1b5c1c687f Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Tue, 24 Mar 2026 20:09:23 -0700 Subject: [PATCH 1/6] cp dines --- src/runloop_api_client/sdk/__init__.py | 6 ++-- src/runloop_api_client/sdk/_types.py | 13 ++++++++ src/runloop_api_client/sdk/async_axon.py | 35 ++++++++++++++++++-- src/runloop_api_client/sdk/axon.py | 35 ++++++++++++++++++-- tests/sdk/conftest.py | 41 ++++++++++++++++++++++++ tests/sdk/test_async_axon.py | 37 ++++++++++++++++++++- tests/sdk/test_axon.py | 35 +++++++++++++++++++- tests/smoketests/sdk/test_async_axon.py | 40 +++++++++++++++++++++++ tests/smoketests/sdk/test_axon.py | 40 +++++++++++++++++++++++ uv.lock | 2 +- 10 files changed, 275 insertions(+), 9 deletions(-) diff --git a/src/runloop_api_client/sdk/__init__.py b/src/runloop_api_client/sdk/__init__.py index 136080fe8..38b3d0e41 100644 --- a/src/runloop_api_client/sdk/__init__.py +++ b/src/runloop_api_client/sdk/__init__.py @@ -5,7 +5,7 @@ from __future__ import annotations -from .axon import Axon +from .axon import Axon, AxonSqlOps from .sync import ( AxonOps, AgentOps, @@ -48,7 +48,7 @@ from .benchmark import Benchmark from .blueprint import Blueprint from .execution import Execution -from .async_axon import AsyncAxon +from .async_axon import AsyncAxon, AsyncAxonSqlOps from .mcp_config import McpConfig from .async_agent import AsyncAgent from .async_devbox import AsyncDevbox, AsyncNamedShell @@ -111,6 +111,8 @@ "AsyncAgent", "Axon", "AsyncAxon", + "AxonSqlOps", + "AsyncAxonSqlOps", "AsyncSecret", "Benchmark", "AsyncBenchmark", diff --git a/src/runloop_api_client/sdk/_types.py b/src/runloop_api_client/sdk/_types.py index ac678f126..7ec9e9943 100644 --- a/src/runloop_api_client/sdk/_types.py +++ b/src/runloop_api_client/sdk/_types.py @@ -196,6 +196,19 @@ class SDKAxonPublishParams(AxonPublishParams, LongRequestOptions): pass +class SDKAxonSqlQueryParams(LongRequestOptions, total=False): + sql: str + """SQL query with ?-style positional placeholders.""" + + params: list[object] + """Positional parameter bindings for ? placeholders.""" + + +class SDKAxonSqlBatchParams(LongRequestOptions, total=False): + statements: list[dict[str, object]] + """The SQL statements to execute atomically within a transaction.""" + + class SDKScenarioListParams(ScenarioListParams, BaseRequestOptions): pass diff --git a/src/runloop_api_client/sdk/async_axon.py b/src/runloop_api_client/sdk/async_axon.py index dda6a3037..546374616 100644 --- a/src/runloop_api_client/sdk/async_axon.py +++ b/src/runloop_api_client/sdk/async_axon.py @@ -7,19 +7,48 @@ from ._types import ( BaseRequestOptions, SDKAxonPublishParams, + SDKAxonSqlQueryParams, + SDKAxonSqlBatchParams, ) from .._client import AsyncRunloop from .._streaming import AsyncStream from ..types.axon_view import AxonView from ..types.axon_event_view import AxonEventView from ..types.publish_result_view import PublishResultView +from ..types.axons.sql_query_result_view import SqlQueryResultView +from ..types.axons.sql_batch_result_view import SqlBatchResultView + + +class AsyncAxonSqlOps: + """[Beta] Async SQL operations for an axon's SQLite database. + + Access via ``axon.sql``. + + Example: + >>> axon = await runloop.axon.create() + >>> await axon.sql.query(sql="CREATE TABLE tasks (id INTEGER PRIMARY KEY, name TEXT)") + >>> result = await axon.sql.query(sql="SELECT * FROM tasks WHERE id = ?", params=[1]) + """ + + def __init__(self, client: AsyncRunloop, axon_id: str) -> None: + self._client = client + self._axon_id = axon_id + + async def query(self, **params: Unpack[SDKAxonSqlQueryParams]) -> SqlQueryResultView: + """[Beta] Execute a single parameterized SQL statement against this axon's SQLite database.""" + return await self._client.axons.sql.query(self._axon_id, **params) + + async def batch(self, **params: Unpack[SDKAxonSqlBatchParams]) -> SqlBatchResultView: + """[Beta] Execute multiple SQL statements atomically within a single transaction.""" + return await self._client.axons.sql.batch(self._axon_id, **params) class AsyncAxon: """[Beta] Wrapper around asynchronous axon operations. - Axons are event communication channels that support publishing events - and subscribing to event streams via server-sent events (SSE). + Axons are event communication channels that support publishing events, + subscribing to event streams via server-sent events (SSE), and executing + SQL queries against an embedded SQLite database. Obtain instances via ``runloop.axon.create()`` or ``runloop.axon.from_id()``. Example: @@ -29,11 +58,13 @@ class AsyncAxon: >>> async with await axon.subscribe_sse() as stream: ... async for event in stream: ... print(event.event_type, event.payload) + >>> await axon.sql.query(sql="CREATE TABLE tasks (id INTEGER PRIMARY KEY, name TEXT)") """ def __init__(self, client: AsyncRunloop, axon_id: str) -> None: self._client = client self._id = axon_id + self.sql = AsyncAxonSqlOps(client, axon_id) @override def __repr__(self) -> str: diff --git a/src/runloop_api_client/sdk/axon.py b/src/runloop_api_client/sdk/axon.py index eb310a88f..b07606b6e 100644 --- a/src/runloop_api_client/sdk/axon.py +++ b/src/runloop_api_client/sdk/axon.py @@ -7,19 +7,48 @@ from ._types import ( BaseRequestOptions, SDKAxonPublishParams, + SDKAxonSqlQueryParams, + SDKAxonSqlBatchParams, ) from .._client import Runloop from .._streaming import Stream from ..types.axon_view import AxonView from ..types.axon_event_view import AxonEventView from ..types.publish_result_view import PublishResultView +from ..types.axons.sql_query_result_view import SqlQueryResultView +from ..types.axons.sql_batch_result_view import SqlBatchResultView + + +class AxonSqlOps: + """[Beta] SQL operations for an axon's SQLite database. + + Access via ``axon.sql``. + + Example: + >>> axon = runloop.axon.create() + >>> axon.sql.query(sql="CREATE TABLE tasks (id INTEGER PRIMARY KEY, name TEXT)") + >>> result = axon.sql.query(sql="SELECT * FROM tasks WHERE id = ?", params=[1]) + """ + + def __init__(self, client: Runloop, axon_id: str) -> None: + self._client = client + self._axon_id = axon_id + + def query(self, **params: Unpack[SDKAxonSqlQueryParams]) -> SqlQueryResultView: + """[Beta] Execute a single parameterized SQL statement against this axon's SQLite database.""" + return self._client.axons.sql.query(self._axon_id, **params) + + def batch(self, **params: Unpack[SDKAxonSqlBatchParams]) -> SqlBatchResultView: + """[Beta] Execute multiple SQL statements atomically within a single transaction.""" + return self._client.axons.sql.batch(self._axon_id, **params) class Axon: """[Beta] Wrapper around synchronous axon operations. - Axons are event communication channels that support publishing events - and subscribing to event streams via server-sent events (SSE). + Axons are event communication channels that support publishing events, + subscribing to event streams via server-sent events (SSE), and executing + SQL queries against an embedded SQLite database. Obtain instances via ``runloop.axon.create()`` or ``runloop.axon.from_id()``. Example: @@ -29,11 +58,13 @@ class Axon: >>> with axon.subscribe_sse() as stream: ... for event in stream: ... print(event.event_type, event.payload) + >>> axon.sql.query(sql="CREATE TABLE tasks (id INTEGER PRIMARY KEY, name TEXT)") """ def __init__(self, client: Runloop, axon_id: str) -> None: self._client = client self._id = axon_id + self.sql = AxonSqlOps(client, axon_id) @override def __repr__(self) -> str: diff --git a/tests/sdk/conftest.py b/tests/sdk/conftest.py index ab8d0d048..92ab25f42 100644 --- a/tests/sdk/conftest.py +++ b/tests/sdk/conftest.py @@ -133,6 +133,47 @@ class MockPublishResultView: timestamp_ms: int = 1234567890000 +@dataclass +class MockSqlColumnMetaView: + """Mock SqlColumnMetaView for testing.""" + + name: str = "id" + type: str = "INTEGER" + + +@dataclass +class MockSqlResultMetaView: + """Mock SqlResultMetaView for testing.""" + + changes: int = 0 + duration_ms: float = 1.5 + rows_read_limit_reached: bool = False + + +@dataclass +class MockSqlQueryResultView: + """Mock SqlQueryResultView for testing.""" + + columns: list[Any] = field(default_factory=lambda: [MockSqlColumnMetaView()]) + meta: Any = field(default_factory=MockSqlResultMetaView) + rows: list[Any] = field(default_factory=lambda: [[1, "hello"]]) + + +@dataclass +class MockSqlStepResultView: + """Mock SqlStepResultView for testing.""" + + success: Any = field(default_factory=lambda: MockSqlQueryResultView()) + error: Any = None + + +@dataclass +class MockSqlBatchResultView: + """Mock SqlBatchResultView for testing.""" + + results: list[Any] = field(default_factory=lambda: [MockSqlStepResultView()]) + + @dataclass class MockScenarioView: """Mock ScenarioView for testing.""" diff --git a/tests/sdk/test_async_axon.py b/tests/sdk/test_async_axon.py index f5ce9761d..99e4fcae5 100644 --- a/tests/sdk/test_async_axon.py +++ b/tests/sdk/test_async_axon.py @@ -6,7 +6,7 @@ import pytest -from tests.sdk.conftest import MockAxonView, MockPublishResultView +from tests.sdk.conftest import MockAxonView, MockPublishResultView, MockSqlQueryResultView, MockSqlBatchResultView from runloop_api_client.sdk import AsyncAxon @@ -75,3 +75,38 @@ async def test_subscribe_sse(self, mock_async_client: AsyncMock) -> None: assert result == mock_stream mock_async_client.axons.subscribe_sse.assert_awaited_once_with("axn_123") + + @pytest.mark.asyncio + async def test_sql_query(self, mock_async_client: AsyncMock) -> None: + """Test sql.query method delegates to client.axons.sql.query.""" + mock_result = MockSqlQueryResultView() + mock_async_client.axons.sql.query = AsyncMock(return_value=mock_result) + + axon = AsyncAxon(mock_async_client, "axn_123") + result = await axon.sql.query(sql="SELECT * FROM test WHERE id = ?", params=[1]) + + assert result == mock_result + mock_async_client.axons.sql.query.assert_awaited_once_with( + "axn_123", + sql="SELECT * FROM test WHERE id = ?", + params=[1], + ) + + @pytest.mark.asyncio + async def test_sql_batch(self, mock_async_client: AsyncMock) -> None: + """Test sql.batch method delegates to client.axons.sql.batch.""" + mock_result = MockSqlBatchResultView() + mock_async_client.axons.sql.batch = AsyncMock(return_value=mock_result) + + statements = [ + {"sql": "CREATE TABLE t (id INTEGER PRIMARY KEY)"}, + {"sql": "INSERT INTO t (id) VALUES (?)", "params": [1]}, + ] + axon = AsyncAxon(mock_async_client, "axn_123") + result = await axon.sql.batch(statements=statements) + + assert result == mock_result + mock_async_client.axons.sql.batch.assert_awaited_once_with( + "axn_123", + statements=statements, + ) diff --git a/tests/sdk/test_axon.py b/tests/sdk/test_axon.py index 58871d339..b8f5c9660 100644 --- a/tests/sdk/test_axon.py +++ b/tests/sdk/test_axon.py @@ -4,7 +4,7 @@ from unittest.mock import Mock -from tests.sdk.conftest import MockAxonView, MockPublishResultView +from tests.sdk.conftest import MockAxonView, MockPublishResultView, MockSqlQueryResultView, MockSqlBatchResultView from runloop_api_client.sdk import Axon @@ -70,3 +70,36 @@ def test_subscribe_sse(self, mock_client: Mock) -> None: assert result == mock_stream mock_client.axons.subscribe_sse.assert_called_once_with("axn_123") + + def test_sql_query(self, mock_client: Mock) -> None: + """Test sql.query method delegates to client.axons.sql.query.""" + mock_result = MockSqlQueryResultView() + mock_client.axons.sql.query.return_value = mock_result + + axon = Axon(mock_client, "axn_123") + result = axon.sql.query(sql="SELECT * FROM test WHERE id = ?", params=[1]) + + assert result == mock_result + mock_client.axons.sql.query.assert_called_once_with( + "axn_123", + sql="SELECT * FROM test WHERE id = ?", + params=[1], + ) + + def test_sql_batch(self, mock_client: Mock) -> None: + """Test sql.batch method delegates to client.axons.sql.batch.""" + mock_result = MockSqlBatchResultView() + mock_client.axons.sql.batch.return_value = mock_result + + statements = [ + {"sql": "CREATE TABLE t (id INTEGER PRIMARY KEY)"}, + {"sql": "INSERT INTO t (id) VALUES (?)", "params": [1]}, + ] + axon = Axon(mock_client, "axn_123") + result = axon.sql.batch(statements=statements) + + assert result == mock_result + mock_client.axons.sql.batch.assert_called_once_with( + "axn_123", + statements=statements, + ) diff --git a/tests/smoketests/sdk/test_async_axon.py b/tests/smoketests/sdk/test_async_axon.py index ced85820d..a9a708821 100644 --- a/tests/smoketests/sdk/test_async_axon.py +++ b/tests/smoketests/sdk/test_async_axon.py @@ -69,6 +69,46 @@ async def test_axon_publish(self, async_sdk_client: AsyncRunloopSDK) -> None: pass +class TestAsyncAxonSql: + """Test async axon SQL operations.""" + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + async def test_sql_query_create_and_select(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test creating a table and querying it via sql.query.""" + axon = await async_sdk_client.axon.create() + + await axon.sql.query(sql="CREATE TABLE IF NOT EXISTS smoke_test (id INTEGER PRIMARY KEY, value TEXT)") + + await axon.sql.query(sql="INSERT INTO smoke_test (id, value) VALUES (?, ?)", params=[1, "hello"]) + + result = await axon.sql.query(sql="SELECT * FROM smoke_test WHERE id = ?", params=[1]) + + assert result.columns is not None + assert len(result.columns) > 0 + assert len(result.rows) == 1 + assert result.meta.duration_ms >= 0 + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + async def test_sql_batch(self, async_sdk_client: AsyncRunloopSDK) -> None: + """Test executing multiple statements atomically via sql.batch.""" + axon = await async_sdk_client.axon.create() + + result = await axon.sql.batch( + statements=[ + {"sql": "CREATE TABLE IF NOT EXISTS batch_test (id INTEGER PRIMARY KEY, name TEXT)"}, + {"sql": "INSERT INTO batch_test (id, name) VALUES (?, ?)", "params": [1, "alice"]}, + {"sql": "INSERT INTO batch_test (id, name) VALUES (?, ?)", "params": [2, "bob"]}, + {"sql": "SELECT * FROM batch_test ORDER BY id"}, + ], + ) + + assert result.results is not None + assert len(result.results) == 4 + select_result = result.results[3] + assert select_result.success is not None + assert len(select_result.success.rows) == 2 + + class TestAsyncAxonListing: """Test axon listing operations.""" diff --git a/tests/smoketests/sdk/test_axon.py b/tests/smoketests/sdk/test_axon.py index c8947d006..95efc3e80 100644 --- a/tests/smoketests/sdk/test_axon.py +++ b/tests/smoketests/sdk/test_axon.py @@ -69,6 +69,46 @@ def test_axon_publish(self, sdk_client: RunloopSDK) -> None: pass +class TestAxonSql: + """Test axon SQL operations.""" + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_sql_query_create_and_select(self, sdk_client: RunloopSDK) -> None: + """Test creating a table and querying it via sql.query.""" + axon = sdk_client.axon.create() + + axon.sql.query(sql="CREATE TABLE IF NOT EXISTS smoke_test (id INTEGER PRIMARY KEY, value TEXT)") + + axon.sql.query(sql="INSERT INTO smoke_test (id, value) VALUES (?, ?)", params=[1, "hello"]) + + result = axon.sql.query(sql="SELECT * FROM smoke_test WHERE id = ?", params=[1]) + + assert result.columns is not None + assert len(result.columns) > 0 + assert len(result.rows) == 1 + assert result.meta.duration_ms >= 0 + + @pytest.mark.timeout(THIRTY_SECOND_TIMEOUT) + def test_sql_batch(self, sdk_client: RunloopSDK) -> None: + """Test executing multiple statements atomically via sql.batch.""" + axon = sdk_client.axon.create() + + result = axon.sql.batch( + statements=[ + {"sql": "CREATE TABLE IF NOT EXISTS batch_test (id INTEGER PRIMARY KEY, name TEXT)"}, + {"sql": "INSERT INTO batch_test (id, name) VALUES (?, ?)", "params": [1, "alice"]}, + {"sql": "INSERT INTO batch_test (id, name) VALUES (?, ?)", "params": [2, "bob"]}, + {"sql": "SELECT * FROM batch_test ORDER BY id"}, + ], + ) + + assert result.results is not None + assert len(result.results) == 4 + select_result = result.results[3] + assert select_result.success is not None + assert len(select_result.success.rows) == 2 + + class TestAxonListing: """Test axon listing operations.""" diff --git a/uv.lock b/uv.lock index a43d415ef..970c26cc8 100644 --- a/uv.lock +++ b/uv.lock @@ -2386,7 +2386,7 @@ wheels = [ [[package]] name = "runloop-api-client" -version = "1.13.0" +version = "1.13.1" source = { editable = "." } dependencies = [ { name = "anyio" }, From 3b2382fdbb90f2aa3d3c94dd09281ae221b30572 Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Tue, 24 Mar 2026 20:12:02 -0700 Subject: [PATCH 2/6] cp dines --- src/runloop_api_client/sdk/async_axon.py | 4 ++-- src/runloop_api_client/sdk/axon.py | 4 ++-- tests/sdk/test_async_axon.py | 2 +- tests/sdk/test_axon.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/runloop_api_client/sdk/async_axon.py b/src/runloop_api_client/sdk/async_axon.py index 546374616..083974425 100644 --- a/src/runloop_api_client/sdk/async_axon.py +++ b/src/runloop_api_client/sdk/async_axon.py @@ -7,16 +7,16 @@ from ._types import ( BaseRequestOptions, SDKAxonPublishParams, - SDKAxonSqlQueryParams, SDKAxonSqlBatchParams, + SDKAxonSqlQueryParams, ) from .._client import AsyncRunloop from .._streaming import AsyncStream from ..types.axon_view import AxonView from ..types.axon_event_view import AxonEventView from ..types.publish_result_view import PublishResultView -from ..types.axons.sql_query_result_view import SqlQueryResultView from ..types.axons.sql_batch_result_view import SqlBatchResultView +from ..types.axons.sql_query_result_view import SqlQueryResultView class AsyncAxonSqlOps: diff --git a/src/runloop_api_client/sdk/axon.py b/src/runloop_api_client/sdk/axon.py index b07606b6e..1894357d8 100644 --- a/src/runloop_api_client/sdk/axon.py +++ b/src/runloop_api_client/sdk/axon.py @@ -7,16 +7,16 @@ from ._types import ( BaseRequestOptions, SDKAxonPublishParams, - SDKAxonSqlQueryParams, SDKAxonSqlBatchParams, + SDKAxonSqlQueryParams, ) from .._client import Runloop from .._streaming import Stream from ..types.axon_view import AxonView from ..types.axon_event_view import AxonEventView from ..types.publish_result_view import PublishResultView -from ..types.axons.sql_query_result_view import SqlQueryResultView from ..types.axons.sql_batch_result_view import SqlBatchResultView +from ..types.axons.sql_query_result_view import SqlQueryResultView class AxonSqlOps: diff --git a/tests/sdk/test_async_axon.py b/tests/sdk/test_async_axon.py index 99e4fcae5..3d62dd4fc 100644 --- a/tests/sdk/test_async_axon.py +++ b/tests/sdk/test_async_axon.py @@ -6,7 +6,7 @@ import pytest -from tests.sdk.conftest import MockAxonView, MockPublishResultView, MockSqlQueryResultView, MockSqlBatchResultView +from tests.sdk.conftest import MockAxonView, MockPublishResultView, MockSqlBatchResultView, MockSqlQueryResultView from runloop_api_client.sdk import AsyncAxon diff --git a/tests/sdk/test_axon.py b/tests/sdk/test_axon.py index b8f5c9660..4e2138fe9 100644 --- a/tests/sdk/test_axon.py +++ b/tests/sdk/test_axon.py @@ -4,7 +4,7 @@ from unittest.mock import Mock -from tests.sdk.conftest import MockAxonView, MockPublishResultView, MockSqlQueryResultView, MockSqlBatchResultView +from tests.sdk.conftest import MockAxonView, MockPublishResultView, MockSqlBatchResultView, MockSqlQueryResultView from runloop_api_client.sdk import Axon From 42708a7ed2286a8d3025b5cfa58792eff3ed9555 Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Tue, 24 Mar 2026 20:18:43 -0700 Subject: [PATCH 3/6] cp dines --- src/runloop_api_client/sdk/_types.py | 5 +++-- tests/sdk/test_async_axon.py | 3 ++- tests/sdk/test_axon.py | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/runloop_api_client/sdk/_types.py b/src/runloop_api_client/sdk/_types.py index 7ec9e9943..b8034b713 100644 --- a/src/runloop_api_client/sdk/_types.py +++ b/src/runloop_api_client/sdk/_types.py @@ -1,4 +1,4 @@ -from typing import Union, Literal, Callable, Optional +from typing import Union, Literal, Callable, Iterable, Optional from typing_extensions import TypedDict from ..types import ( @@ -39,6 +39,7 @@ ) from .._types import Body, Query, Headers, Timeout, NotGiven from ..lib.polling import PollingConfig +from ..types.axons.sql_statement_params import SqlStatementParams from ..types.devboxes import DiskSnapshotListParams, DiskSnapshotUpdateParams from ..types.scenarios import ScorerListParams, ScorerCreateParams, ScorerUpdateParams from ..types.devbox_create_params import DevboxBaseCreateParams @@ -205,7 +206,7 @@ class SDKAxonSqlQueryParams(LongRequestOptions, total=False): class SDKAxonSqlBatchParams(LongRequestOptions, total=False): - statements: list[dict[str, object]] + statements: Iterable[SqlStatementParams] """The SQL statements to execute atomically within a transaction.""" diff --git a/tests/sdk/test_async_axon.py b/tests/sdk/test_async_axon.py index 3d62dd4fc..f24101673 100644 --- a/tests/sdk/test_async_axon.py +++ b/tests/sdk/test_async_axon.py @@ -8,6 +8,7 @@ from tests.sdk.conftest import MockAxonView, MockPublishResultView, MockSqlBatchResultView, MockSqlQueryResultView from runloop_api_client.sdk import AsyncAxon +from runloop_api_client.types.axons.sql_statement_params import SqlStatementParams class TestAsyncAxon: @@ -98,7 +99,7 @@ async def test_sql_batch(self, mock_async_client: AsyncMock) -> None: mock_result = MockSqlBatchResultView() mock_async_client.axons.sql.batch = AsyncMock(return_value=mock_result) - statements = [ + statements: list[SqlStatementParams] = [ {"sql": "CREATE TABLE t (id INTEGER PRIMARY KEY)"}, {"sql": "INSERT INTO t (id) VALUES (?)", "params": [1]}, ] diff --git a/tests/sdk/test_axon.py b/tests/sdk/test_axon.py index 4e2138fe9..16cf565c3 100644 --- a/tests/sdk/test_axon.py +++ b/tests/sdk/test_axon.py @@ -6,6 +6,7 @@ from tests.sdk.conftest import MockAxonView, MockPublishResultView, MockSqlBatchResultView, MockSqlQueryResultView from runloop_api_client.sdk import Axon +from runloop_api_client.types.axons.sql_statement_params import SqlStatementParams class TestAxon: @@ -91,7 +92,7 @@ def test_sql_batch(self, mock_client: Mock) -> None: mock_result = MockSqlBatchResultView() mock_client.axons.sql.batch.return_value = mock_result - statements = [ + statements: list[SqlStatementParams] = [ {"sql": "CREATE TABLE t (id INTEGER PRIMARY KEY)"}, {"sql": "INSERT INTO t (id) VALUES (?)", "params": [1]}, ] From 3b32f34e3e1ca1738cda20578b230daf9d42112b Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Tue, 24 Mar 2026 20:22:40 -0700 Subject: [PATCH 4/6] cp dines --- src/runloop_api_client/sdk/_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runloop_api_client/sdk/_types.py b/src/runloop_api_client/sdk/_types.py index b8034b713..fe1e99133 100644 --- a/src/runloop_api_client/sdk/_types.py +++ b/src/runloop_api_client/sdk/_types.py @@ -39,11 +39,11 @@ ) from .._types import Body, Query, Headers, Timeout, NotGiven from ..lib.polling import PollingConfig -from ..types.axons.sql_statement_params import SqlStatementParams from ..types.devboxes import DiskSnapshotListParams, DiskSnapshotUpdateParams from ..types.scenarios import ScorerListParams, ScorerCreateParams, ScorerUpdateParams from ..types.devbox_create_params import DevboxBaseCreateParams from ..types.scenario_start_run_params import ScenarioStartRunBaseParams +from ..types.axons.sql_statement_params import SqlStatementParams from ..types.benchmark_start_run_params import BenchmarkSelfStartRunParams from ..types.devbox_execute_async_params import DevboxNiceExecuteAsyncParams From bb3e2cbfffd3208d474ca220df9fa5e2f0528d10 Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Tue, 24 Mar 2026 20:30:13 -0700 Subject: [PATCH 5/6] cp dines --- src/runloop_api_client/sdk/_types.py | 18 +++++++----------- tests/smoketests/sdk/test_axon.py | 12 +++++++++--- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/runloop_api_client/sdk/_types.py b/src/runloop_api_client/sdk/_types.py index fe1e99133..aa7e0208a 100644 --- a/src/runloop_api_client/sdk/_types.py +++ b/src/runloop_api_client/sdk/_types.py @@ -1,4 +1,4 @@ -from typing import Union, Literal, Callable, Iterable, Optional +from typing import Union, Literal, Callable, Optional from typing_extensions import TypedDict from ..types import ( @@ -42,8 +42,9 @@ from ..types.devboxes import DiskSnapshotListParams, DiskSnapshotUpdateParams from ..types.scenarios import ScorerListParams, ScorerCreateParams, ScorerUpdateParams from ..types.devbox_create_params import DevboxBaseCreateParams +from ..types.axons.sql_batch_params import SqlBatchParams +from ..types.axons.sql_query_params import SqlQueryParams from ..types.scenario_start_run_params import ScenarioStartRunBaseParams -from ..types.axons.sql_statement_params import SqlStatementParams from ..types.benchmark_start_run_params import BenchmarkSelfStartRunParams from ..types.devbox_execute_async_params import DevboxNiceExecuteAsyncParams @@ -197,17 +198,12 @@ class SDKAxonPublishParams(AxonPublishParams, LongRequestOptions): pass -class SDKAxonSqlQueryParams(LongRequestOptions, total=False): - sql: str - """SQL query with ?-style positional placeholders.""" - - params: list[object] - """Positional parameter bindings for ? placeholders.""" +class SDKAxonSqlQueryParams(SqlQueryParams, LongRequestOptions): + pass -class SDKAxonSqlBatchParams(LongRequestOptions, total=False): - statements: Iterable[SqlStatementParams] - """The SQL statements to execute atomically within a transaction.""" +class SDKAxonSqlBatchParams(SqlBatchParams, LongRequestOptions): + pass class SDKScenarioListParams(ScenarioListParams, BaseRequestOptions): diff --git a/tests/smoketests/sdk/test_axon.py b/tests/smoketests/sdk/test_axon.py index 95efc3e80..10cd5fc48 100644 --- a/tests/smoketests/sdk/test_axon.py +++ b/tests/smoketests/sdk/test_axon.py @@ -3,11 +3,16 @@ from __future__ import annotations import json +import uuid import pytest from runloop_api_client.sdk import RunloopSDK + +def _unique_table() -> str: + return f"t_{uuid.uuid4().hex[:12]}" + pytestmark = [pytest.mark.smoketest] THIRTY_SECOND_TIMEOUT = 30 @@ -76,12 +81,13 @@ class TestAxonSql: def test_sql_query_create_and_select(self, sdk_client: RunloopSDK) -> None: """Test creating a table and querying it via sql.query.""" axon = sdk_client.axon.create() + table = _unique_table() - axon.sql.query(sql="CREATE TABLE IF NOT EXISTS smoke_test (id INTEGER PRIMARY KEY, value TEXT)") + axon.sql.query(sql=f"CREATE TABLE {table} (id INTEGER PRIMARY KEY, value TEXT)") - axon.sql.query(sql="INSERT INTO smoke_test (id, value) VALUES (?, ?)", params=[1, "hello"]) + axon.sql.query(sql=f"INSERT INTO {table} (id, value) VALUES (?, ?)", params=[1, "hello"]) - result = axon.sql.query(sql="SELECT * FROM smoke_test WHERE id = ?", params=[1]) + result = axon.sql.query(sql=f"SELECT * FROM {table} WHERE id = ?", params=[1]) assert result.columns is not None assert len(result.columns) > 0 From 8973ef6e6c12da445d38abf4d69b0ad2d282a519 Mon Sep 17 00:00:00 2001 From: Alexander Dines Date: Tue, 24 Mar 2026 20:32:28 -0700 Subject: [PATCH 6/6] cp dines --- src/runloop_api_client/sdk/async_axon.py | 6 +++++- src/runloop_api_client/sdk/axon.py | 6 +++++- tests/smoketests/sdk/test_async_axon.py | 22 +++++++++++++++------- tests/smoketests/sdk/test_axon.py | 10 ++++++---- 4 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/runloop_api_client/sdk/async_axon.py b/src/runloop_api_client/sdk/async_axon.py index 083974425..f4410126f 100644 --- a/src/runloop_api_client/sdk/async_axon.py +++ b/src/runloop_api_client/sdk/async_axon.py @@ -64,7 +64,11 @@ class AsyncAxon: def __init__(self, client: AsyncRunloop, axon_id: str) -> None: self._client = client self._id = axon_id - self.sql = AsyncAxonSqlOps(client, axon_id) + self._sql = AsyncAxonSqlOps(client, axon_id) + + @property + def sql(self) -> AsyncAxonSqlOps: + return self._sql @override def __repr__(self) -> str: diff --git a/src/runloop_api_client/sdk/axon.py b/src/runloop_api_client/sdk/axon.py index 1894357d8..c86a31efa 100644 --- a/src/runloop_api_client/sdk/axon.py +++ b/src/runloop_api_client/sdk/axon.py @@ -64,7 +64,11 @@ class Axon: def __init__(self, client: Runloop, axon_id: str) -> None: self._client = client self._id = axon_id - self.sql = AxonSqlOps(client, axon_id) + self._sql = AxonSqlOps(client, axon_id) + + @property + def sql(self) -> AxonSqlOps: + return self._sql @override def __repr__(self) -> str: diff --git a/tests/smoketests/sdk/test_async_axon.py b/tests/smoketests/sdk/test_async_axon.py index a9a708821..065535ad2 100644 --- a/tests/smoketests/sdk/test_async_axon.py +++ b/tests/smoketests/sdk/test_async_axon.py @@ -3,11 +3,17 @@ from __future__ import annotations import json +import uuid import pytest from runloop_api_client.sdk import AsyncRunloopSDK + +def _unique_table() -> str: + return f"t_{uuid.uuid4().hex[:12]}" + + pytestmark = [pytest.mark.smoketest, pytest.mark.asyncio] THIRTY_SECOND_TIMEOUT = 30 @@ -76,12 +82,13 @@ class TestAsyncAxonSql: async def test_sql_query_create_and_select(self, async_sdk_client: AsyncRunloopSDK) -> None: """Test creating a table and querying it via sql.query.""" axon = await async_sdk_client.axon.create() + table = _unique_table() - await axon.sql.query(sql="CREATE TABLE IF NOT EXISTS smoke_test (id INTEGER PRIMARY KEY, value TEXT)") + await axon.sql.query(sql=f"CREATE TABLE {table} (id INTEGER PRIMARY KEY, value TEXT)") - await axon.sql.query(sql="INSERT INTO smoke_test (id, value) VALUES (?, ?)", params=[1, "hello"]) + await axon.sql.query(sql=f"INSERT INTO {table} (id, value) VALUES (?, ?)", params=[1, "hello"]) - result = await axon.sql.query(sql="SELECT * FROM smoke_test WHERE id = ?", params=[1]) + result = await axon.sql.query(sql=f"SELECT * FROM {table} WHERE id = ?", params=[1]) assert result.columns is not None assert len(result.columns) > 0 @@ -92,13 +99,14 @@ async def test_sql_query_create_and_select(self, async_sdk_client: AsyncRunloopS async def test_sql_batch(self, async_sdk_client: AsyncRunloopSDK) -> None: """Test executing multiple statements atomically via sql.batch.""" axon = await async_sdk_client.axon.create() + table = _unique_table() result = await axon.sql.batch( statements=[ - {"sql": "CREATE TABLE IF NOT EXISTS batch_test (id INTEGER PRIMARY KEY, name TEXT)"}, - {"sql": "INSERT INTO batch_test (id, name) VALUES (?, ?)", "params": [1, "alice"]}, - {"sql": "INSERT INTO batch_test (id, name) VALUES (?, ?)", "params": [2, "bob"]}, - {"sql": "SELECT * FROM batch_test ORDER BY id"}, + {"sql": f"CREATE TABLE {table} (id INTEGER PRIMARY KEY, name TEXT)"}, + {"sql": f"INSERT INTO {table} (id, name) VALUES (?, ?)", "params": [1, "alice"]}, + {"sql": f"INSERT INTO {table} (id, name) VALUES (?, ?)", "params": [2, "bob"]}, + {"sql": f"SELECT * FROM {table} ORDER BY id"}, ], ) diff --git a/tests/smoketests/sdk/test_axon.py b/tests/smoketests/sdk/test_axon.py index 10cd5fc48..ffdd2dd7e 100644 --- a/tests/smoketests/sdk/test_axon.py +++ b/tests/smoketests/sdk/test_axon.py @@ -13,6 +13,7 @@ def _unique_table() -> str: return f"t_{uuid.uuid4().hex[:12]}" + pytestmark = [pytest.mark.smoketest] THIRTY_SECOND_TIMEOUT = 30 @@ -98,13 +99,14 @@ def test_sql_query_create_and_select(self, sdk_client: RunloopSDK) -> None: def test_sql_batch(self, sdk_client: RunloopSDK) -> None: """Test executing multiple statements atomically via sql.batch.""" axon = sdk_client.axon.create() + table = _unique_table() result = axon.sql.batch( statements=[ - {"sql": "CREATE TABLE IF NOT EXISTS batch_test (id INTEGER PRIMARY KEY, name TEXT)"}, - {"sql": "INSERT INTO batch_test (id, name) VALUES (?, ?)", "params": [1, "alice"]}, - {"sql": "INSERT INTO batch_test (id, name) VALUES (?, ?)", "params": [2, "bob"]}, - {"sql": "SELECT * FROM batch_test ORDER BY id"}, + {"sql": f"CREATE TABLE {table} (id INTEGER PRIMARY KEY, name TEXT)"}, + {"sql": f"INSERT INTO {table} (id, name) VALUES (?, ?)", "params": [1, "alice"]}, + {"sql": f"INSERT INTO {table} (id, name) VALUES (?, ?)", "params": [2, "bob"]}, + {"sql": f"SELECT * FROM {table} ORDER BY id"}, ], )