hubcio commented on code in PR #4020:
URL: https://github.com/apache/iggy/pull/4020#discussion_r3922437659


##########
foreign/python/Cargo.toml:
##########
@@ -38,6 +38,7 @@ doc = false
 bytes = "1.12.1"
 futures = "0.3.34"
 iggy = { path = "../../core/sdk", version = "0.11.0-edge.6" }
+iggy_common = { path = "../../core/common", version = "0.11.0-edge.6" }

Review Comment:
   drop this dep and take the two types from `iggy::prelude` instead - see the 
import comment. it also carries a pin nothing bumps: `bump-version.sh`'s 
`rust-common` case only touches `core/common/Cargo.toml` and the root manifest, 
so a minor bump breaks the wheel build.



##########
foreign/python/tests/test_client_info.py:
##########
@@ -0,0 +1,111 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from apache_iggy import ClientInfo, ClientInfoDetails, IggyClient
+
+
+class TestGetMe:
+    """Test get_me returns the currently connected client."""
+
+    @pytest.mark.asyncio
+    async def test_get_me_returns_client_info_details(self, iggy_client: 
IggyClient):
+        """get_me() should return a ClientInfoDetails with valid fields."""
+        me = await iggy_client.get_me()
+        assert isinstance(me, ClientInfoDetails)
+        assert isinstance(me.client_id, int)
+        assert me.client_id > 0
+        assert isinstance(me.address, str)
+        assert len(me.address) > 0
+        assert isinstance(me.transport, str)

Review Comment:
   also at line 74.
   
   `transport` is only checked for being a string. the fixture is always tcp, 
so `assert me.transport == "TCP"` is free and would catch a wire-code shift 
that turns every client into `"Unknown"`.



##########
foreign/python/tests/test_client_info.py:
##########
@@ -0,0 +1,111 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from apache_iggy import ClientInfo, ClientInfoDetails, IggyClient
+
+
+class TestGetMe:
+    """Test get_me returns the currently connected client."""
+
+    @pytest.mark.asyncio
+    async def test_get_me_returns_client_info_details(self, iggy_client: 
IggyClient):
+        """get_me() should return a ClientInfoDetails with valid fields."""
+        me = await iggy_client.get_me()
+        assert isinstance(me, ClientInfoDetails)
+        assert isinstance(me.client_id, int)
+        assert me.client_id > 0
+        assert isinstance(me.address, str)
+        assert len(me.address) > 0
+        assert isinstance(me.transport, str)
+        assert isinstance(me.consumer_groups_count, int)
+        assert isinstance(me.consumer_groups, list)

Review Comment:
   the fixture never joins a group, so this list is always empty and 
`ConsumerGroupInfo` plus the field mapping in `client_info.rs` never run. join 
a group first and assert the ids, otherwise a `stream_id`/`topic_id` swap ships 
green.



##########
foreign/python/tests/test_client_info.py:
##########
@@ -0,0 +1,111 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from apache_iggy import ClientInfo, ClientInfoDetails, IggyClient
+
+
+class TestGetMe:
+    """Test get_me returns the currently connected client."""
+
+    @pytest.mark.asyncio
+    async def test_get_me_returns_client_info_details(self, iggy_client: 
IggyClient):
+        """get_me() should return a ClientInfoDetails with valid fields."""
+        me = await iggy_client.get_me()
+        assert isinstance(me, ClientInfoDetails)
+        assert isinstance(me.client_id, int)
+        assert me.client_id > 0
+        assert isinstance(me.address, str)
+        assert len(me.address) > 0
+        assert isinstance(me.transport, str)
+        assert isinstance(me.consumer_groups_count, int)
+        assert isinstance(me.consumer_groups, list)
+
+    @pytest.mark.asyncio
+    async def test_get_me_user_id_matches_logged_in_user(self, iggy_client: 
IggyClient):

Review Comment:
   the name says it matches the logged-in user but nothing compares it - root's 
id is 0, so `Some(0)` vs any other id is untested. compare against `(await 
iggy_client.get_user("iggy")).id`, or rename.



##########
foreign/python/tests/test_client_info.py:
##########
@@ -0,0 +1,111 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from apache_iggy import ClientInfo, ClientInfoDetails, IggyClient
+
+
+class TestGetMe:
+    """Test get_me returns the currently connected client."""
+
+    @pytest.mark.asyncio
+    async def test_get_me_returns_client_info_details(self, iggy_client: 
IggyClient):
+        """get_me() should return a ClientInfoDetails with valid fields."""
+        me = await iggy_client.get_me()
+        assert isinstance(me, ClientInfoDetails)
+        assert isinstance(me.client_id, int)
+        assert me.client_id > 0
+        assert isinstance(me.address, str)
+        assert len(me.address) > 0
+        assert isinstance(me.transport, str)
+        assert isinstance(me.consumer_groups_count, int)
+        assert isinstance(me.consumer_groups, list)
+
+    @pytest.mark.asyncio
+    async def test_get_me_user_id_matches_logged_in_user(self, iggy_client: 
IggyClient):
+        """get_me().user_id should be set after login."""
+        me = await iggy_client.get_me()
+        assert me.user_id is not None
+        assert isinstance(me.user_id, int)
+
+
+class TestGetClients:
+    """Test get_clients returns all connected clients."""
+
+    @pytest.mark.asyncio
+    async def test_get_clients_returns_list(self, iggy_client: IggyClient):
+        """get_clients() should return a non-empty list of ClientInfo."""
+        clients = await iggy_client.get_clients()
+        assert isinstance(clients, list)
+        assert len(clients) >= 1
+
+    @pytest.mark.asyncio
+    async def test_get_clients_contains_current_client(self, iggy_client: 
IggyClient):
+        """The current client (from get_me) should appear in get_clients()."""
+        me = await iggy_client.get_me()
+        clients = await iggy_client.get_clients()
+        client_ids = [c.client_id for c in clients]
+        assert me.client_id in client_ids
+
+    @pytest.mark.asyncio
+    async def test_get_clients_items_are_client_info(self, iggy_client: 
IggyClient):
+        """Each item in get_clients() should be a ClientInfo with valid 
fields."""
+        clients = await iggy_client.get_clients()
+        for client in clients:
+            assert isinstance(client, ClientInfo)
+            assert isinstance(client.client_id, int)
+            assert client.client_id > 0
+            assert isinstance(client.address, str)
+            assert isinstance(client.transport, str)
+            assert isinstance(client.consumer_groups_count, int)
+
+
+class TestGetClient:
+    """Test get_client returns details for a specific client."""
+
+    @pytest.mark.asyncio
+    async def test_get_client_returns_client_info_details(
+        self, iggy_client: IggyClient
+    ):
+        """get_client() with a valid id should return ClientInfoDetails."""
+        me = await iggy_client.get_me()
+        client = await iggy_client.get_client(me.client_id)
+        assert client is not None
+        assert isinstance(client, ClientInfoDetails)
+        assert client.client_id == me.client_id
+
+    @pytest.mark.asyncio
+    async def test_get_client_unknown_id_returns_none(self, iggy_client: 
IggyClient):
+        """get_client() with an unknown id should return None."""
+        client = await iggy_client.get_client(999999)
+        assert client is None
+
+    @pytest.mark.asyncio
+    async def test_get_client_details_match_get_clients(self, iggy_client: 
IggyClient):
+        """get_client() fields should match the corresponding get_clients() 
entry."""
+        me = await iggy_client.get_me()
+        clients = await iggy_client.get_clients()
+        summary = next(c for c in clients if c.client_id == me.client_id)

Review Comment:
   bare `next()` raises `StopIteration` inside an async test, which surfaces as 
an error with no message instead of a failed assert. use `next(..., None)` and 
assert it's not none.



##########
foreign/python/tests/test_client_info.py:
##########
@@ -0,0 +1,111 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from apache_iggy import ClientInfo, ClientInfoDetails, IggyClient
+
+
+class TestGetMe:
+    """Test get_me returns the currently connected client."""
+
+    @pytest.mark.asyncio
+    async def test_get_me_returns_client_info_details(self, iggy_client: 
IggyClient):
+        """get_me() should return a ClientInfoDetails with valid fields."""
+        me = await iggy_client.get_me()
+        assert isinstance(me, ClientInfoDetails)
+        assert isinstance(me.client_id, int)
+        assert me.client_id > 0
+        assert isinstance(me.address, str)
+        assert len(me.address) > 0
+        assert isinstance(me.transport, str)
+        assert isinstance(me.consumer_groups_count, int)
+        assert isinstance(me.consumer_groups, list)
+
+    @pytest.mark.asyncio
+    async def test_get_me_user_id_matches_logged_in_user(self, iggy_client: 
IggyClient):
+        """get_me().user_id should be set after login."""
+        me = await iggy_client.get_me()
+        assert me.user_id is not None
+        assert isinstance(me.user_id, int)
+
+
+class TestGetClients:
+    """Test get_clients returns all connected clients."""
+
+    @pytest.mark.asyncio
+    async def test_get_clients_returns_list(self, iggy_client: IggyClient):
+        """get_clients() should return a non-empty list of ClientInfo."""
+        clients = await iggy_client.get_clients()
+        assert isinstance(clients, list)
+        assert len(clients) >= 1
+
+    @pytest.mark.asyncio
+    async def test_get_clients_contains_current_client(self, iggy_client: 
IggyClient):
+        """The current client (from get_me) should appear in get_clients()."""
+        me = await iggy_client.get_me()
+        clients = await iggy_client.get_clients()
+        client_ids = [c.client_id for c in clients]
+        assert me.client_id in client_ids

Review Comment:
   also at lines 88, 103.
   
   `get_clients` is best-effort - a shard that misses the 3s gather budget gets 
dropped from the result. fine for a single-node test server, but worth a 
comment saying why completeness holds here.



##########
foreign/python/src/client_info.rs:
##########
@@ -0,0 +1,166 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use iggy::prelude::ClientInfoDetails as RustClientInfoDetails;
+use iggy_common::{ClientInfo as RustClientInfo, ConsumerGroupInfo as 
RustConsumerGroupInfo};
+use pyo3::prelude::*;
+use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods};
+
+/// Information about a consumer group that a client is part of.
+#[gen_stub_pyclass]
+#[pyclass]
+pub struct ConsumerGroupInfo {
+    pub(crate) inner: RustConsumerGroupInfo,
+}
+
+impl From<RustConsumerGroupInfo> for ConsumerGroupInfo {
+    fn from(cg: RustConsumerGroupInfo) -> Self {
+        Self { inner: cg }
+    }
+}
+
+#[gen_stub_pymethods]
+#[pymethods]
+impl ConsumerGroupInfo {
+    /// The unique identifier (numeric) of the stream.
+    #[getter]
+    pub fn stream_id(&self) -> u32 {
+        self.inner.stream_id
+    }
+
+    /// The unique identifier (numeric) of the topic.
+    #[getter]
+    pub fn topic_id(&self) -> u32 {
+        self.inner.topic_id
+    }
+
+    /// The unique identifier (numeric) of the consumer group.
+    #[getter]
+    pub fn group_id(&self) -> u32 {
+        self.inner.group_id
+    }
+}
+
+/// Summary information about a connected client.
+#[gen_stub_pyclass]
+#[pyclass]
+pub struct ClientInfo {
+    pub(crate) inner: RustClientInfo,
+}
+
+impl From<RustClientInfo> for ClientInfo {
+    fn from(client: RustClientInfo) -> Self {
+        Self { inner: client }
+    }
+}
+
+#[gen_stub_pymethods]
+#[pymethods]
+impl ClientInfo {
+    /// The unique identifier of the client.
+    #[getter]
+    pub fn client_id(&self) -> u32 {
+        self.inner.client_id
+    }
+
+    /// The unique identifier of the authenticated user, or `None` if not 
authenticated.
+    #[getter]
+    pub fn user_id(&self) -> Option<u32> {
+        self.inner.user_id
+    }
+
+    /// The remote address of the client.
+    #[getter]
+    pub fn address(&self) -> String {
+        self.inner.address.clone()
+    }
+
+    /// The transport protocol used by the client.
+    #[getter]
+    pub fn transport(&self) -> String {
+        self.inner.transport.clone()
+    }
+
+    /// The number of consumer groups the client is part of.
+    #[getter]
+    pub fn consumer_groups_count(&self) -> u32 {
+        self.inner.consumer_groups_count
+    }
+}
+
+/// Detailed information about a connected client, including its consumer 
groups.
+#[gen_stub_pyclass]
+#[pyclass]
+pub struct ClientInfoDetails {
+    pub(crate) inner: RustClientInfoDetails,
+}
+
+impl From<RustClientInfoDetails> for ClientInfoDetails {
+    fn from(client: RustClientInfoDetails) -> Self {
+        Self { inner: client }
+    }
+}
+
+#[gen_stub_pymethods]
+#[pymethods]
+impl ClientInfoDetails {
+    /// The unique identifier of the client.
+    #[getter]
+    pub fn client_id(&self) -> u32 {
+        self.inner.client_id
+    }
+
+    /// The unique identifier of the authenticated user, or `None` if not 
authenticated.
+    #[getter]
+    pub fn user_id(&self) -> Option<u32> {
+        self.inner.user_id
+    }
+
+    /// The remote address of the client.
+    #[getter]
+    pub fn address(&self) -> String {
+        self.inner.address.clone()
+    }
+
+    /// The transport protocol used by the client.
+    #[getter]
+    pub fn transport(&self) -> String {
+        self.inner.transport.clone()
+    }
+
+    /// The number of consumer groups the client is part of.
+    #[getter]
+    pub fn consumer_groups_count(&self) -> u32 {
+        self.inner.consumer_groups_count
+    }
+
+    /// The consumer groups this client is part of.
+    #[getter]
+    pub fn consumer_groups(&self) -> Vec<ConsumerGroupInfo> {

Review Comment:
   this rebuilds the list and allocates a fresh object per group on every 
attribute read, so `d.consumer_groups[0] is d.consumer_groups[0]` is false and 
`.append()` on it does nothing. worth saying in the docstring that callers 
should bind it once.



##########
foreign/python/src/client_info.rs:
##########
@@ -0,0 +1,166 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use iggy::prelude::ClientInfoDetails as RustClientInfoDetails;
+use iggy_common::{ClientInfo as RustClientInfo, ConsumerGroupInfo as 
RustConsumerGroupInfo};
+use pyo3::prelude::*;
+use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods};
+
+/// Information about a consumer group that a client is part of.
+#[gen_stub_pyclass]
+#[pyclass]
+pub struct ConsumerGroupInfo {
+    pub(crate) inner: RustConsumerGroupInfo,
+}
+
+impl From<RustConsumerGroupInfo> for ConsumerGroupInfo {
+    fn from(cg: RustConsumerGroupInfo) -> Self {
+        Self { inner: cg }
+    }
+}
+
+#[gen_stub_pymethods]
+#[pymethods]
+impl ConsumerGroupInfo {
+    /// The unique identifier (numeric) of the stream.
+    #[getter]
+    pub fn stream_id(&self) -> u32 {
+        self.inner.stream_id
+    }
+
+    /// The unique identifier (numeric) of the topic.
+    #[getter]
+    pub fn topic_id(&self) -> u32 {
+        self.inner.topic_id
+    }
+
+    /// The unique identifier (numeric) of the consumer group.
+    #[getter]
+    pub fn group_id(&self) -> u32 {
+        self.inner.group_id
+    }
+}
+
+/// Summary information about a connected client.
+#[gen_stub_pyclass]
+#[pyclass]
+pub struct ClientInfo {
+    pub(crate) inner: RustClientInfo,
+}
+
+impl From<RustClientInfo> for ClientInfo {
+    fn from(client: RustClientInfo) -> Self {
+        Self { inner: client }
+    }
+}
+
+#[gen_stub_pymethods]
+#[pymethods]
+impl ClientInfo {
+    /// The unique identifier of the client.
+    #[getter]
+    pub fn client_id(&self) -> u32 {
+        self.inner.client_id
+    }
+
+    /// The unique identifier of the authenticated user, or `None` if not 
authenticated.
+    #[getter]
+    pub fn user_id(&self) -> Option<u32> {
+        self.inner.user_id
+    }
+
+    /// The remote address of the client.
+    #[getter]
+    pub fn address(&self) -> String {
+        self.inner.address.clone()
+    }
+
+    /// The transport protocol used by the client.
+    #[getter]
+    pub fn transport(&self) -> String {

Review Comment:
   also at lines 88, 133, 140.
   
   these clone a `String` that pyo3 immediately copies into a `PyString` and 
drops. returning `&self.inner.transport` works on a getter and renders the same 
in the stub.



##########
foreign/python/tests/test_client_info.py:
##########
@@ -0,0 +1,111 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from apache_iggy import ClientInfo, ClientInfoDetails, IggyClient
+
+
+class TestGetMe:
+    """Test get_me returns the currently connected client."""
+
+    @pytest.mark.asyncio
+    async def test_get_me_returns_client_info_details(self, iggy_client: 
IggyClient):
+        """get_me() should return a ClientInfoDetails with valid fields."""
+        me = await iggy_client.get_me()
+        assert isinstance(me, ClientInfoDetails)
+        assert isinstance(me.client_id, int)

Review Comment:
   also at lines 33, 36, 44, 71, 73, 75.
   
   these check pyo3's type system, not the sdk - a `#[getter] -> u32` can't 
return a non-int. drop them and keep the value assertions plus the class checks.



##########
foreign/python/src/client.rs:
##########
@@ -450,6 +451,70 @@ impl IggyClient {
         })
     }
 
+    /// Returns information about the currently connected client.
+    ///
+    /// Returns:
+    ///     An awaitable that resolves to `ClientInfoDetails`.
+    ///
+    /// Raises:
+    ///     RuntimeError: If the request fails.

Review Comment:
   over an http connection this always fails - `get_me` is unimplemented in the 
http client and returns `FeatureUnavailable` unconditionally. worth saying so 
here, since `get_client` and `get_clients` do work over http.



##########
foreign/python/src/client.rs:
##########
@@ -450,6 +451,70 @@ impl IggyClient {
         })
     }
 
