This is an automated email from the ASF dual-hosted git repository.
spetz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git
The following commit(s) were added to refs/heads/master by this push:
new e0a8196ef feat(python): expose update_user options (#4173)
e0a8196ef is described below
commit e0a8196efd7238a98ab9f4228ac8fb2455b5f408
Author: Jorge Polanco <[email protected]>
AuthorDate: Mon Sep 14 07:41:14 2026 -0600
feat(python): expose update_user options (#4173)
Closes #4164.
---------
Co-authored-by: Piotr Gankiewicz <[email protected]>
---
foreign/python/apache_iggy.pyi | 4 ++++
foreign/python/src/client.rs | 20 +++++++++++---------
foreign/python/tests/test_user.py | 36 ++++++++++++++++++++++++++++++++++++
3 files changed, 51 insertions(+), 9 deletions(-)
diff --git a/foreign/python/apache_iggy.pyi b/foreign/python/apache_iggy.pyi
index e628a7c4d..579fca81c 100644
--- a/foreign/python/apache_iggy.pyi
+++ b/foreign/python/apache_iggy.pyi
@@ -1246,6 +1246,7 @@ class IggyClient:
user_id: builtins.str | builtins.int,
username: builtins.str | None = None,
status: UserStatus | None = None,
+ options: builtins.dict[builtins.str, builtins.str] | None = None,
) -> collections.abc.Awaitable[None]:
r"""
Update a user by unique ID or username.
@@ -1254,6 +1255,9 @@ class IggyClient:
user_id: User identifier as `str | int`.
username: New username as `str | None`; unchanged when `None`.
status: New status as `UserStatus | None`; unchanged when `None`.
+ options: Reserved for future use. Additional option keys as
+ `dict[str, str] | None`, forwarded to the server. No user
update
+ option key exists yet, so a current server rejects every key.
Returns:
An awaitable that resolves to `None` when the user is updated.
diff --git a/foreign/python/src/client.rs b/foreign/python/src/client.rs
index d75f7c350..4c5799c17 100644
--- a/foreign/python/src/client.rs
+++ b/foreign/python/src/client.rs
@@ -354,6 +354,9 @@ impl IggyClient {
/// user_id: User identifier as `str | int`.
/// username: New username as `str | None`; unchanged when `None`.
/// status: New status as `UserStatus | None`; unchanged when `None`.
+ /// options: Reserved for future use. Additional option keys as
+ /// `dict[str, str] | None`, forwarded to the server. No user
update
+ /// option key exists yet, so a current server rejects every key.
///
/// Returns:
/// An awaitable that resolves to `None` when the user is updated.
@@ -361,7 +364,7 @@ impl IggyClient {
/// Raises:
/// ValueError: If a string identifier is invalid.
/// RuntimeError: If the request fails.
- #[pyo3(signature = (user_id, username=None, status=None))]
+ #[pyo3(signature = (user_id, username=None, status=None, options=None))]
#[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[None]",
imports=("collections.abc")))]
fn update_user<'a>(
&self,
@@ -369,20 +372,19 @@ impl IggyClient {
user_id: PyIdentifier,
#[gen_stub(override_type(type_repr = "builtins.str | None"))]
username: Option<String>,
#[gen_stub(override_type(type_repr = "UserStatus | None"))] status:
Option<PyUserStatus>,
+ #[gen_stub(override_type(type_repr = "builtins.dict[builtins.str,
builtins.str] | None"))]
+ options: Option<BTreeMap<String, String>>,
) -> PyResult<Bound<'a, PyAny>> {
let user_id = Identifier::try_from(user_id)?;
let status = status.map(UserStatus::from);
+ let update_options = UserUpdateOptions {
+ raw: options.unwrap_or_default(),
+ };
let inner = self.inner.clone();
future_into_py(py, async move {
inner
- .update_user(
- &user_id,
- username.as_deref(),
- status,
- // Users have no option keys yet.
- &UserUpdateOptions::default(),
- )
+ .update_user(&user_id, username.as_deref(), status,
&update_options)
.await
.map_err(to_runtime_error)?;
Ok(())
@@ -590,7 +592,7 @@ impl IggyClient {
/// `manage_streams` or per-stream `manage_stream` permission, the
/// stream does not exist, the new name is invalid or already
used, or
/// the request fails.
- #[pyo3(signature = (stream_id, name, options = None))]
+ #[pyo3(signature = (stream_id, name, options=None))]
#[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[None]",
imports=("collections.abc")))]
fn update_stream<'a>(
&self,
diff --git a/foreign/python/tests/test_user.py
b/foreign/python/tests/test_user.py
index a15b714b1..f6a328ca1 100644
--- a/foreign/python/tests/test_user.py
+++ b/foreign/python/tests/test_user.py
@@ -361,6 +361,42 @@ class TestUpdateUser:
await iggy_client.delete_user(created.id)
+ @pytest.mark.asyncio
+ async def test_update_user_with_empty_options_succeeds(
+ self, iggy_client: IggyClient, unique_name
+ ):
+ """Test update_user accepts an empty options map."""
+ username, password = unique_credentials(unique_name)
+ new_username = unique_name(max_bytes=MAX_USERNAME_BYTES)
+ created = await iggy_client.create_user(username, password)
+
+ await iggy_client.update_user(created.id, username=new_username,
options={})
+
+ user = await iggy_client.get_user(created.id)
+ assert user is not None
+ assert user.username == new_username
+
+ await iggy_client.delete_user(created.id)
+
+ @pytest.mark.asyncio
+ async def test_update_user_forwards_options(
+ self, iggy_client: IggyClient, unique_name
+ ):
+ """Test update_user forwards option keys to the server."""
+ username, password = unique_credentials(unique_name)
+ created = await iggy_client.create_user(username, password)
+
+ with pytest.raises(RuntimeError) as rejection:
+ await iggy_client.update_user(
+ created.id,
+ options={"unknown": "value"},
+ )
+
+ # Binary transports carry the code alone, so the key itself is empty.
+ assert "Unsupported option key" in str(rejection.value)
+
+ await iggy_client.delete_user(created.id)
+
@pytest.mark.asyncio
async def test_update_user_with_no_fields_is_a_noop(
self, iggy_client: IggyClient, unique_name