Skip to content

Commit 2087a03

Browse files
committed
feat(rust,python,typescript): add connections API (set/list/remove)
Add native client.connections() methods across all three SDKs so consumers don't need raw HTTP calls for connection management. Endpoints: - POST /v1/user/connections/:provider — set API key connection - GET /v1/user/connections — list connections - DELETE /v1/user/connections/:provider — remove connection Closes #66
1 parent d37f9ed commit 2087a03

10 files changed

Lines changed: 374 additions & 1 deletion

File tree

python/everruns_sdk/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
Agent,
2222
AgentCapabilityConfig,
2323
CapabilityInfo,
24+
Connection,
2425
ContentPart,
2526
Controls,
2627
DeleteFileResponse,
@@ -51,6 +52,7 @@
5152
"Agent",
5253
"AgentCapabilityConfig",
5354
"CapabilityInfo",
55+
"Connection",
5456
"DeleteFileResponse",
5557
"FileInfo",
5658
"FileStat",

python/everruns_sdk/client.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
Agent,
1414
AgentCapabilityConfig,
1515
CapabilityInfo,
16+
Connection,
1617
ContentPart,
1718
Controls,
1819
CreateAgentRequest,
@@ -120,6 +121,11 @@ def session_files(self) -> "SessionFilesClient":
120121
"""Get the session files client."""
121122
return SessionFilesClient(self)
122123