+    /// Returns information about the currently connected client.
+    ///
+    /// Returns:
+    ///     An awaitable that resolves to `ClientInfoDetails`.
+    ///
+    /// Raises:
+    ///     RuntimeError: If the request fails.
+    
#[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[ClientInfoDetails]",
 imports=("collections.abc")))]
+    fn get_me<'a>(&self, py: Python<'a>) -> PyResult<Bound<'a, PyAny>> {
+        let inner = self.inner.clone();
+        future_into_py(py, async move {
+            let client = inner
+                .get_me()
+                .await
+                .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, 
_>(e.to_string()))?;
+            Ok(PyClientInfoDetails::from(client))
+        })
+    }
+
+    /// Returns information about a specific client by its ID.
+    ///
+    /// Args:
+    ///     client_id: The unique identifier of the client.
+    ///
+    /// Returns:
+    ///     An awaitable that resolves to `ClientInfoDetails | None`.
+    ///     Returns `None` when the id is unknown.
+    ///
+    /// Raises:
+    ///     RuntimeError: If the request fails.
+    
#[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[ClientInfoDetails
 | None]", imports=("collections.abc")))]
+    fn get_client<'a>(&self, py: Python<'a>, client_id: u32) -> 
PyResult<Bound<'a, PyAny>> {
+        let inner = self.inner.clone();
+        future_into_py(py, async move {
+            let client = inner
+                .get_client(client_id)
+                .await
+                .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, 
_>(e.to_string()))?;
+            Ok(client.map(PyClientInfoDetails::from))
+        })
+    }
+
+    /// Returns information about all connected clients.
+    ///
+    /// Returns:
+    ///     An awaitable that resolves to `list[ClientInfo]`.
+    ///
+    /// Raises:
+    ///     RuntimeError: If the request fails.

Review Comment:
   also at line 483.
   
   `get_clients` and `get_client` need `manage_servers` or `read_servers` and 
return `Unauthorized` without them, but the docstring only mentions a generic 
failure. name the permission and add a test like 
`tests/test_permissions.py:267`.



##########
foreign/python/src/client.rs:
##########
@@ -450,6 +451,70 @@ impl IggyClient {
         })
     }
 
+    /// Returns information about the currently connected client.
+    ///
+    /// Returns:
+    ///     An awaitable that resolves to `ClientInfoDetails`.
+    ///
+    /// Raises:
+    ///     RuntimeError: If the request fails.
+    
#[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[ClientInfoDetails]",
 imports=("collections.abc")))]
+    fn get_me<'a>(&self, py: Python<'a>) -> PyResult<Bound<'a, PyAny>> {
+        let inner = self.inner.clone();
+        future_into_py(py, async move {
+            let client = inner
+                .get_me()
+                .await
+                .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, 
_>(e.to_string()))?;
+            Ok(PyClientInfoDetails::from(client))
+        })
+    }
+
+    /// Returns information about a specific client by its ID.
+    ///
+    /// Args:
+    ///     client_id: The unique identifier of the client.
+    ///
+    /// Returns:
+    ///     An awaitable that resolves to `ClientInfoDetails | None`.
+    ///     Returns `None` when the id is unknown.
+    ///
+    /// Raises:
+    ///     RuntimeError: If the request fails.

