Skip to content

Commit 708fba3

Browse files
authored
update listDaemons to return a structure (#17)
* update listDaemons to return a structure Signed-off-by: kerthcet <kerthcet@gmail.com> * fix lint Signed-off-by: kerthcet <kerthcet@gmail.com> * optimize the vec initialization Signed-off-by: kerthcet <kerthcet@gmail.com> --------- Signed-off-by: kerthcet <kerthcet@gmail.com>
1 parent 0315231 commit 708fba3

9 files changed

Lines changed: 132 additions & 44 deletions

File tree

examples/exec_commands.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
print(f"\rConnected: {stats.total_daemons} | Platforms: {stats.by_platform}", end="", flush=True)
2121

2222
if daemons and len(daemons) > 0:
23-
for daemon_id in daemons:
23+
for daemon in daemons:
24+
daemon_id = daemon.id
2425
try:
2526
# Test 1: Python script
2627
result = server.exec(

examples/install_htop.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,9 @@ def main():
9090
time.sleep(1)
9191
daemons = server.list_daemons()
9292

93-
daemon_id = daemons[0]
94-
print(f"✓ Found daemon: {daemon_id}\n")
93+
daemon = daemons[0]
94+
daemon_id = daemon.id
95+
print(f"✓ Found daemon: {daemon_id} (version={daemon.version})\n")
9596

9697
# Check if htop is available
9798
print("Checking if htop is available...")

examples/interactive_session.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@ def main():
2929
time.sleep(1)
3030
daemons = server.list_daemons()
3131

32-
daemon_id = daemons[0]
33-
print(f"✓ Found daemon: {daemon_id}\n")
32+
daemon = daemons[0]
33+
print(f"✓ Found daemon: {daemon.id} (version={daemon.version})\n")
3434

3535
print("Starting interactive terminal...")
3636
print()
3737

3838
# Start session in interactive mode - this blocks until user exits
39-
server.new_session(daemon_id, interactive=True)
39+
server.new_session(daemon.id, interactive=True)
4040

4141

4242
if __name__ == "__main__":

python/sandd/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
... )
4040
"""
4141

42-
from .models import CommandResult, ServerStats
42+
from .models import CommandResult, ServerStats, DaemonInfo
4343
from .server import Server
4444
from .async_server import AsyncServer
4545

@@ -57,4 +57,5 @@
5757
"Session",
5858
"CommandResult",
5959
"ServerStats",
60+
"DaemonInfo",
6061
]

python/sandd/models.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,35 @@ def __repr__(self) -> str:
5858
)
5959

6060

61+
class DaemonInfo:
62+
"""Information about a connected daemon
63+
64+
Attributes:
65+
id: Daemon identifier
66+
version: Daemon version string
67+
labels: Key-value labels for filtering
68+
is_busy: Whether daemon has pending commands
69+
"""
70+
71+
def __init__(
72+
self,
73+
id: str,
74+
version: str,
75+
labels: Dict[str, str],
76+
is_busy: bool,
77+
):
78+
self.id = id
79+
self.version = version
80+
self.labels = labels
81+
self.is_busy = is_busy
82+
83+
def __repr__(self) -> str:
84+
return (
85+
f"DaemonInfo(id={self.id!r}, version={self.version!r}, "
86+
f"labels={self.labels}, is_busy={self.is_busy})"
87+
)
88+
89+
6190
class ServerStats:
6291
"""Server statistics
6392

python/sandd/server.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import sys
66
import select
77

8-
from .models import CommandResult, ServerStats
8+
from .models import CommandResult, ServerStats, DaemonInfo
99

1010
try:
1111
from ._core import Server as _RustServer, Session
@@ -185,30 +185,41 @@ def download_file(
185185
def list_daemons(
186186
self,
187187
labels: Optional[Dict[str, str]] = None,
188-
) -> List[str]:
189-
"""List all connected daemon IDs, optionally filtered by labels
188+
) -> List[DaemonInfo]:
189+
"""List all connected daemons with their information, optionally filtered by labels
190190
191191
Args:
192192
labels: Dictionary of label key-value pairs to filter by (AND logic)
193193
All specified labels must match for a daemon to be included
194194
195195
Returns:
196-
List of daemon IDs
196+
List of DaemonInfo objects
197197
198198
Example:
199199
>>> # List all daemons
200200
>>> daemons = server.list_daemons()
201201
>>> print(f"Connected: {len(daemons)} daemons")
202+
>>> for daemon in daemons:
203+
... print(f" {daemon.id}: {daemon.version}, busy={daemon.is_busy}")
202204
>>>
203205
>>> # List daemons with single label
204206
>>> prod_daemons = server.list_daemons(labels={"env": "prod"})
205207
>>>
206208
>>> # List daemons with multiple labels (AND logic)
207209
>>> west_prod = server.list_daemons(labels={"env": "prod", "region": "us-west"})
208-
>>> for daemon_id in west_prod:
209-
... print(f" - {daemon_id}")
210+
>>> for daemon in west_prod:
211+
... print(f" - {daemon.id} ({daemon.labels})")
210212
"""
211-
return self._server.list_daemons(labels)
213+
py_infos = self._server.list_daemons(labels)
214+
return [
215+
DaemonInfo(
216+
id=info.id,
217+
version=info.version,
218+
labels=info.labels,
219+
is_busy=info.is_busy,
220+
)
221+
for info in py_infos
222+
]
212223

213224
def daemon_count(self) -> int:
214225
"""Get number of connected daemons
@@ -339,10 +350,12 @@ def broadcast(
339350
import concurrent.futures
340351

341352
# Get matching daemons
342-
daemon_ids = self.list_daemons(labels=labels)
343-
if not daemon_ids:
353+
daemons = self.list_daemons(labels=labels)
354+
if not daemons:
344355
return {}
345356

357+
daemon_ids = [d.id for d in daemons]
358+
346359
# Execute command on all daemons concurrently
347360
def run_command(daemon_id):
348361
try:
@@ -390,7 +403,8 @@ def wait_for_daemon(
390403
"""
391404
start = time.time()
392405
while time.time() - start < timeout:
393-
if daemon_id in self.list_daemons():
406+
daemons = self.list_daemons()
407+
if any(d.id == daemon_id for d in daemons):
394408
return True
395409
time.sleep(poll_interval)
396410
return False

python/tests/test_e2e.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,14 @@ class TestE2EBasicOperations:
6060
def test_all_daemons_connected(self, server):
6161
"""Verify all 6 daemons connected (2 debian + 2 alpine + 2 rocky)"""
6262
daemons = server.list_daemons()
63+
daemon_ids = [d.id for d in daemons]
6364
expected = [
6465
"daemon-debian-1", "daemon-debian-2",
6566
"daemon-alpine-1", "daemon-alpine-2",
6667
"daemon-rocky-1", "daemon-rocky-2"
6768
]
6869
for daemon_id in expected:
69-
assert daemon_id in daemons
70+
assert daemon_id in daemon_ids
7071
assert server.daemon_count() >= 6
7172

7273
def test_execute_on_each_daemon(self, server):
@@ -224,29 +225,34 @@ class TestE2ELabels:
224225
def test_filter_by_env_label(self, server):
225226
"""Filter daemons by env label"""
226227
test_daemons = server.list_daemons(labels={"env": "test"})
227-
assert "daemon-debian-1" in test_daemons
228-
assert "daemon-debian-2" in test_daemons
229-
assert "daemon-alpine-1" in test_daemons
230-
assert "daemon-rocky-2" in test_daemons
228+
test_ids = [d.id for d in test_daemons]
229+
assert "daemon-debian-1" in test_ids
230+
assert "daemon-debian-2" in test_ids
231+
assert "daemon-alpine-1" in test_ids
232+
assert "daemon-rocky-2" in test_ids
231233

232234
prod_daemons = server.list_daemons(labels={"env": "prod"})
233-
assert "daemon-alpine-2" in prod_daemons
234-
assert "daemon-rocky-1" in prod_daemons
235+
prod_ids = [d.id for d in prod_daemons]
236+
assert "daemon-alpine-2" in prod_ids
237+
assert "daemon-rocky-1" in prod_ids
235238

236239
def test_filter_by_distro_label(self, server):
237240
"""Filter daemons by distribution"""
238241
debian_daemons = server.list_daemons(labels={"distro": "debian"})
239-
assert "daemon-debian-1" in debian_daemons
240-
assert "daemon-debian-2" in debian_daemons
242+
debian_ids = [d.id for d in debian_daemons]
243+
assert "daemon-debian-1" in debian_ids
244+
assert "daemon-debian-2" in debian_ids
241245
assert len(debian_daemons) >= 2
242246

243247
alpine_daemons = server.list_daemons(labels={"distro": "alpine"})
244-
assert "daemon-alpine-1" in alpine_daemons
245-
assert "daemon-alpine-2" in alpine_daemons
248+
alpine_ids = [d.id for d in alpine_daemons]
249+
assert "daemon-alpine-1" in alpine_ids
250+
assert "daemon-alpine-2" in alpine_ids
246251

247252
rocky_daemons = server.list_daemons(labels={"distro": "rocky"})
248-
assert "daemon-rocky-1" in rocky_daemons
249-
assert "daemon-rocky-2" in rocky_daemons
253+
rocky_ids = [d.id for d in rocky_daemons]
254+
assert "daemon-rocky-1" in rocky_ids
255+
assert "daemon-rocky-2" in rocky_ids
250256

251257

252258
class TestE2EResilience:

python/tests/test_integration.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ def test_daemon_connects(self, server, daemon_process):
7979

8080
# Verify daemon is in the list
8181
daemons = server.list_daemons()
82-
assert daemon_id in daemons
82+
daemon_ids = [d.id for d in daemons]
83+
assert daemon_id in daemon_ids
8384

8485
# Verify daemon count
8586
assert server.daemon_count() == 1
@@ -109,9 +110,10 @@ def test_multiple_daemons_connect(self, server, sandd_binary):
109110

110111
# Verify all connected
111112
daemons = server.list_daemons()
113+
daemon_id_list = [d.id for d in daemons]
112114
assert server.daemon_count() == 3
113115
for daemon_id in daemon_ids:
114-
assert daemon_id in daemons
116+
assert daemon_id in daemon_id_list
115117

116118
finally:
117119
# Cleanup all daemons
@@ -160,29 +162,34 @@ def test_daemon_with_labels(self, server, sandd_binary):
160162

161163
# Test: list all daemons (no filter)
162164
all_daemons = server.list_daemons()
163-
assert daemon_id_prod in all_daemons
164-
assert daemon_id_dev in all_daemons
165+
all_ids = [d.id for d in all_daemons]
166+
assert daemon_id_prod in all_ids
167+
assert daemon_id_dev in all_ids
165168
assert len(all_daemons) >= 2
166169

167170
# Test: filter by env=prod
168171
prod_daemons = server.list_daemons(labels={"env": "prod"})
169-
assert daemon_id_prod in prod_daemons
170-
assert daemon_id_dev not in prod_daemons
172+
prod_ids = [d.id for d in prod_daemons]
173+
assert daemon_id_prod in prod_ids
174+
assert daemon_id_dev not in prod_ids
171175

172176
# Test: filter by env=dev
173177
dev_daemons = server.list_daemons(labels={"env": "dev"})
174-
assert daemon_id_dev in dev_daemons
175-
assert daemon_id_prod not in dev_daemons
178+
dev_ids = [d.id for d in dev_daemons]
179+
assert daemon_id_dev in dev_ids
180+
assert daemon_id_prod not in dev_ids
176181

177182
# Test: filter by region=us-west
178183
region_daemons = server.list_daemons(labels={"region": "us-west"})
179-
assert daemon_id_prod in region_daemons
180-
assert daemon_id_dev not in region_daemons
184+
region_ids = [d.id for d in region_daemons]
185+
assert daemon_id_prod in region_ids
186+
assert daemon_id_dev not in region_ids
181187

182188
# Test: filter by multiple labels (AND logic)
183189
west_prod = server.list_daemons(labels={"env": "prod", "region": "us-west"})
184-
assert daemon_id_prod in west_prod
185-
assert daemon_id_dev not in west_prod
190+
west_prod_ids = [d.id for d in west_prod]
191+
assert daemon_id_prod in west_prod_ids
192+
assert daemon_id_dev not in west_prod_ids
186193

187194
# Test: filter by non-existent label
188195
none_daemons = server.list_daemons(labels={"env": "staging"})

server/src/lib.rs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,22 @@ impl Server {
225225

226226
/// List all connected daemons, optionally filtered by labels
227227
#[pyo3(signature = (labels=None))]
228-
fn list_daemons(&self, labels: Option<HashMap<String, String>>) -> PyResult<Vec<String>> {
229-
Ok(self.registry.list_all(labels.as_ref()))
228+
fn list_daemons(&self, labels: Option<HashMap<String, String>>) -> PyResult<Vec<PyDaemonInfo>> {
229+
let daemon_ids = self.registry.list_all(labels.as_ref());
230+
let mut result = Vec::with_capacity(daemon_ids.len());
231+
232+
for daemon_id in daemon_ids {
233+
if let Some(conn) = self.registry.get(&daemon_id) {
234+
result.push(PyDaemonInfo {
235+
id: conn.id.clone(),
236+
version: conn.metadata.version.clone(),
237+
labels: conn.metadata.labels.clone(),
238+
is_busy: conn.is_busy(),
239+
});
240+
}
241+
}
242+
243+
Ok(result)
230244
}
231245

232246
/// Get daemon count
@@ -325,6 +339,20 @@ impl Session {
325339
}
326340
}
327341

342+
/// Daemon information
343+
#[pyclass]
344+
#[derive(Clone)]
345+
pub struct PyDaemonInfo {
346+
#[pyo3(get)]
347+
pub id: String,
348+
#[pyo3(get)]
349+
pub version: String,
350+
#[pyo3(get)]
351+
pub labels: HashMap<String, String>,
352+
#[pyo3(get)]
353+
pub is_busy: bool,
354+
}
355+
328356
/// Command execution result
329357
#[pyclass]
330358
#[derive(Clone)]
@@ -370,6 +398,7 @@ fn _core(_py: Python, m: &PyModule) -> PyResult<()> {
370398
m.add_class::<Server>()?;
371399
m.add_class::<Session>()?;
372400
m.add_class::<PyCommandResult>()?;
401+
m.add_class::<PyDaemonInfo>()?;
373402
m.add_class::<PyStats>()?;
374403
Ok(())
375404
}

0 commit comments

Comments
 (0)