124+
@property
125+
def connections(self) -> "ConnectionsClient":
126+
"""Get the connections client."""
127+
return ConnectionsClient(self)
128+
123129
def _url(self, path: str) -> str:
124130
# Use relative path (no leading slash) for correct joining with base URL.
125131
# The path parameter starts with "/" (e.g., "/agents"), so we strip it.
@@ -722,3 +728,36 @@ async def stat(self, session_id: str, path: str) -> FileStat:
722728
"""
723729
resp = await self._client._post(f"/sessions/{session_id}/fs/_/stat", {"path": path})
724730
return FileStat(**resp)
731+
732+
733+
class ConnectionsClient:
734+
"""Client for user connection operations."""
735+
736+
def __init__(self, client: Everruns):
737+
self._client = client
738+
739+
async def set(self, provider: str, api_key: str) -> Connection:
740+
"""Set an API key connection for a provider.
741+
742+
Args:
743+
provider: Provider name (e.g. "daytona").
744+
api_key: API key for the provider.
745+
"""
746+
resp = await self._client._post(
747+
f"/user/connections/{provider}",
748+
{"api_key": api_key},
749+
)
750+
return Connection(**resp)
751+
752+
async def list(self) -> list[Connection]:
753+
"""List all connections."""
754+
resp = await self._client._get("/user/connections")
755+
return [Connection(**c) for c in resp.get("data", [])]
756+
757+
async def remove(self, provider: str) -> None:
758+
"""Remove a connection.
759+
760+
Args:
761+
provider: Provider name (e.g. "daytona").
762+
"""
763+
await self._client._delete(f"/user/connections/{provider}")

python/everruns_sdk/models.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,17 @@ class DeleteFileResponse(BaseModel):
325325
deleted: bool
326326

327327

328+
# --- Connections Models ---
329+
330+
331+
class Connection(BaseModel):
332+
"""A user connection to an external provider."""
333+
334+
provider: str
335+
created_at: str
336+
updated_at: str
337+
338+
328339
def extract_tool_calls(data: dict[str, Any]) -> list[ToolCallInfo]:
329340
"""Extract tool call info from event data (``data.message.content``)."""
330341
message = data.get("message")

python/tests/test_client.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -950,3 +950,73 @@ def test_list_response_with_pagination_fields():
950950
assert resp.total == 10
951951
assert resp.offset == 5
952952
assert resp.limit == 25
953+
954+
955+
# --- Connections Tests ---
956+
957+
CONN_RESPONSE = {
958+
"provider": "daytona",
959+
"created_at": "2026-03-31T00:00:00Z",
960+
"updated_at": "2026-03-31T00:00:00Z",
961+
}
962+
963+
964+
@pytest.mark.asyncio
965+
@respx.mock
966+
async def test_connections_set():
967+
route = respx.post("https://custom.example.com/api/v1/user/connections/daytona").mock(
968+
return_value=httpx.Response(200, json=CONN_RESPONSE)
969+
)
970+
971+
client = Everruns(api_key="evr_test_key")
972+
try:
973+
conn = await client.connections.set("daytona", "dtn_secret_key")
974+
finally:
975+
await client.close()
976+
977+
assert conn.provider == "daytona"
978+
assert route.called
979+
body = json.loads(route.calls[0].request.content)
980+
assert body["api_key"] == "dtn_secret_key"
981+
982+
983+
@pytest.mark.asyncio
984+
@respx.mock
985+
async def test_connections_list():
986+
route = respx.get("https://custom.example.com/api/v1/user/connections").mock(
987+
return_value=httpx.Response(
988+
200,
989+
json={
990+
"data": [CONN_RESPONSE],
991+
"total": 1,
992+
"offset": 0,
993+
"limit": 100,
994+
},
995+
)
996+
)
997+
998+
client = Everruns(api_key="evr_test_key")
999+
try:
1000+
connections = await client.connections.list()
1001+
finally:
1002+
await client.close()
1003+
1004+
assert len(connections) == 1
1005+
assert connections[0].provider == "daytona"
1006+
assert route.called
1007+
1008+
1009+
@pytest.mark.asyncio
1010+
@respx.mock
1011+
async def test_connections_remove():
1012+
route = respx.delete("https://custom.example.com/api/v1/user/connections/daytona").mock(
1013+
return_value=httpx.Response(204)
1014+
)
1015+
1016+
client = Everruns(api_key="evr_test_key")
1017+
try:
1018+
await client.connections.remove("daytona")
1019+
finally:
1020+
await client.close()
1021+
1022+
assert route.called

rust/src/client.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ impl Everruns {
9797
SessionFilesClient { client: self }
9898
}
9999

100+
/// Get the connections client
101+
pub fn connections(&self) -> ConnectionsClient<'_> {
102+
ConnectionsClient { client: self }
103+
}
104+
100105
pub(crate) fn url(&self, path: &str) -> Url {
101106
// Use relative path (no leading slash) for correct joining with base URL.
102107
// The path parameter starts with "/" (e.g., "/agents"), so we strip it.
@@ -763,6 +768,33 @@ impl<'a> SessionFilesClient<'a> {
763768
}
764769
}
765770

771+
/// Client for user connection operations
772+
pub struct ConnectionsClient<'a> {
773+
client: &'a Everruns,
774+
}
775+
776+
impl<'a> ConnectionsClient<'a> {
777+
/// Set an API key connection for a provider
778+
pub async fn set(&self, provider: &str, api_key: &str) -> Result<Connection> {
779+
let req = SetConnectionRequest::new(api_key);
780+
self.client
781+
.post(&format!("/user/connections/{}", provider), &req)
782+
.await
783+
}
784+
785+
/// List all connections
786+
pub async fn list(&self) -> Result<ListResponse<Connection>> {
787+
self.client.get("/user/connections").await
788+
}
789+
790+
/// Remove a connection
791+
pub async fn remove(&self, provider: &str) -> Result<()> {
792+
self.client
793+
.delete(&format!("/user/connections/{}", provider))
794+
.await
795+
}
796+
}
797+
766798
impl std::fmt::Debug for Everruns {
767799
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
768800
f.debug_struct("Everruns")

rust/src/models.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -928,6 +928,31 @@ pub struct DeleteResponse {
928928
pub deleted: bool,
929929
}
930930

931+
// --- Connections Models ---
932+
933+
/// A user connection to an external provider
934+
#[derive(Debug, Clone, Serialize, Deserialize)]
935+
#[non_exhaustive]
936+
pub struct Connection {
937+
pub provider: String,
938+
pub created_at: String,
939+
pub updated_at: String,
940+
}
941+
942+
/// Request to set a connection API key
943+
#[derive(Debug, Clone, Serialize)]
944+
pub struct SetConnectionRequest {
945+
pub api_key: String,
946+
}
947+
948+
impl SetConnectionRequest {
949+
pub fn new(api_key: impl Into<String>) -> Self {
950+
Self {
951+
api_key: api_key.into(),
952+
}
953+
}
954+
}
955+
931956
#[cfg(test)]
932957
mod tests {
933958
use super::*;

rust/tests/client_test.rs

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use everruns_sdk::{
44
CreateAgentRequest, CreateFileRequest, CreateSessionRequest, Everruns, InitialFile,
5-
UpdateFileRequest,
5+
SetConnectionRequest, UpdateFileRequest,
66
};
77
use wiremock::{
88
Mock, MockServer, ResponseTemplate,
@@ -547,3 +547,80 @@ async fn test_session_files_stat() {
547547
assert_eq!(stat.size_bytes, 5);
548548
assert!(!stat.is_directory);
549549
}
550+
551+
// --- Connections Tests ---
552+
553+
#[tokio::test]
554+
async fn test_connections_set() {
555+
let server = MockServer::start().await;
556+
let client = Everruns::with_base_url("evr_test_key", &server.uri()).expect("client");
557+
558+
Mock::given(method("POST"))
559+
.and(path("/v1/user/connections/daytona"))
560+
.and(body_json(serde_json::json!({
561+
"api_key": "dtn_secret_key"
562+
})))
563+
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
564+
"provider": "daytona",
565+
"created_at": "2026-03-31T00:00:00Z",
566+
"updated_at": "2026-03-31T00:00:00Z"
567+
})))
568+
.mount(&server)
569+
.await;
570+
571+
let conn = client
572+
.connections()
573+
.set("daytona", "dtn_secret_key")
574+
.await
575+
.expect("set connection should succeed");
576+
577+
assert_eq!(conn.provider, "daytona");
578+
}
579+
580+
#[tokio::test]
581+
async fn test_connections_list() {
582+
let server = MockServer::start().await;
583+
let client = Everruns::with_base_url("evr_test_key", &server.uri()).expect("client");
584+
585+
Mock::given(method("GET"))
586+
.and(path("/v1/user/connections"))
587+
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
588+
"data": [{
589+
"provider": "daytona",
590+
"created_at": "2026-03-31T00:00:00Z",
591+
"updated_at": "2026-03-31T00:00:00Z"
592+
}],
593+
"total": 1,
594+
"offset": 0,
595+
"limit": 100
596+
})))
597+
.mount(&server)
598+
.await;
599+
600+
let connections = client
601+
.connections()
602+
.list()
603+
.await
604+
.expect("list connections should succeed");
605+
606+
assert_eq!(connections.data.len(), 1);
607+
assert_eq!(connections.data[0].provider, "daytona");
608+
}
609+
610+
#[tokio::test]
611+
async fn test_connections_remove() {
612+
let server = MockServer::start().await;
613+
let client = Everruns::with_base_url("evr_test_key", &server.uri()).expect("client");
614+
615+
Mock::given(method("DELETE"))
616+
.and(path("/v1/user/connections/daytona"))
617+
.respond_with(ResponseTemplate::new(204))
618+
.mount(&server)
619+
.await;
620+
621+
client
622+
.connections()
623+
.remove("daytona")
624+
.await
625+
.expect("remove connection should succeed");
626+
}

typescript/src/client.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ApiKey } from "./auth.js";
55
import {
66
Agent,
77
CapabilityInfo,
8+
Connection,
89
ContentPart,
910
CreateAgentRequest,
1011
DeleteFileResponse,
@@ -44,6 +45,7 @@ export class Everruns {
4445
readonly events: EventsClient;
4546
readonly capabilities: CapabilitiesClient;
4647
readonly sessionFiles: SessionFilesClient;
48+
readonly connections: ConnectionsClient;
4749

4850
constructor(options: EverrunsOptions = {}) {
4951
if (options.apiKey instanceof ApiKey) {
@@ -66,6 +68,7 @@ export class Everruns {
6668
this.events = new EventsClient(this);
6769
this.capabilities = new CapabilitiesClient(this);
6870
this.sessionFiles = new SessionFilesClient(this);
71+
this.connections = new ConnectionsClient(this);
6972
}
7073

7174
/**
@@ -578,6 +581,33 @@ class SessionFilesClient {
578581
}
579582
}
580583

584+
class ConnectionsClient {
585+
constructor(private readonly client: Everruns) {}
586+
587+
/** Set an API key connection for a provider. */
588+
async set(provider: string, apiKey: string): Promise<Connection> {
589+
return this.client.fetch(`/user/connections/${provider}`, {
590+
method: "POST",
591+
body: JSON.stringify({ api_key: apiKey }),
592+
});
593+
}
594+
595+
/** List all connections. */
596+
async list(): Promise<Connection[]> {
597+
const response = await this.client.fetch<{ data: Connection[] }>(
598+
"/user/connections",
599+
);
600+
return response.data;
601+
}
602+
603+
/** Remove a connection. */
604+
async remove(provider: string): Promise<void> {
605+
await this.client.fetch(`/user/connections/${provider}`, {
606+
method: "DELETE",
607+
});
608+
}
609+
}
610+
581611
/** Build the JSON body for agent creation from a CreateAgentRequest. */
582612
function toAgentBody(request: CreateAgentRequest): Record<string, unknown> {
583613
const body: Record<string, unknown> = {

0 commit comments

Comments
 (0)