Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading