| // 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 bytes::Bytes; |
| use iggy::prelude::{ |
| AutoCommit as RustAutoCommit, Consumer as RustConsumer, IggyClient as RustIggyClient, |
| IggyExpiry as RustIggyExpiry, IggyMessage as RustMessage, MaxTopicSize as RustMaxTopicSize, |
| PollingStrategy as RustPollingStrategy, *, |
| }; |
| use pyo3::PyRef; |
| use pyo3::prelude::*; |
| use pyo3::types::{PyBytes, PyDelta, PyList, PyType}; |
| use pyo3_async_runtimes::tokio::future_into_py; |
| use pyo3_stub_gen::define_stub_info_gatherer; |
| use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; |
| use std::str::FromStr; |
| use std::sync::Arc; |
| |
| use crate::consumer::{ |
| AutoCommit, ConsumerGroup as PyConsumerGroup, ConsumerGroupDetails as PyConsumerGroupDetails, |
| IggyConsumer, py_delta_to_iggy_duration, |
| }; |
| use crate::identifier::PyIdentifier; |
| use crate::permissions::Permissions as PyPermissions; |
| use crate::receive_message::{PollingStrategy, ReceiveMessage}; |
| use crate::send_message::{SendMessage, SendMessagesResponse as PySendMessagesResponse}; |
| use crate::stream::StreamDetails; |
| use crate::topic::{IggyExpiry, MaxTopicSize, 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. |
| /// It wraps the RustIggyClient and provides asynchronous functionality |
| /// through the contained runtime. |
| #[gen_stub_pyclass] |
| #[pyclass] |
| pub struct IggyClient { |
| inner: Arc<RustIggyClient>, |
| } |
| |
| /// Resolves the shared `create_topic`/`update_topic` parameters, applying |
| /// server defaults where the caller left them unset. |
| fn resolve_topic_params( |
| compression_algorithm: Option<String>, |
| message_expiry: Option<&IggyExpiry>, |
| max_topic_size: Option<&MaxTopicSize>, |
| ) -> PyResult<(CompressionAlgorithm, RustIggyExpiry, RustMaxTopicSize)> { |
| let compression_algorithm = match compression_algorithm { |
| Some(algo) => CompressionAlgorithm::from_str(&algo) |
| .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?, |
| None => CompressionAlgorithm::default(), |
| }; |
| |
| let expiry = message_expiry |
| .map(RustIggyExpiry::try_from) |
| .transpose()? |
| .unwrap_or(RustIggyExpiry::ServerDefault); |
| |
| let max_size = max_topic_size |
| .map(RustMaxTopicSize::try_from) |
| .transpose()? |
| .unwrap_or(RustMaxTopicSize::ServerDefault); |
| |
| Ok((compression_algorithm, expiry, max_size)) |
| } |
| |
| #[gen_stub_pymethods] |
| #[pymethods] |
| impl IggyClient { |
| /// Constructs a new IggyClient from a TCP server address. |
| /// This initializes a new runtime for asynchronous operations. |
| /// Future versions might utilize asyncio for more Pythonic async. |
| #[new] |
| #[pyo3(signature = (conn=None))] |
| fn new( |
| #[gen_stub(override_type(type_repr = "builtins.str | None"))] conn: Option<String>, |
| ) -> PyResult<Self> { |
| let client = IggyClientBuilder::new() |
| .with_tcp() |
| .with_server_address(conn.unwrap_or("127.0.0.1:8090".to_string())) |
| .build() |
| .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?; |
| Ok(IggyClient { |
| inner: Arc::new(client), |
| }) |
| } |
| |
| /// Constructs a new IggyClient from a connection string. |
| /// Returns an error if the connection string provided is invalid. |
| // TODO: add examples for connection strings or at least a link to the doc page where |
| // connection strings are explained. |
| #[classmethod] |
| #[pyo3(signature = (connection_string))] |
| fn from_connection_string( |
| _cls: &Bound<'_, PyType>, |
| connection_string: String, |
| ) -> PyResult<Self> { |
| let client = RustIggyClient::from_connection_string(&connection_string) |
| .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?; |
| Ok(Self { |
| inner: Arc::new(client), |
| }) |
| } |
| |
| /// Sends a ping request to the server to check connectivity. |
| /// Returns `Ok(())` if the server responds successfully, or a `PyRuntimeError` |
| /// if the connection fails. |
| #[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[None]", imports=("collections.abc")))] |
| fn ping<'a>(&self, py: Python<'a>) -> PyResult<Bound<'a, PyAny>> { |
| let inner = self.inner.clone(); |
| future_into_py(py, async move { |
| inner |
| .ping() |
| .await |
| .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string())) |
| }) |
| } |
| |
| /// Logs in the user with the given credentials. |
| /// Returns `Ok(())` on success, or a PyRuntimeError on failure. |
| #[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[None]", imports=("collections.abc")))] |
| fn login_user<'a>( |
| &self, |
| py: Python<'a>, |
| username: String, |
| password: String, |
| ) -> PyResult<Bound<'a, PyAny>> { |
| let inner = self.inner.clone(); |
| future_into_py(py, async move { |
| inner |
| .login_user(&username, &password) |
| .await |
| .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?; |
| Ok(()) |
| }) |
| } |
| |
| /// 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. |
| /// |
| /// Args: |
| /// username: Username as `str`. |
| /// password: Password as `str`. |
| /// status: User status as `UserStatus | None`; defaults to `UserStatus.Active`. |
| /// permissions: Permissions as `Permissions | None`; the user has none when `None`. |
| /// |
| /// 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, permissions=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>, |
| #[gen_stub(override_type(type_repr = "Permissions | None"))] permissions: Option< |
| PyPermissions, |
| >, |
| ) -> PyResult<Bound<'a, PyAny>> { |
| let status = status.map_or(UserStatus::Active, UserStatus::from); |
| let permissions = permissions.map(|permissions| permissions.inner); |
| let inner = self.inner.clone(); |
| |
| future_into_py(py, async move { |
| let user = inner |
| .create_user(&username, &password, status, permissions) |
| .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(()) |
| }) |
| } |
| |
| /// Update the permissions of a user by unique ID or username. |
| /// |
| /// This is a full replacement: the given permissions overwrite the previous |
| /// ones, and `None` removes them entirely. |
| /// |
| /// Args: |
| /// user_id: User identifier as `str | int`. |
| /// permissions: New permissions as `Permissions | None`. |
| /// |
| /// Returns: |
| /// An awaitable that resolves to `None` when the permissions are updated. |
| /// |
| /// Raises: |
| /// PyValueError: If a string identifier is invalid. |
| /// PyRuntimeError: If the request fails. |
| #[pyo3(signature = (user_id, permissions))] |
| #[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[None]", imports=("collections.abc")))] |
| fn update_permissions<'a>( |
| &self, |
| py: Python<'a>, |
| user_id: PyIdentifier, |
| #[gen_stub(override_type(type_repr = "Permissions | None"))] permissions: Option< |
| PyPermissions, |
| >, |
| ) -> PyResult<Bound<'a, PyAny>> { |
| let user_id = Identifier::try_from(user_id)?; |
| let permissions = permissions.map(|permissions| permissions.inner); |
| let inner = self.inner.clone(); |
| |
| future_into_py(py, async move { |
| inner |
| .update_permissions(&user_id, permissions) |
| .await |
| .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?; |
| Ok(()) |
| }) |
| } |
| |
| /// Change the password of a user by unique ID or username. |
| /// |
| /// Args: |
| /// user_id: User identifier as `str | int`. |
| /// current_password: Current password as `str`. |
| /// new_password: New password as `str`. |
| /// |
| /// Returns: |
| /// An awaitable that resolves to `None` when the password is changed. |
| /// |
| /// Raises: |
| /// PyValueError: If a string identifier is invalid. |
| /// PyRuntimeError: If the current password is wrong or the request fails. |
| #[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[None]", imports=("collections.abc")))] |
| fn change_password<'a>( |
| &self, |
| py: Python<'a>, |
| user_id: PyIdentifier, |
| current_password: String, |
| new_password: String, |
| ) -> PyResult<Bound<'a, PyAny>> { |
| let user_id = Identifier::try_from(user_id)?; |
| let inner = self.inner.clone(); |
| |
| future_into_py(py, async move { |
| inner |
| .change_password(&user_id, ¤t_password, &new_password) |
| .await |
| .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?; |
| Ok(()) |
| }) |
| } |
| |
| /// Log out the currently authenticated user. |
| /// |
| /// Returns: |
| /// An awaitable that resolves to `None` when the user is logged out. |
| /// |
| /// Raises: |
| /// PyRuntimeError: If the request fails. |
| #[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[None]", imports=("collections.abc")))] |
| fn logout_user<'a>(&self, py: Python<'a>) -> PyResult<Bound<'a, PyAny>> { |
| let inner = self.inner.clone(); |
| |
| future_into_py(py, async move { |
| inner |
| .logout_user() |
| .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")))] |
| fn connect<'a>(&self, py: Python<'a>) -> PyResult<Bound<'a, PyAny>> { |
| let inner = self.inner.clone(); |
| future_into_py(py, async move { |
| inner |
| .connect() |
| .await |
| .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?; |
| Ok(()) |
| }) |
| } |
| |
| /// Creates a new stream with the provided ID and name. |
| /// Returns Ok(()) on successful stream creation or a PyRuntimeError on failure. |
| #[pyo3(signature = (name))] |
| #[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[None]", imports=("collections.abc")))] |
| fn create_stream<'a>(&self, py: Python<'a>, name: String) -> PyResult<Bound<'a, PyAny>> { |
| let inner = self.inner.clone(); |
| future_into_py(py, async move { |
| inner |
| .create_stream(&name) |
| .await |
| .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?; |
| Ok(()) |
| }) |
| } |
| |
| /// Gets stream by id. |
| /// Returns Option of stream details or a PyRuntimeError on failure. |
| #[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[StreamDetails | None]", imports=("collections.abc")))] |
| fn get_stream<'a>( |
| &self, |
| py: Python<'a>, |
| stream_id: PyIdentifier, |
| ) -> PyResult<Bound<'a, PyAny>> { |
| let stream_id = Identifier::try_from(stream_id)?; |
| let inner = self.inner.clone(); |
| |
| future_into_py(py, async move { |
| let stream = inner |
| .get_stream(&stream_id) |
| .await |
| .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?; |
| Ok(stream.map(StreamDetails::from)) |
| }) |
| } |
| |
| /// Creates a new topic with the given parameters. |
| /// |
| /// Args: |
| /// stream: Stream identifier as `str | int`. |
| /// name: Topic name as `str`. |
| /// partitions_count: Number of partitions as `int`. |
| /// compression_algorithm: Compression algorithm as `str | None`. |
| /// replication_factor: Replication factor as `int | None`. |
| /// message_expiry: Message expiry as `IggyExpiry | None`. |
| /// max_topic_size: Maximum topic size as `MaxTopicSize | None`. |
| /// |
| /// Returns: |
| /// An awaitable that resolves to `None` when the topic is created. |
| /// |
| /// Raises: |
| /// ValueError: If `message_expiry` or `max_topic_size` is out of range. |
| /// PyRuntimeError: If another argument is invalid or the request fails. |
| #[pyo3( |
| signature = (stream, name, partitions_count, compression_algorithm = None, replication_factor = None, message_expiry = None, max_topic_size = None) |
| )] |
| #[allow(clippy::too_many_arguments)] |
| #[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[None]", imports=("collections.abc")))] |
| fn create_topic<'a>( |
| &self, |
| py: Python<'a>, |
| stream: PyIdentifier, |
| name: String, |
| partitions_count: u32, |
| #[gen_stub(override_type(type_repr = "builtins.str | None"))] compression_algorithm: Option< |
| String, |
| >, |
| #[gen_stub(override_type(type_repr = "builtins.int | None"))] replication_factor: Option< |
| u8, |
| >, |
| #[gen_stub(override_type(type_repr = "IggyExpiry | None"))] message_expiry: Option< |
| &IggyExpiry, |
| >, |
| #[gen_stub(override_type(type_repr = "MaxTopicSize | None"))] max_topic_size: Option< |
| &MaxTopicSize, |
| >, |
| ) -> PyResult<Bound<'a, PyAny>> { |
| let (compression_algorithm, expiry, max_size) = |
| resolve_topic_params(compression_algorithm, message_expiry, max_topic_size)?; |
| |
| let stream = Identifier::try_from(stream)?; |
| let inner = self.inner.clone(); |
| |
| future_into_py(py, async move { |
| inner |
| .create_topic( |
| &stream, |
| &name, |
| partitions_count, |
| compression_algorithm, |
| replication_factor, |
| expiry, |
| max_size, |
| ) |
| .await |
| .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?; |
| Ok(()) |
| }) |
| } |
| |
| /// Gets topic by stream and id. |
| /// Returns Option of topic details or a PyRuntimeError on failure. |
| #[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[TopicDetails | None]", imports=("collections.abc")))] |
| fn get_topic<'a>( |
| &self, |
| py: Python<'a>, |
| stream_id: PyIdentifier, |
| topic_id: PyIdentifier, |
| ) -> PyResult<Bound<'a, PyAny>> { |
| let stream_id = Identifier::try_from(stream_id)?; |
| let topic_id = Identifier::try_from(topic_id)?; |
| let inner = self.inner.clone(); |
| |
| future_into_py(py, async move { |
| let topic = inner |
| .get_topic(&stream_id, &topic_id) |
| .await |
| .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?; |
| Ok(topic.map(TopicDetails::from)) |
| }) |
| } |
| |
| /// Get all topics in a stream. |
| /// |
| /// Args: |
| /// stream_id: Stream identifier as `str | int`. |
| /// |
| /// Returns: |
| /// An awaitable that resolves to `list[Topic]`. |
| /// |
| /// Raises: |
| /// PyRuntimeError: If the identifier is invalid or the request fails. |
| #[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[list[Topic]]", imports=("collections.abc")))] |
| fn get_topics<'a>( |
| &self, |
| py: Python<'a>, |
| stream_id: PyIdentifier, |
| ) -> PyResult<Bound<'a, PyAny>> { |
| let stream_id = Identifier::try_from(stream_id)?; |
| let inner = self.inner.clone(); |
| |
| future_into_py(py, async move { |
| let topics = inner |
| .get_topics(&stream_id) |
| .await |
| .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?; |
| Ok(topics.into_iter().map(Topic::from).collect::<Vec<_>>()) |
| }) |
| } |
| |
| /// Update an existing topic. |
| /// |
| /// This is a full replacement: any optional parameter left unset is reset to |
| /// its server default rather than preserved. |
| /// |
| /// Args: |
| /// stream_id: Stream identifier as `str | int`. |
| /// topic_id: Topic identifier as `str | int`. |
| /// name: New topic name as `str`. |
| /// compression_algorithm: Compression algorithm as `str | None`. |
| /// replication_factor: Replication factor as `int | None`. |
| /// message_expiry: Message expiry as `IggyExpiry | None`. |
| /// max_topic_size: Maximum topic size as `MaxTopicSize | None`. |
| /// |
| /// Returns: |
| /// An awaitable that resolves to `None` when the topic is updated. |
| /// |
| /// Raises: |
| /// ValueError: If `message_expiry` or `max_topic_size` is out of range. |
| /// PyRuntimeError: If another argument is invalid or the request fails. |
| #[pyo3( |
| signature = (stream_id, topic_id, name, compression_algorithm = None, replication_factor = None, message_expiry = None, max_topic_size = None) |
| )] |
| #[allow(clippy::too_many_arguments)] |
| #[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[None]", imports=("collections.abc")))] |
| fn update_topic<'a>( |
| &self, |
| py: Python<'a>, |
| stream_id: PyIdentifier, |
| topic_id: PyIdentifier, |
| name: String, |
| #[gen_stub(override_type(type_repr = "builtins.str | None"))] compression_algorithm: Option< |
| String, |
| >, |
| #[gen_stub(override_type(type_repr = "builtins.int | None"))] replication_factor: Option< |
| u8, |
| >, |
| #[gen_stub(override_type(type_repr = "IggyExpiry | None"))] message_expiry: Option< |
| &IggyExpiry, |
| >, |
| #[gen_stub(override_type(type_repr = "MaxTopicSize | None"))] max_topic_size: Option< |
| &MaxTopicSize, |
| >, |
| ) -> PyResult<Bound<'a, PyAny>> { |
| let (compression_algorithm, expiry, max_size) = |
| resolve_topic_params(compression_algorithm, message_expiry, max_topic_size)?; |
| |
| let stream_id = Identifier::try_from(stream_id)?; |
| let topic_id = Identifier::try_from(topic_id)?; |
| let inner = self.inner.clone(); |
| |
| future_into_py(py, async move { |
| inner |
| .update_topic( |
| &stream_id, |
| &topic_id, |
| &name, |
| compression_algorithm, |
| replication_factor, |
| expiry, |
| max_size, |
| ) |
| .await |
| .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?; |
| Ok(()) |
| }) |
| } |
| |
| /// Delete a topic from a stream. |
| /// |
| /// Args: |
| /// stream_id: Stream identifier as `str | int`. |
| /// topic_id: Topic identifier as `str | int`. |
| /// |
| /// Returns: |
| /// An awaitable that resolves to `None` when the topic is deleted. |
| /// |
| /// Raises: |
| /// PyRuntimeError: If an identifier is invalid or the request fails. |
| #[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[None]", imports=("collections.abc")))] |
| fn delete_topic<'a>( |
| &self, |
| py: Python<'a>, |
| stream_id: PyIdentifier, |
| topic_id: PyIdentifier, |
| ) -> PyResult<Bound<'a, PyAny>> { |
| let stream_id = Identifier::try_from(stream_id)?; |
| let topic_id = Identifier::try_from(topic_id)?; |
| let inner = self.inner.clone(); |
| |
| future_into_py(py, async move { |
| inner |
| .delete_topic(&stream_id, &topic_id) |
| .await |
| .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?; |
| Ok(()) |
| }) |
| } |
| |
| /// Purge all messages from a topic. |
| /// |
| /// Args: |
| /// stream_id: Stream identifier as `str | int`. |
| /// topic_id: Topic identifier as `str | int`. |
| /// |
| /// Returns: |
| /// An awaitable that resolves to `None` when the topic is purged. |
| /// |
| /// Raises: |
| /// PyRuntimeError: If an identifier is invalid or the request fails. |
| #[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[None]", imports=("collections.abc")))] |
| fn purge_topic<'a>( |
| &self, |
| py: Python<'a>, |
| stream_id: PyIdentifier, |
| topic_id: PyIdentifier, |
| ) -> PyResult<Bound<'a, PyAny>> { |
| let stream_id = Identifier::try_from(stream_id)?; |
| let topic_id = Identifier::try_from(topic_id)?; |
| let inner = self.inner.clone(); |
| |
| future_into_py(py, async move { |
| inner |
| .purge_topic(&stream_id, &topic_id) |
| .await |
| .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?; |
| Ok(()) |
| }) |
| } |
| |
| /// Create a consumer group for a stream and topic. |
| /// |
| /// Args: |
| /// stream_id: Stream identifier as `str | int`. |
| /// topic_id: Topic identifier as `str | int`. |
| /// name: Consumer group name as `str`. |
| /// |
| /// Returns: |
| /// An awaitable that resolves to `None` when the consumer group is created. |
| /// |
| /// Raises: |
| /// PyValueError: If an identifier is invalid. |
| /// PyRuntimeError: If the request fails. |
| #[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[None]", imports=("collections.abc")))] |
| fn create_consumer_group<'a>( |
| &self, |
| py: Python<'a>, |
| stream_id: PyIdentifier, |
| topic_id: PyIdentifier, |
| name: String, |
| ) -> PyResult<Bound<'a, PyAny>> { |
| let stream_id = Identifier::try_from(stream_id)?; |
| let topic_id = Identifier::try_from(topic_id)?; |
| let inner = self.inner.clone(); |
| |
| future_into_py(py, async move { |
| inner |
| .create_consumer_group(&stream_id, &topic_id, &name) |
| .await |
| .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?; |
| Ok(()) |
| }) |
| } |
| |
| /// Retrieve details for a consumer group from the specified stream and topic. |
| /// |
| /// Args: |
| /// stream_id: Stream identifier as `str | int`. |
| /// topic_id: Topic identifier as `str | int`. |
| /// group_id: Consumer group identifier as `str | int`. |
| /// |
| /// Returns: |
| /// An awaitable that resolves to `ConsumerGroupDetails` if the consumer group exists, |
| /// or `None` otherwise. |
| /// |
| /// Raises: |
| /// PyValueError: If an identifier is invalid. |
| /// PyRuntimeError: If the request fails. |
| #[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[ConsumerGroupDetails | None]", imports=("collections.abc")))] |
| fn get_consumer_group<'a>( |
| &self, |
| py: Python<'a>, |
| stream_id: PyIdentifier, |
| topic_id: PyIdentifier, |
| group_id: PyIdentifier, |
| ) -> PyResult<Bound<'a, PyAny>> { |
| let stream_id = Identifier::try_from(stream_id)?; |
| let topic_id = Identifier::try_from(topic_id)?; |
| let group_id = Identifier::try_from(group_id)?; |
| let inner = self.inner.clone(); |
| |
| future_into_py(py, async move { |
| let group = inner |
| .get_consumer_group(&stream_id, &topic_id, &group_id) |
| .await |
| .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?; |
| Ok(group.map(PyConsumerGroupDetails::from)) |
| }) |
| } |
| |
| /// Get all consumer groups for the specified stream and topic. |
| /// |
| /// Args: |
| /// stream_id: Stream identifier as `str | int`. |
| /// topic_id: Topic identifier as `str | int`. |
| /// |
| /// Returns: |
| /// An awaitable that resolves to `list[ConsumerGroup]`. |
| /// |
| /// Raises: |
| /// PyValueError: If an identifier is invalid. |
| /// PyRuntimeError: If the request fails. |
| #[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[list[ConsumerGroup]]", imports=("collections.abc")))] |
| fn get_consumer_groups<'a>( |
| &self, |
| py: Python<'a>, |
| stream_id: PyIdentifier, |
| topic_id: PyIdentifier, |
| ) -> PyResult<Bound<'a, PyAny>> { |
| let stream_id = Identifier::try_from(stream_id)?; |
| let topic_id = Identifier::try_from(topic_id)?; |
| let inner = self.inner.clone(); |
| |
| future_into_py(py, async move { |
| let groups = inner |
| .get_consumer_groups(&stream_id, &topic_id) |
| .await |
| .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?; |
| Ok(groups |
| .into_iter() |
| .map(PyConsumerGroup::from) |
| .collect::<Vec<_>>()) |
| }) |
| } |
| |
| /// Delete a consumer group for a stream and topic. |
| /// |
| /// Args: |
| /// stream_id: Stream identifier as `str | int`. |
| /// topic_id: Topic identifier as `str | int`. |
| /// group_id: Consumer group identifier as `str | int`. |
| /// |
| /// Returns: |
| /// An awaitable that resolves to `None` when the consumer group 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_consumer_group<'a>( |
| &self, |
| py: Python<'a>, |
| stream_id: PyIdentifier, |
| topic_id: PyIdentifier, |
| group_id: PyIdentifier, |
| ) -> PyResult<Bound<'a, PyAny>> { |
| let stream_id = Identifier::try_from(stream_id)?; |
| let topic_id = Identifier::try_from(topic_id)?; |
| let group_id = Identifier::try_from(group_id)?; |
| let inner = self.inner.clone(); |
| |
| future_into_py(py, async move { |
| inner |
| .delete_consumer_group(&stream_id, &topic_id, &group_id) |
| .await |
| .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?; |
| Ok(()) |
| }) |
| } |
| |
| /// Join a consumer group for a stream and topic. |
| /// |
| /// This method only registers the current client as a group member. To consume messages |
| /// as a group, use `consumer_group()`, which enables auto-join by default. |
| /// |
| /// Args: |
| /// stream_id: Stream identifier as `str | int`. |
| /// topic_id: Topic identifier as `str | int`. |
| /// group_id: Consumer group identifier as `str | int`. |
| /// |
| /// Returns: |
| /// An awaitable that resolves to `None` when the client joins the consumer group. |
| /// |
| /// Raises: |
| /// PyValueError: If a string identifier is invalid. |
| /// PyRuntimeError: If the request fails, including `Feature is unavailable` on HTTP transport. |
| #[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[None]", imports=("collections.abc")))] |
| fn join_consumer_group<'a>( |
| &self, |
| py: Python<'a>, |
| stream_id: PyIdentifier, |
| topic_id: PyIdentifier, |
| group_id: PyIdentifier, |
| ) -> PyResult<Bound<'a, PyAny>> { |
| let stream_id = Identifier::try_from(stream_id)?; |
| let topic_id = Identifier::try_from(topic_id)?; |
| let group_id = Identifier::try_from(group_id)?; |
| let inner = self.inner.clone(); |
| |
| future_into_py(py, async move { |
| inner |
| .join_consumer_group(&stream_id, &topic_id, &group_id) |
| .await |
| .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?; |
| Ok(()) |
| }) |
| } |
| |
| /// Leave a consumer group for a stream and topic. |
| /// |
| /// Args: |
| /// stream_id: Stream identifier as `str | int`. |
| /// topic_id: Topic identifier as `str | int`. |
| /// group_id: Consumer group identifier as `str | int`. |
| /// |
| /// Returns: |
| /// An awaitable that resolves to `None` when the client leaves the consumer group. |
| /// |
| /// Note: |
| /// Consumers created from this client for the same group share one server-side |
| /// membership. Leaving revokes that membership. Consumers with auto-join enabled |
| /// rejoin on their next poll. |
| /// |
| /// Raises: |
| /// PyValueError: If a string identifier is invalid. |
| /// PyRuntimeError: If the request fails, including `Feature is unavailable` on HTTP transport. |
| #[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[None]", imports=("collections.abc")))] |
| fn leave_consumer_group<'a>( |
| &self, |
| py: Python<'a>, |
| stream_id: PyIdentifier, |
| topic_id: PyIdentifier, |
| group_id: PyIdentifier, |
| ) -> PyResult<Bound<'a, PyAny>> { |
| let stream_id = Identifier::try_from(stream_id)?; |
| let topic_id = Identifier::try_from(topic_id)?; |
| let group_id = Identifier::try_from(group_id)?; |
| let inner = self.inner.clone(); |
| |
| future_into_py(py, async move { |
| inner |
| .leave_consumer_group(&stream_id, &topic_id, &group_id) |
| .await |
| .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?; |
| Ok(()) |
| }) |
| } |
| |
| /// Sends a list of messages to the specified topic. |
| /// Returns a SendMessagesResponse carrying the per-partition commit |
| /// confirmations, or a PyRuntimeError on failure. The confirmation list is |
| /// empty when the server reports no offsets, and the legacy server never |
| /// reports any. |
| #[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[SendMessagesResponse]", imports=("collections.abc")))] |
| fn send_messages<'a>( |
| &self, |
| py: Python<'a>, |
| stream: PyIdentifier, |
| topic: PyIdentifier, |
| partitioning: u32, |
| #[gen_stub(override_type(type_repr = "list[SendMessage]"))] messages: &Bound<'_, PyList>, |
| ) -> PyResult<Bound<'a, PyAny>> { |
| let messages: Vec<SendMessage> = messages |
| .iter() |
| .map(|item| { |
| let msg: PyRef<'_, SendMessage> = item.extract()?; |
| Ok::<_, PyErr>(msg.clone()) |
| }) |
| .collect::<Result<Vec<_>, _>>()?; |
| let mut messages: Vec<RustMessage> = messages |
| .into_iter() |
| .map(|message| message.inner) |
| .collect::<Vec<_>>(); |
| |
| let stream = Identifier::try_from(stream)?; |
| let topic = Identifier::try_from(topic)?; |
| let partitioning = Partitioning::partition_id(partitioning); |
| let inner = self.inner.clone(); |
| |
| future_into_py(py, async move { |
| let response = inner |
| .send_messages(&stream, &topic, &partitioning, messages.as_mut()) |
| .await |
| .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?; |
| Ok(PySendMessagesResponse::from(response)) |
| }) |
| } |
| |
| /// Polls for messages from the specified topic and partition. |
| /// Returns a list of received messages or a PyRuntimeError on failure. |
| #[allow(clippy::too_many_arguments)] |
| #[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[list[ReceiveMessage]]", imports=("collections.abc")))] |
| fn poll_messages<'a>( |
| &self, |
| py: Python<'a>, |
| stream: PyIdentifier, |
| topic: PyIdentifier, |
| partition_id: u32, |
| polling_strategy: &PollingStrategy, |
| count: u32, |
| auto_commit: bool, |
| ) -> PyResult<Bound<'a, PyAny>> { |
| let consumer = RustConsumer::default(); |
| let stream = Identifier::try_from(stream)?; |
| let topic = Identifier::try_from(topic)?; |
| let strategy: RustPollingStrategy = polling_strategy.into(); |
| |
| let inner = self.inner.clone(); |
| |
| future_into_py(py, async move { |
| let polled_messages = inner |
| .poll_messages( |
| &stream, |
| &topic, |
| Some(partition_id), |
| &consumer, |
| &strategy, |
| count, |
| auto_commit, |
| ) |
| .await |
| .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?; |
| let messages = polled_messages |
| .messages |
| .into_iter() |
| .map(|m| ReceiveMessage { |
| inner: m, |
| partition_id, |
| }) |
| .collect::<Vec<_>>(); |
| Ok(messages) |
| }) |
| } |
| |
| /// Creates a new consumer group consumer. |
| /// Returns the consumer or a PyRuntimeError on failure. |
| #[allow(clippy::too_many_arguments)] |
| #[pyo3(signature = ( |
| name, |
| stream, |
| topic, |
| partition_id=None, |
| polling_strategy=None, |
| batch_length=None, |
| auto_commit=None, |
| create_consumer_group_if_not_exists=true, |
| auto_join_consumer_group=true, |
| poll_interval=None, |
| polling_retry_interval=None, |
| init_retries=None, |
| init_retry_interval=None, |
| allow_replay=false, |
| ))] |
| #[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[IggyConsumer]", imports=("collections.abc")))] |
| fn consumer_group<'a>( |
| &self, |
| py: Python<'a>, |
| name: &str, |
| stream: &str, |
| topic: &str, |
| #[gen_stub(override_type(type_repr = "builtins.int | None"))] partition_id: Option<u32>, |
| #[gen_stub(override_type(type_repr = "PollingStrategy | None"))] polling_strategy: Option< |
| &PollingStrategy, |
| >, |
| #[gen_stub(override_type(type_repr = "builtins.int | None"))] batch_length: Option<u32>, |
| #[gen_stub(override_type(type_repr = "AutoCommit | None"))] auto_commit: Option< |
| &AutoCommit, |
| >, |
| create_consumer_group_if_not_exists: bool, |
| auto_join_consumer_group: bool, |
| #[gen_stub(override_type(type_repr = "datetime.timedelta | None", imports=("datetime")))] |
| poll_interval: Option<Py<PyDelta>>, |
| #[gen_stub(override_type(type_repr = "datetime.timedelta | None", imports=("datetime")))] |
| polling_retry_interval: Option<Py<PyDelta>>, |
| #[gen_stub(override_type(type_repr = "builtins.int | None"))] init_retries: Option<u32>, |
| #[gen_stub(override_type(type_repr = "datetime.timedelta | None", imports=("datetime")))] |
| init_retry_interval: Option<Py<PyDelta>>, |
| allow_replay: bool, |
| ) -> PyResult<Bound<'a, PyAny>> { |
| let mut builder = self |
| .inner |
| .consumer_group(name, stream, topic) |
| .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))? |
| .without_encryptor() |
| .partition(partition_id); |
| |
| if create_consumer_group_if_not_exists { |
| builder = builder.create_consumer_group_if_not_exists() |
| } else { |
| builder = builder.do_not_create_consumer_group_if_not_exists() |
| }; |
| if auto_join_consumer_group { |
| builder = builder.auto_join_consumer_group() |
| } else { |
| builder = builder.do_not_auto_join_consumer_group() |
| }; |
| if let Some(polling_strategy) = polling_strategy { |
| builder = builder.polling_strategy(polling_strategy.into()) |
| }; |
| if let Some(batch_length) = batch_length { |
| builder = builder.batch_length(batch_length) |
| }; |
| if let Some(auto_commit) = auto_commit { |
| builder = builder.auto_commit(RustAutoCommit::try_from(auto_commit)?) |
| }; |
| if let Some(poll_interval) = poll_interval { |
| builder = builder.poll_interval(py_delta_to_iggy_duration(&poll_interval)?) |
| } else { |
| builder = builder.without_poll_interval() |
| }; |
| if let Some(polling_retry_interval) = polling_retry_interval { |
| builder = |
| builder.polling_retry_interval(py_delta_to_iggy_duration(&polling_retry_interval)?) |
| } |
| if init_retries.is_some() && init_retry_interval.is_none() { |
| return Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>( |
| "'init_retry_interval' is required if 'init_retries' is set", |
| )); |
| } |
| if init_retries.is_none() && init_retry_interval.is_some() { |
| return Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>( |
| "'init_retries' is required if 'init_retry_interval' is set", |
| )); |
| } |
| if let (Some(init_retries), Some(init_retry_interval)) = (init_retries, init_retry_interval) |
| { |
| builder = builder.init_retries( |
| init_retries, |
| py_delta_to_iggy_duration(&init_retry_interval)?, |
| ); |
| } |
| if allow_replay { |
| builder = builder.allow_replay() |
| } |
| let mut consumer = builder.build(); |
| |
| future_into_py(py, async move { |
| consumer |
| .init() |
| .await |
| .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?; |
| Ok(IggyConsumer { |
| inner: Arc::new(Mutex::new(consumer)), |
| }) |
| }) |
| } |
| |
| /// Send a command code with a payload and return the raw response bytes. |
| /// |
| /// Session-control codes are rejected client-side. HTTP transport does not |
| /// support raw binary commands. |
| /// |
| /// Args: |
| /// code: Command code as `int`. |
| /// payload: Request payload as `bytes`. |
| /// |
| /// Returns: |
| /// An awaitable that resolves to the raw response `bytes`. |
| /// |
| /// Raises: |
| /// PyRuntimeError: If the command cannot be sent or the server returns an error. |
| #[gen_stub(override_return_type(type_repr="collections.abc.Awaitable[bytes]", imports=("collections.abc")))] |
| fn send_binary_request<'a>( |
| &self, |
| py: Python<'a>, |
| code: u32, |
| #[gen_stub(override_type(type_repr = "builtins.bytes"))] payload: Vec<u8>, |
| ) -> PyResult<Bound<'a, PyAny>> { |
| let inner = self.inner.clone(); |
| future_into_py(py, async move { |
| let response = inner |
| .send_binary_request(code, Bytes::from(payload)) |
| .await |
| .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(e.to_string()))?; |
| Ok(Python::attach(|py| PyBytes::new(py, &response).unbind())) |
| }) |
| } |
| } |
| |
| define_stub_info_gatherer!(stub_info); |