From c86d42c810a85863df37e8ad15d751ec24a31d7d Mon Sep 17 00:00:00 2001 From: haze518 Date: Thu, 27 Feb 2025 21:37:34 +0600 Subject: [PATCH] add stub file generator --- .github/workflows/python.yml | 6 ++ .gitignore | 1 - Cargo.lock | 152 ++++++++++++++++++++++++--- Cargo.toml | 13 ++- README.md | 7 ++ iggy_py.pyi | 198 +++++++++++++++++++++++++++++++++++ pyproject.toml | 13 ++- src/bin/stub_gen.rs | 8 ++ src/client.rs | 23 +++- src/lib.rs | 2 +- src/receive_message.rs | 7 +- src/send_message.rs | 3 + src/stream.rs | 3 + src/topic.rs | 3 + tests/__init__.py | 0 tests/conftest.py | 98 +++++++++++++++++ tests/test_client.py | 40 +++++++ 17 files changed, 552 insertions(+), 25 deletions(-) create mode 100644 iggy_py.pyi create mode 100644 src/bin/stub_gen.rs create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_client.py diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 994d501..b36c684 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -29,6 +29,8 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.10' + - name: Install dependencies + run: pip install ".[testing]" - name: Build wheels uses: PyO3/maturin-action@v1 with: @@ -37,6 +39,8 @@ jobs: manylinux: '2_28' args: --release --out dist --find-interpreter sccache: 'true' + - name: Run tests + run: pytest - name: Upload wheels uses: actions/upload-artifact@v4 with: @@ -94,6 +98,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Install dependencies + run: pip install ".[testing]" - name: Build sdist uses: PyO3/maturin-action@v1 with: diff --git a/.gitignore b/.gitignore index af3ca5e..7590764 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,6 @@ __pycache__/ .Python .venv/ env/ -bin/ build/ develop-eggs/ dist/ diff --git a/Cargo.lock b/Cargo.lock index 6175c1d..48529e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -297,7 +297,7 @@ dependencies = [ "bitflags 2.8.0", "cexpr", "clang-sys", - "itertools", + "itertools 0.12.1", "lazy_static", "lazycell", "log", @@ -1359,11 +1359,12 @@ dependencies = [ [[package]] name = "iggy-py" -version = "0.3.0" +version = "0.4.0" dependencies = [ "iggy", "pyo3", "pyo3-async-runtimes", + "pyo3-stub-gen", ] [[package]] @@ -1415,6 +1416,15 @@ dependencies = [ "web-sys", ] +[[package]] +name = "inventory" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b12ebb6799019b044deaf431eadfe23245b259bba5a2c0796acec3943a3cdb" +dependencies = [ + "rustversion", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -1436,6 +1446,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.14" @@ -1547,6 +1566,22 @@ version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "matrixmultiply" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9380b911e3e96d10c1f415da0876389aaf1b56759054eeb0de7df940c456ba1a" +dependencies = [ + "autocfg", + "rawpointer", +] + [[package]] name = "memchr" version = "2.7.4" @@ -1603,6 +1638,21 @@ dependencies = [ "getrandom 0.2.15", ] +[[package]] +name = "ndarray" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + [[package]] name = "nom" version = "7.1.3" @@ -1623,6 +1673,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1647,6 +1706,21 @@ dependencies = [ "autocfg", ] +[[package]] +name = "numpy" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb929bc0da91a4d85ed6c0a84deaa53d411abfb387fc271124f91bf6b89f14e" +dependencies = [ + "libc", + "ndarray", + "num-complex", + "num-integer", + "num-traits", + "pyo3", + "rustc-hash 1.1.0", +] + [[package]] name = "object" version = "0.36.7" @@ -1796,6 +1870,15 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1861,9 +1944,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.23.4" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57fe09249128b3173d092de9523eaa75136bf7ba85e0d69eca241c7939c933cc" +checksum = "f402062616ab18202ae8319da13fa4279883a2b8a9d9f83f20dbade813ce1884" dependencies = [ "cfg-if", "indoc", @@ -1879,9 +1962,9 @@ dependencies = [ [[package]] name = "pyo3-async-runtimes" -version = "0.23.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "977dc837525cfd22919ba6a831413854beb7c99a256c03bf8624ad707e45810e" +checksum = "2529f0be73ffd2be0cc43c013a640796558aa12d7ca0aab5cc14f375b4733031" dependencies = [ "futures", "once_cell", @@ -1893,9 +1976,9 @@ dependencies = [ [[package]] name = "pyo3-async-runtimes-macros" -version = "0.23.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2df2884957d2476731f987673befac5d521dff10abb0a7cbe12015bc7702fe9" +checksum = "22c26fd8e9fc19f53f0c1e00bf61471de6789f7eb263056f7f944a9cceb5823e" dependencies = [ "proc-macro2", "quote", @@ -1904,9 +1987,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.23.4" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd3927b5a78757a0d71aa9dff669f903b1eb64b54142a9bd9f757f8fde65fd7" +checksum = "b14b5775b5ff446dd1056212d778012cbe8a0fbffd368029fd9e25b514479c38" dependencies = [ "once_cell", "target-lexicon", @@ -1914,9 +1997,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.23.4" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dab6bb2102bd8f991e7749f130a70d05dd557613e39ed2deeee8e9ca0c4d548d" +checksum = "9ab5bcf04a2cdcbb50c7d6105de943f543f9ed92af55818fd17b660390fc8636" dependencies = [ "libc", "pyo3-build-config", @@ -1924,9 +2007,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.23.4" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91871864b353fd5ffcb3f91f2f703a22a9797c91b9ab497b1acac7b07ae509c7" +checksum = "0fd24d897903a9e6d80b968368a34e1525aeb719d568dba8b3d4bfa5dc67d453" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -1936,9 +2019,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.23.4" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43abc3b80bc20f3facd86cd3c60beed58c3e2aa26213f3cda368de39c60a27e4" +checksum = "36c011a03ba1e50152b4b394b479826cad97e7a21eb52df179cd91ac411cbfbe" dependencies = [ "heck", "proc-macro2", @@ -1947,6 +2030,37 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "pyo3-stub-gen" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca7c2d6e22cba51cc9766b6dee4087218cc445fdf99db62fa4f269e074351b46" +dependencies = [ + "anyhow", + "chrono", + "inventory", + "itertools 0.13.0", + "log", + "maplit", + "num-complex", + "numpy", + "pyo3", + "pyo3-stub-gen-derive", + "serde", + "toml", +] + +[[package]] +name = "pyo3-stub-gen-derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee49d727163163a0c6fc3fee4636c8b5c82e1bb868e85cf411be7ae9e4e5b40" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "quinn" version = "0.11.6" @@ -2076,6 +2190,12 @@ dependencies = [ "zerocopy 0.8.20", ] +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + [[package]] name = "redox_syscall" version = "0.2.16" diff --git a/Cargo.toml b/Cargo.toml index 7b7592f..a6ea6eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iggy-py" -version = "0.3.0" +version = "0.4.0" edition = "2021" authors = ["Dario Lencina Talarico "] license = "Apache-2.0" @@ -11,9 +11,14 @@ repository = "https://github.com/iggy-rs/iggy" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] name = "iggy_py" -crate-type = ["cdylib"] +crate-type = ["cdylib", "rlib"] [dependencies] -pyo3 = "0.23.0" +pyo3 = "0.22.0" iggy = "0.6.201" -pyo3-async-runtimes = { version = "0.23.0", features = ["attributes", "tokio-runtime"] } +pyo3-async-runtimes = { version = "0.22.0", features = ["attributes", "tokio-runtime"] } +pyo3-stub-gen = "0.7.0" + +[[bin]] +name = "stub_gen" +doc = false diff --git a/README.md b/README.md index f30cd89..9d18ed1 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,13 @@ All examples rely on a running iggy server. To start the server, execute: docker run --rm -p 8080:8080 -p 3000:3000 -p 8090:8090 iggyrs/iggy:0.4.21 ``` +## Generating Stub Files +To generate a stub file, execute the following command: + +``` +cargo run --bin stub_gen +``` + Refer to the python_examples directory for examples on how to use the iggy library. ## Running the Examples: diff --git a/iggy_py.pyi b/iggy_py.pyi new file mode 100644 index 0000000..f944083 --- /dev/null +++ b/iggy_py.pyi @@ -0,0 +1,198 @@ +# This file is automatically generated by pyo3_stub_gen +# ruff: noqa: E501, F401 + +import builtins +import typing +from enum import Enum, auto + +class IggyClient: + r""" + A Python class representing the Iggy client. + It wraps the RustIggyClient and provides asynchronous functionality + through the contained runtime. + """ + def new(self, conn:typing.Optional[builtins.str]) -> IggyClient: + r""" + Constructs a new IggyClient. + + This initializes a new runtime for asynchronous operations. + Future versions might utilize asyncio for more Pythonic async. + """ + ... + + def ping(self) -> typing.Any: + r""" + Sends a ping request to the server to check connectivity. + + Returns `Ok(())` if the server responds successfully, or a `PyRuntimeError` + if the connection fails. + """ + ... + + def login_user(self, username:builtins.str, password:builtins.str) -> typing.Any: + r""" + Logs in the user with the given credentials. + + Returns `Ok(())` on success, or a PyRuntimeError on failure. + """ + ... + + def connect(self) -> typing.Any: + r""" + Connects the IggyClient to its service. + + Returns Ok(()) on successful connection or a PyRuntimeError on failure. + """ + ... + + def create_stream(self, name:builtins.str, stream_id:typing.Optional[builtins.int]) -> typing.Any: + r""" + Creates a new stream with the provided ID and name. + + Returns Ok(()) on successful stream creation or a PyRuntimeError on failure. + """ + ... + + def get_stream(self, stream_id:PyIdentifier) -> typing.Any: + r""" + Gets stream by id. + + Returns Option of stream details or a PyRuntimeError on failure. + """ + ... + + def create_topic(self, stream:PyIdentifier, name:builtins.str, partitions_count:builtins.int, compression_algorithm:typing.Optional[builtins.str], topic_id:typing.Optional[builtins.int], replication_factor:typing.Optional[builtins.int]) -> typing.Any: + r""" + Creates a new topic with the given parameters. + + Returns Ok(()) on successful topic creation or a PyRuntimeError on failure. + """ + ... + + def get_topic(self, stream_id:PyIdentifier, topic_id:PyIdentifier) -> typing.Any: + r""" + Gets topic by stream and id. + + Returns Option of topic details or a PyRuntimeError on failure. + """ + ... + + def send_messages(self, stream:PyIdentifier, topic:PyIdentifier, partitioning:builtins.int, messages:list) -> typing.Any: + r""" + Sends a list of messages to the specified topic. + + Returns Ok(()) on successful sending or a PyRuntimeError on failure. + """ + ... + + def poll_messages(self, stream:PyIdentifier, topic:PyIdentifier, partition_id:builtins.int, polling_strategy:PollingStrategy, count:builtins.int, auto_commit:builtins.bool) -> typing.Any: + r""" + Polls for messages from the specified topic and partition. + + Returns a list of received messages or a PyRuntimeError on failure. + """ + ... + + +class ReceiveMessage: + r""" + A Python class representing a received message. + + This class wraps a Rust message, allowing for access to its payload and offset from Python. + """ + def payload(self) -> typing.Any: + r""" + Retrieves the payload of the received message. + + The payload is returned as a Python bytes object. + """ + ... + + def offset(self) -> builtins.int: + r""" + Retrieves the offset of the received message. + + The offset represents the position of the message within its topic. + """ + ... + + def timestamp(self) -> builtins.int: + r""" + Retrieves the timestamp of the received message. + + The timestamp represents the time of the message within its topic. + """ + ... + + def id(self) -> builtins.int: + r""" + Retrieves the id of the received message. + + The id represents unique identifier of the message within its topic. + """ + ... + + def checksum(self) -> builtins.int: + r""" + Retrieves the checksum of the received message. + + The checksum represents the integrity of the message within its topic. + """ + ... + + def state(self) -> MessageState: + r""" + Retrieves the Message's state of the received message. + + State represents the state of the response. + """ + ... + + def length(self) -> builtins.int: + r""" + Retrieves the length of the received message. + + The length represents the length of the payload. + """ + ... + + +class SendMessage: + r""" + A Python class representing a message to be sent. + + This class wraps a Rust message meant for sending, facilitating + the creation of such messages from Python and their subsequent use in Rust. + """ + def __new__(cls,data:builtins.str): ... + ... + +class StreamDetails: + id: builtins.int + name: builtins.str + messages_count: builtins.int + topics_count: builtins.int + +class TopicDetails: + id: builtins.int + name: builtins.str + messages_count: builtins.int + topics_count: builtins.int + +class MessageState(Enum): + Available = auto() + Unavailable = auto() + Poisoned = auto() + MarkedForDeletion = auto() + +class PollingStrategy(Enum): + Offset = auto() + Timestamp = auto() + First = auto() + Last = auto() + Next = auto() + +class PyIdentifier(Enum): + String = auto() + Int = auto() + diff --git a/pyproject.toml b/pyproject.toml index abb813c..bba4b03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "maturin" [project] name = "iggy_py" requires-python = ">=3.7" -version = "0.3.0" +version = "0.4.0" classifiers = [ "Programming Language :: Rust", "Programming Language :: Python :: Implementation :: CPython", @@ -15,3 +15,14 @@ description= "Apache Iggy is the persistent message streaming platform written i [tool.maturin] features = ["pyo3/extension-module"] + +[project.optional-dependencies] +testing = [ + "pytest", + "pytest-asyncio", + "testcontainers[docker]", + "maturin" +] + +[tool.pytest.ini_options] +asyncio_mode = "auto" diff --git a/src/bin/stub_gen.rs b/src/bin/stub_gen.rs new file mode 100644 index 0000000..5caf20f --- /dev/null +++ b/src/bin/stub_gen.rs @@ -0,0 +1,8 @@ +use pyo3_stub_gen::Result; + +fn main() -> Result<()> { + // `stub_info` is a function defined by `define_stub_info_gatherer!` macro. + let stub = iggy_py::client::stub_info()?; + stub.generate()?; + Ok(()) +} diff --git a/src/client.rs b/src/client.rs index 92f55b3..3bed7cb 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use std::sync::Arc; -use iggy::client::TopicClient; +use iggy::client::{SystemClient, TopicClient}; use iggy::client::{Client, MessageClient, StreamClient, UserClient}; use iggy::clients::builder::IggyClientBuilder; use iggy::clients::client::IggyClient as RustIggyClient; @@ -15,6 +15,8 @@ use iggy::utils::topic_size::MaxTopicSize; use pyo3::prelude::*; use pyo3::types::PyList; 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_pyclass_enum, gen_stub_pymethods}; use crate::receive_message::{PollingStrategy, ReceiveMessage}; use crate::send_message::SendMessage; @@ -24,11 +26,13 @@ use crate::topic::TopicDetails; /// 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, } +#[gen_stub_pyclass_enum] #[derive(FromPyObject)] enum PyIdentifier { #[pyo3(transparent, annotation = "str")] @@ -47,6 +51,7 @@ impl From for Identifier { } #[pymethods] +#[gen_stub_pymethods] impl IggyClient { /// Constructs a new IggyClient. /// @@ -65,6 +70,20 @@ impl IggyClient { } } + /// Sends a ping request to the server to check connectivity. + /// + /// Returns `Ok(())` if the server responds successfully, or a `PyRuntimeError` + /// if the connection fails. + fn ping<'a>(&self, py: Python<'a>) -> PyResult> { + let inner = self.inner.clone(); + future_into_py(py, async move { + inner.ping().await.map_err(|e| { + PyErr::new::(format!("{:?}", e)) + })?; + Ok(()) + }) + } + /// Logs in the user with the given credentials. /// /// Returns `Ok(())` on success, or a PyRuntimeError on failure. @@ -283,3 +302,5 @@ impl IggyClient { }) } } + +define_stub_info_gatherer!(stub_info); diff --git a/src/lib.rs b/src/lib.rs index b2ead09..e23214c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -mod client; +pub mod client; mod receive_message; mod send_message; mod stream; diff --git a/src/receive_message.rs b/src/receive_message.rs index 39adb18..73c83d1 100644 --- a/src/receive_message.rs +++ b/src/receive_message.rs @@ -3,11 +3,13 @@ use iggy::models::messages::MessageState as RustMessageState; use iggy::models::messages::PolledMessage as RustReceiveMessage; use pyo3::prelude::*; use pyo3::types::PyBytes; +use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pyclass_enum, gen_stub_pymethods}; /// A Python class representing a received message. /// /// This class wraps a Rust message, allowing for access to its payload and offset from Python. #[pyclass] +#[gen_stub_pyclass] pub struct ReceiveMessage { pub(crate) inner: RustReceiveMessage, } @@ -21,6 +23,7 @@ impl ReceiveMessage { } } +#[gen_stub_pyclass_enum] #[pyclass(eq, eq_int)] #[derive(PartialEq)] pub enum MessageState { @@ -30,13 +33,14 @@ pub enum MessageState { MarkedForDeletion, } +#[gen_stub_pymethods] #[pymethods] impl ReceiveMessage { /// Retrieves the payload of the received message. /// /// The payload is returned as a Python bytes object. pub fn payload(&self, py: Python) -> PyObject { - PyBytes::new(py, &self.inner.payload).into() + PyBytes::new_bound(py, &self.inner.payload).into() } /// Retrieves the offset of the received message. @@ -88,6 +92,7 @@ impl ReceiveMessage { } #[derive(Clone, Copy)] +#[gen_stub_pyclass_enum] #[pyclass] pub enum PollingStrategy { Offset { value: u64 }, diff --git a/src/send_message.rs b/src/send_message.rs index 5afce13..b359d1b 100644 --- a/src/send_message.rs +++ b/src/send_message.rs @@ -1,5 +1,6 @@ use iggy::messages::send_messages::Message as RustSendMessage; use pyo3::prelude::*; +use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; use std::str::FromStr; /// A Python class representing a message to be sent. @@ -7,6 +8,7 @@ use std::str::FromStr; /// This class wraps a Rust message meant for sending, facilitating /// the creation of such messages from Python and their subsequent use in Rust. #[pyclass] +#[gen_stub_pyclass] pub struct SendMessage { pub(crate) inner: RustSendMessage, } @@ -23,6 +25,7 @@ impl Clone for SendMessage { } } +#[gen_stub_pymethods] #[pymethods] impl SendMessage { /// Constructs a new `SendMessage` instance from a string. diff --git a/src/stream.rs b/src/stream.rs index a7387a0..73f32ce 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -1,7 +1,9 @@ use iggy::models::stream::StreamDetails as RustStreamDetails; use pyo3::prelude::*; +use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; #[pyclass] +#[gen_stub_pyclass] pub struct StreamDetails { pub(crate) inner: RustStreamDetails, } @@ -14,6 +16,7 @@ impl From for StreamDetails { } } +#[gen_stub_pymethods] #[pymethods] impl StreamDetails { #[getter] diff --git a/src/topic.rs b/src/topic.rs index c6f8843..7f8f26e 100644 --- a/src/topic.rs +++ b/src/topic.rs @@ -1,6 +1,8 @@ use iggy::models::topic::TopicDetails as RustTopicDetails; use pyo3::prelude::*; +use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods}; +#[gen_stub_pyclass] #[pyclass] pub struct TopicDetails { pub(crate) inner: RustTopicDetails, @@ -14,6 +16,7 @@ impl From for TopicDetails { } } +#[gen_stub_pymethods] #[pymethods] impl TopicDetails { #[getter] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..cd02a74 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,98 @@ +import socket +import pytest +import time +from typing import Generator +from testcontainers.core.container import DockerContainer + +from iggy_py import IggyClient + + +@pytest.fixture(scope="session") +def iggy_container() -> Generator[DockerContainer, None, None]: + """ + Creates and starts an Iggy server container using Docker. + """ + container = DockerContainer("iggyrs/iggy:0.4.21").with_exposed_ports( + 8080, 3000, 8090 + ) + container.start() + + yield container + + container.stop() + + +@pytest.fixture(scope="session") +async def iggy_client(iggy_container: DockerContainer) -> IggyClient: + """ + Initializes and returns an Iggy client connected to the running Iggy server container. + + This fixture ensures that the client is authenticated and ready for use in tests. + + :param iggy_container: The running Iggy container fixture. + :return: An instance of IggyClient connected to the server. + """ + host = iggy_container.get_container_host_ip() + port = iggy_container.get_exposed_port(8090) + wait_for_container(port, host, timeout=30, interval=5) + + client = IggyClient(f"{host}:{port}") + + await client.connect() + + await wait_for_ping(client, timeout=30, interval=5) + + await client.login_user("iggy", "iggy") + return client + + +def wait_for_container(port: int, host: str, timeout: int, interval: int) -> None: + """ + Waits for a container to become alive by polling a specified port. + + :param port: The port number to poll. + :param host: The hostname or IP address of the container (default is 'localhost'). + :param timeout: The maximum time in seconds to wait for the container to become available (default is 30). + :param interval: The time in seconds between each polling attempt (default is 2). + """ + start_time = time.time() + + while True: + try: + with socket.create_connection((host, port), timeout=interval): + return + except (socket.timeout, ConnectionRefusedError): + elapsed_time = time.time() - start_time + if elapsed_time >= timeout: + raise TimeoutError( + f"Timed out after {timeout} seconds waiting for container to become available at {host}:{port}" + ) + + time.sleep(interval) + + +async def wait_for_ping( + client: IggyClient, timeout: int = 30, interval: int = 5 +) -> None: + """ + Waits for the Iggy server to respond to ping requests before proceeding. + + :param client: The Iggy client instance. + :param timeout: The maximum time in seconds to wait for the server to respond. + :param interval: The time in seconds between each ping attempt. + :raises TimeoutError: If the server does not respond within the timeout period. + """ + start_time = time.time() + + while True: + try: + await client.ping() + return + except Exception: + elapsed_time = time.time() - start_time + if elapsed_time >= timeout: + raise TimeoutError( + f"Timed out after {timeout} seconds waiting for Iggy server to respond to ping." + ) + + time.sleep(interval) diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..fe1243c --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,40 @@ +from iggy_py import PollingStrategy +from iggy_py import SendMessage as Message +from iggy_py import IggyClient + +STREAM_NAME = "test-stream" +TOPIC_NAME = "test-topic" +PARTITION_ID = 1 + + +async def test_send_and_poll_messages(iggy_client: IggyClient): + assert iggy_client is not None + + await iggy_client.create_stream(STREAM_NAME) + stream = await iggy_client.get_stream(STREAM_NAME) + assert stream is not None + assert stream.name == STREAM_NAME + + await iggy_client.create_topic(STREAM_NAME, TOPIC_NAME, partitions_count=1) + topic = await iggy_client.get_topic(STREAM_NAME, TOPIC_NAME) + assert topic is not None + assert topic.name == TOPIC_NAME + + messages = [ + Message("Message 1"), + Message("Message 2"), + ] + await iggy_client.send_messages(STREAM_NAME, TOPIC_NAME, PARTITION_ID, messages) + + polled_messages = await iggy_client.poll_messages( + STREAM_NAME, + TOPIC_NAME, + PARTITION_ID, + PollingStrategy.Next(), + count=10, + auto_commit=True, + ) + + assert len(polled_messages) >= 2 + assert polled_messages[0].payload().decode("utf-8") == "Message 1" + assert polled_messages[1].payload().decode("utf-8") == "Message 2"