Review Comment:
   `client_id: u32` is converted before the coroutine exists, so 
`get_client(-1)` raises `OverflowError`, not `RuntimeError`. document it and 
add a parametrized test like `tests/test_permissions.py:148` - not the 
`get_user` one, that asserts `TypeError`.



##########
foreign/python/src/client_info.rs:
##########
@@ -0,0 +1,166 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use iggy::prelude::ClientInfoDetails as RustClientInfoDetails;
+use iggy_common::{ClientInfo as RustClientInfo, ConsumerGroupInfo as 
RustConsumerGroupInfo};
+use pyo3::prelude::*;
+use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods};
+
+/// Information about a consumer group that a client is part of.
+#[gen_stub_pyclass]
+#[pyclass]
+pub struct ConsumerGroupInfo {
+    pub(crate) inner: RustConsumerGroupInfo,
+}
+
+impl From<RustConsumerGroupInfo> for ConsumerGroupInfo {

Review Comment:
   nothing calls this - the `consumer_groups` getter builds the struct by hand 
at line 157. make it `From<&RustConsumerGroupInfo>` like `consumer.rs:342` and 
the getter collapses to `.iter().map(ConsumerGroupInfo::from).collect()`.



##########
foreign/python/src/client_info.rs:
##########
@@ -0,0 +1,166 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use iggy::prelude::ClientInfoDetails as RustClientInfoDetails;
+use iggy_common::{ClientInfo as RustClientInfo, ConsumerGroupInfo as 
RustConsumerGroupInfo};

Review Comment:
   `iggy::prelude` re-exports `ClientInfoDetails` but not `ClientInfo` or 
`ConsumerGroupInfo`, even though it exports `SystemClient::get_clients` which 
returns `Vec<ClientInfo>`. please add both names to `core/sdk/src/prelude.rs` 
and import all three from the prelude rather than working around the gap with a 
new dependency.



##########
foreign/python/tests/test_client_info.py:
##########
@@ -0,0 +1,111 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from apache_iggy import ClientInfo, ClientInfoDetails, IggyClient
+
+
+class TestGetMe:
+    """Test get_me returns the currently connected client."""
+
+    @pytest.mark.asyncio
+    async def test_get_me_returns_client_info_details(self, iggy_client: 
IggyClient):
+        """get_me() should return a ClientInfoDetails with valid fields."""
+        me = await iggy_client.get_me()
+        assert isinstance(me, ClientInfoDetails)
+        assert isinstance(me.client_id, int)
+        assert me.client_id > 0
+        assert isinstance(me.address, str)
+        assert len(me.address) > 0
+        assert isinstance(me.transport, str)
+        assert isinstance(me.consumer_groups_count, int)
+        assert isinstance(me.consumer_groups, list)
+
+    @pytest.mark.asyncio
+    async def test_get_me_user_id_matches_logged_in_user(self, iggy_client: 
IggyClient):
+        """get_me().user_id should be set after login."""
+        me = await iggy_client.get_me()
+        assert me.user_id is not None
+        assert isinstance(me.user_id, int)
+
+
+class TestGetClients:
+    """Test get_clients returns all connected clients."""
+
+    @pytest.mark.asyncio
+    async def test_get_clients_returns_list(self, iggy_client: IggyClient):
+        """get_clients() should return a non-empty list of ClientInfo."""
+        clients = await iggy_client.get_clients()
+        assert isinstance(clients, list)
+        assert len(clients) >= 1
+
+    @pytest.mark.asyncio
+    async def test_get_clients_contains_current_client(self, iggy_client: 
IggyClient):
+        """The current client (from get_me) should appear in get_clients()."""
+        me = await iggy_client.get_me()
+        clients = await iggy_client.get_clients()
+        client_ids = [c.client_id for c in clients]
+        assert me.client_id in client_ids
+
+    @pytest.mark.asyncio
+    async def test_get_clients_items_are_client_info(self, iggy_client: 
IggyClient):
+        """Each item in get_clients() should be a ClientInfo with valid 
fields."""
+        clients = await iggy_client.get_clients()
+        for client in clients:
+            assert isinstance(client, ClientInfo)
+            assert isinstance(client.client_id, int)
+            assert client.client_id > 0
+            assert isinstance(client.address, str)
+            assert isinstance(client.transport, str)
+            assert isinstance(client.consumer_groups_count, int)
+
+
+class TestGetClient:
+    """Test get_client returns details for a specific client."""
+
+    @pytest.mark.asyncio
+    async def test_get_client_returns_client_info_details(
+        self, iggy_client: IggyClient
+    ):
+        """get_client() with a valid id should return ClientInfoDetails."""
+        me = await iggy_client.get_me()
+        client = await iggy_client.get_client(me.client_id)
+        assert client is not None
+        assert isinstance(client, ClientInfoDetails)
+        assert client.client_id == me.client_id
+
+    @pytest.mark.asyncio
+    async def test_get_client_unknown_id_returns_none(self, iggy_client: 
IggyClient):
+        """get_client() with an unknown id should return None."""
+        client = await iggy_client.get_client(999999)

Review Comment:
   give `999999` a name so the reader knows it's meant to be an id that can't 
exist.



##########
foreign/python/src/client.rs:
##########
@@ -450,6 +451,70 @@ impl IggyClient {
         })
     }
 
