Skip to content
4 changes: 2 additions & 2 deletions core/sdk/src/prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ pub use iggy_common::{
QuicClientConfig, QuicClientConfigBuilder, QuicClientReconnectionConfig, SendMessages,
Sizeable, SnapshotCompression, Stats, Stream, StreamDetails, StreamPermissions,
SystemSnapshotType, TcpClientConfig, TcpClientConfigBuilder, TcpClientReconnectionConfig,
Topic, TopicDetails, TopicPermissions, TransportEndpoints, TransportProtocol, UserId,
UserStatus, Validatable, WebSocketClientConfig, WebSocketClientConfigBuilder,
Topic, TopicDetails, TopicPermissions, TransportEndpoints, TransportProtocol, UserId, UserInfo,
UserInfoDetails, UserStatus, Validatable, WebSocketClientConfig, WebSocketClientConfigBuilder,
WebSocketClientReconnectionConfig, defaults, locking,
};
pub use iggy_common::{
Expand Down
151 changes: 151 additions & 0 deletions foreign/python/apache_iggy.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import asyncio
import builtins
import collections.abc
import datetime
import enum
import typing

__all__ = [
Expand All @@ -39,6 +40,9 @@ __all__ = [
"StreamDetails",
"Topic",
"TopicDetails",
"UserInfo",
"UserInfoDetails",
"UserStatus",
]

class AutoCommit:
Expand Down Expand Up @@ -335,6 +339,92 @@ class IggyClient:
Logs in the user with the given credentials.
Returns `Ok(())` on success, or a PyRuntimeError on failure.
"""
def get_user(
self, user_id: builtins.str | builtins.int
) -> collections.abc.Awaitable[UserInfoDetails | None]:
r"""
Get the info about a specific user by unique ID or username.

Args:
user_id: User identifier as `str | int`.

Returns:
An awaitable that resolves to `UserInfoDetails` if the user exists,
or `None` otherwise.

Raises:
PyValueError: If a string identifier is invalid.
PyRuntimeError: If the request fails.
"""
def get_users(self) -> collections.abc.Awaitable[list[UserInfo]]:
r"""
Get the info about all the users.

Returns:
An awaitable that resolves to `list[UserInfo]`.

Raises:
PyRuntimeError: If the request fails.
"""
def create_user(
self,
username: builtins.str,
password: builtins.str,
status: UserStatus | None = None,
) -> collections.abc.Awaitable[UserInfoDetails]:
r"""
Create a new user.

The user is created without permissions.

Args:
username: Username as `str`.
password: Password as `str`.
status: User status as `UserStatus | None`; defaults to `UserStatus.Active`.

Returns:
An awaitable that resolves to the created `UserInfoDetails`.

Raises:
PyRuntimeError: If an argument is invalid or the request fails.
"""
def update_user(
self,
user_id: builtins.str | builtins.int,
username: builtins.str | None = None,
status: UserStatus | None = None,
) -> collections.abc.Awaitable[None]:
r"""
Update a user by unique ID or username.

Args:
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`.

Returns:
An awaitable that resolves to `None` when the user is updated.

Raises:
PyValueError: If a string identifier is invalid.
PyRuntimeError: If the request fails.
"""
def delete_user(
self, user_id: builtins.str | builtins.int
) -> collections.abc.Awaitable[None]:
r"""
Delete a user by unique ID or username.

Args:
user_id: User identifier as `str | int`.

Returns:
An awaitable that resolves to `None` when the user is deleted.

Raises:
PyValueError: If a string identifier is invalid.
PyRuntimeError: If the request fails.
"""
def connect(self) -> collections.abc.Awaitable[None]:
r"""
Connects the IggyClient to its service.
Expand Down Expand Up @@ -862,3 +952,64 @@ class TopicDetails:
r"""
Replication factor for the topic.
"""

@typing.final
class UserInfo:
@property
def id(self) -> builtins.int:
r"""
The unique identifier (numeric) of the user.
"""
@property
def created_at(self) -> builtins.int:
r"""
The timestamp when the user was created, in microseconds since the Unix epoch.
"""
@property
def status(self) -> UserStatus:
r"""
The status of the user.
"""
@property
def username(self) -> builtins.str:
r"""
The username of the user.
"""

@typing.final
class UserInfoDetails:
@property
def id(self) -> builtins.int:
r"""
The unique identifier (numeric) of the user.
"""
@property
def created_at(self) -> builtins.int:
r"""
The timestamp when the user was created, in microseconds since the Unix epoch.
"""
@property
def status(self) -> UserStatus:
r"""
The status of the user.
"""
@property
def username(self) -> builtins.str:
r"""
The username of the user.
"""

@typing.final
class UserStatus(enum.Enum):
r"""
The status of a user account.
"""

Active = ...
r"""
The user account is active and can be used.
"""
Inactive = ...
r"""
The user account is inactive and cannot be used.
"""
144 changes: 144 additions & 0 deletions foreign/python/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ use crate::receive_message::{PollingStrategy, ReceiveMessage};
use crate::send_message::SendMessage;
use crate::stream::StreamDetails;
use crate::topic::{Topic, TopicDetails};
use crate::user::{
UserInfo as PyUserInfo, UserInfoDetails as PyUserInfoDetails, UserStatus as PyUserStatus,
};
use tokio::sync::Mutex;

/// A Python class representing the Iggy client.
Expand Down Expand Up @@ -119,6 +122,147 @@ impl IggyClient {
})
}

/// Get the info about a specific user by unique ID or username.
///
/// Args:
/// user_id: User identifier as `str | int`.
///
/// Returns:
/// An awaitable that resolves to `UserInfoDetails` if the user exists,
/// or `None` otherwise.
///
/// Raises:
/// PyValueError: If a string identifier is invalid.
/// PyRuntimeError: If the request fails.
#[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[UserInfoDetails | None]", imports=("collections.abc")))]
fn get_user<'a>(&self, py: Python<'a>, user_id: PyIdentifier) -> PyResult<Bound<'a, PyAny>> {
let user_id = Identifier::try_from(user_id)?;
let inner = self.inner.clone();

future_into_py(py, async move {
let user = inner
.get_user(&user_id)
.await
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
Ok(user.map(PyUserInfoDetails::from))
})
}

/// Get the info about all the users.
///
/// Returns:
/// An awaitable that resolves to `list[UserInfo]`.
///
/// Raises:
/// PyRuntimeError: If the request fails.
#[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[list[UserInfo]]", imports=("collections.abc")))]
fn get_users<'a>(&self, py: Python<'a>) -> PyResult<Bound<'a, PyAny>> {
let inner = self.inner.clone();

future_into_py(py, async move {
let users = inner
.get_users()
.await
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
Ok(users.into_iter().map(PyUserInfo::from).collect::<Vec<_>>())
})
}

/// Create a new user.
///
/// The user is created without permissions.
///
/// Args:
/// username: Username as `str`.
/// password: Password as `str`.
/// status: User status as `UserStatus | None`; defaults to `UserStatus.Active`.
///
/// Returns:
/// An awaitable that resolves to the created `UserInfoDetails`.
///
/// Raises:
/// PyRuntimeError: If an argument is invalid or the request fails.
#[pyo3(signature = (username, password, status=None))]
#[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[UserInfoDetails]", imports=("collections.abc")))]
fn create_user<'a>(
&self,
py: Python<'a>,
username: String,
password: String,
#[gen_stub(override_type(type_repr = "UserStatus | None"))] status: Option<PyUserStatus>,
) -> PyResult<Bound<'a, PyAny>> {
let status = status.map_or(UserStatus::Active, UserStatus::from);
let inner = self.inner.clone();

future_into_py(py, async move {
let user = inner
.create_user(&username, &password, status, None)
.await
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
Ok(PyUserInfoDetails::from(user))
})
}

/// Update a user by unique ID or username.
///
/// Args:
/// 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`.
///
/// Returns:
/// An awaitable that resolves to `None` when the user is updated.
///
/// Raises:
/// PyValueError: If a string identifier is invalid.
/// PyRuntimeError: If the request fails.
#[pyo3(signature = (user_id, username=None, status=None))]
#[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[None]", imports=("collections.abc")))]
fn update_user<'a>(
&self,
py: Python<'a>,
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>,
) -> PyResult<Bound<'a, PyAny>> {
let user_id = Identifier::try_from(user_id)?;
let status = status.map(UserStatus::from);
let inner = self.inner.clone();

future_into_py(py, async move {
inner
.update_user(&user_id, username.as_deref(), status)
.await
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
Ok(())
})
}

/// Delete a user by unique ID or username.
///
/// Args:
/// user_id: User identifier as `str | int`.
///
/// Returns:
/// An awaitable that resolves to `None` when the user is deleted.
///
/// Raises:
/// PyValueError: If a string identifier is invalid.
/// PyRuntimeError: If the request fails.
#[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[None]", imports=("collections.abc")))]
fn delete_user<'a>(&self, py: Python<'a>, user_id: PyIdentifier) -> PyResult<Bound<'a, PyAny>> {
let user_id = Identifier::try_from(user_id)?;
let inner = self.inner.clone();

future_into_py(py, async move {
inner
.delete_user(&user_id)
.await
.map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?;
Ok(())
})
}

/// Connects the IggyClient to its service.
/// Returns Ok(()) on successful connection or a PyRuntimeError on failure.
#[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[None]", imports=("collections.abc")))]
Expand Down
5 changes: 5 additions & 0 deletions foreign/python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ mod receive_message;
mod send_message;
mod stream;
mod topic;
mod user;

use client::IggyClient;
use consumer::{
Expand All @@ -33,6 +34,7 @@ use receive_message::{PollingStrategy, ReceiveMessage};
use send_message::SendMessage;
use stream::StreamDetails;
use topic::{Topic, TopicDetails};
use user::{UserInfo, UserInfoDetails, UserStatus};

/// A Python module implemented in Rust.
#[pymodule]
Expand All @@ -52,5 +54,8 @@ fn apache_iggy(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<AutoCommitAfter>()?;
m.add_class::<AutoCommitWhen>()?;
m.add_class::<ReceiveMessageIterator>()?;
m.add_class::<UserStatus>()?;
m.add_class::<UserInfo>()?;
m.add_class::<UserInfoDetails>()?;
Ok(())
}
Loading