+    /// Returns information about the currently connected client.
+    ///
+    /// Returns:
+    ///     An awaitable that resolves to `ClientInfoDetails`.
+    ///
+    /// Raises:
+    ///     RuntimeError: If the request fails.
+    
#[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[ClientInfoDetails]",
 imports=("collections.abc")))]
+    fn get_me<'a>(&self, py: Python<'a>) -> PyResult<Bound<'a, PyAny>> {

Review Comment:
   these three sit in the middle of the user-management methods while `ping` 
and `describe_options` are up at the top. moving them is free right now, the 
stub follows definition order.



##########
foreign/python/src/client_info.rs:
##########
@@ -0,0 +1,166 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use iggy::prelude::ClientInfoDetails as RustClientInfoDetails;
+use iggy_common::{ClientInfo as RustClientInfo, ConsumerGroupInfo as 
RustConsumerGroupInfo};
+use pyo3::prelude::*;
+use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods};
+
+/// Information about a consumer group that a client is part of.
+#[gen_stub_pyclass]
+#[pyclass]
+pub struct ConsumerGroupInfo {
+    pub(crate) inner: RustConsumerGroupInfo,
+}
+
+impl From<RustConsumerGroupInfo> for ConsumerGroupInfo {
+    fn from(cg: RustConsumerGroupInfo) -> Self {
+        Self { inner: cg }
+    }
+}
+
+#[gen_stub_pymethods]
+#[pymethods]
+impl ConsumerGroupInfo {
+    /// The unique identifier (numeric) of the stream.
+    #[getter]
+    pub fn stream_id(&self) -> u32 {
+        self.inner.stream_id
+    }
+
+    /// The unique identifier (numeric) of the topic.
+    #[getter]
+    pub fn topic_id(&self) -> u32 {
+        self.inner.topic_id
+    }
+
+    /// The unique identifier (numeric) of the consumer group.
+    #[getter]
+    pub fn group_id(&self) -> u32 {
+        self.inner.group_id
+    }
+}
+
+/// Summary information about a connected client.
+#[gen_stub_pyclass]
+#[pyclass]
+pub struct ClientInfo {
+    pub(crate) inner: RustClientInfo,
+}
+
+impl From<RustClientInfo> for ClientInfo {
+    fn from(client: RustClientInfo) -> Self {
+        Self { inner: client }
+    }
+}
+
+#[gen_stub_pymethods]
+#[pymethods]
+impl ClientInfo {
+    /// The unique identifier of the client.
+    #[getter]
+    pub fn client_id(&self) -> u32 {
+        self.inner.client_id
+    }
+
+    /// The unique identifier of the authenticated user, or `None` if not 
authenticated.
+    #[getter]
+    pub fn user_id(&self) -> Option<u32> {
+        self.inner.user_id
+    }
+
+    /// The remote address of the client.
+    #[getter]
+    pub fn address(&self) -> String {
+        self.inner.address.clone()
+    }
+
+    /// The transport protocol used by the client.

Review Comment:
   also at line 139.
   
   this is one of five fixed strings - `TCP`, `QUIC`, `HTTP`, `WebSocket`, 
`Unknown`. list them here, the docstring ends up in the `.pyi` and it's the 
only place a python caller learns the set.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to