Skip to content

Commit b6075d2

Browse files
committed
Added push and pull consumers.
1 parent 0e6b01b commit b6075d2

7 files changed

Lines changed: 125 additions & 35 deletions

File tree

‎python/natsrpy/_natsrpy_rs/js/consumers.pyi‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,16 @@ class PushConsumerConfig:
147147
pause_until: int | None = None,
148148
) -> None: ...
149149

150-
class PushConsumer: ...
150+
class MessagesIterator:
151+
def __aiter__(self) -> MessagesIterator: ...
152+
async def __anext__(self) -> JetStreamMessage: ...
153+
async def next(
154+
self,
155+
timeout: float | timedelta | None = None,
156+
) -> JetStreamMessage: ...
157+
158+
class PushConsumer:
159+
async def messages(self) -> MessagesIterator: ...
151160

152161
class PullConsumer:
153162
async def fetch(

‎src/exceptions/rust_err.rs‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use pyo3::exceptions::{PyTimeoutError, PyTypeError};
1+
use pyo3::exceptions::{PyStopAsyncIteration, PyTimeoutError, PyTypeError};
22

33
use crate::exceptions::py_err::{NatsrpyPublishError, NatsrpySessionError};
44

@@ -12,6 +12,8 @@ pub enum NatsrpyError {
1212
InvalidArgument(String),
1313
#[error("Session is not initialized. Call startup() first.")]
1414
NotInitialized,
15+
#[error("The end of stream")]
16+
AsyncStopIteration,
1517
#[error("Connection is closed or lost.")]
1618
Disconnected,
1719
#[error(transparent)]
@@ -66,12 +68,17 @@ pub enum NatsrpyError {
6668
PullConsumerError(#[from] async_nats::jetstream::stream::ConsumerError),
6769
#[error(transparent)]
6870
PullConsumerBatchError(#[from] async_nats::jetstream::consumer::pull::BatchError),
71+
#[error(transparent)]
72+
PushConsumerMessageError(#[from] async_nats::jetstream::consumer::push::MessagesError),
73+
#[error(transparent)]
74+
ConsumerStreamError(#[from] async_nats::jetstream::consumer::StreamError),
6975
}
7076

7177
impl From<NatsrpyError> for pyo3::PyErr {
7278
fn from(value: NatsrpyError) -> Self {
7379
match value {
7480
NatsrpyError::PublishError(_) => NatsrpyPublishError::new_err(value.to_string()),
81+
NatsrpyError::AsyncStopIteration => PyStopAsyncIteration::new_err("End of the stream."),
7582
NatsrpyError::Timeout(_) => PyTimeoutError::new_err(value.to_string()),
7683
NatsrpyError::PyError(py_err) => py_err,
7784
NatsrpyError::InvalidArgument(descr) => PyTypeError::new_err(descr),

‎src/js/consumers/mod.rs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,7 @@ pub mod pymod {
99
#[pymodule_export]
1010
pub use super::pull::{config::PullConsumerConfig, consumer::PullConsumer};
1111
#[pymodule_export]
12-
pub use super::push::{config::PushConsumerConfig, consumer::PushConsumer};
12+
pub use super::push::{
13+
config::PushConsumerConfig, consumer::MessagesIterator, consumer::PushConsumer,
14+
};
1315
}

‎src/js/consumers/pull/consumer.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ impl PullConsumer {
8989
let mut ret_messages = Vec::new();
9090
while let Some(msg) = messages.next().await {
9191
let raw_msg = msg?;
92-
ret_messages.push(crate::js::message::JetStreamMessage::from(raw_msg));
92+
ret_messages.push(crate::js::message::JetStreamMessage::try_from(raw_msg)?);
9393
}
9494
Ok(ret_messages)
9595
})

‎src/js/consumers/push/consumer.rs‎

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
use std::sync::Arc;
22

3+
use futures_util::StreamExt;
4+
use pyo3::{Bound, PyAny, PyRef, Python};
35
use tokio::sync::RwLock;
46

7+
use crate::{
8+
exceptions::rust_err::{NatsrpyError, NatsrpyResult},
9+
js::pymod::JetStreamMessage,
10+
utils::{futures::natsrpy_future_with_timeout, natsrpy_future, py_types::TimeValue},
11+
};
12+
513
type NatsPushConsumer =
614
async_nats::jetstream::consumer::Consumer<async_nats::jetstream::consumer::push::Config>;
715

816
#[pyo3::pyclass(from_py_object)]
917
#[derive(Debug, Clone)]
10-
#[allow(dead_code)] // TODO! remove later.
1118
pub struct PushConsumer {
1219
consumer: Arc<RwLock<NatsPushConsumer>>,
1320
}
@@ -21,5 +28,67 @@ impl PushConsumer {
2128
}
2229
}
2330

31+
#[pyo3::pyclass]
32+
pub struct MessagesIterator {
33+
messages: Option<Arc<RwLock<async_nats::jetstream::consumer::push::Messages>>>,
34+
}
35+
36+
impl From<async_nats::jetstream::consumer::push::Messages> for MessagesIterator {
37+
fn from(value: async_nats::jetstream::consumer::push::Messages) -> Self {
38+
Self {
39+
messages: Some(Arc::new(RwLock::new(value))),
40+
}
41+
}
42+
}
43+
44+
#[pyo3::pymethods]
45+
impl PushConsumer {
46+
pub fn messages<'py>(&self, py: Python<'py>) -> NatsrpyResult<Bound<'py, PyAny>> {
47+
let consumer_guard = self.consumer.clone();
48+
natsrpy_future(py, async move {
49+
Ok(MessagesIterator::from(
50+
consumer_guard.read().await.messages().await?,
51+
))
52+
})
53+
}
54+
}
55+
2456
#[pyo3::pymethods]
25-
impl PushConsumer {}
57+
impl MessagesIterator {
58+
#[must_use]
59+
pub const fn __aiter__(slf: PyRef<Self>) -> PyRef<Self> {
60+
slf
61+
}
62+
63+
pub fn next<'py>(
64+
&self,
65+
py: Python<'py>,
66+
timeout: Option<TimeValue>,
67+
) -> NatsrpyResult<Bound<'py, PyAny>> {
68+
let Some(messages_guard) = self.messages.clone() else {
69+
unreachable!("Message is always Some in runtime.")
70+
};
71+
#[allow(clippy::significant_drop_tightening)]
72+
natsrpy_future_with_timeout(py, timeout, async move {
73+
let mut messages = messages_guard.write().await;
74+
let Some(message) = messages.next().await else {
75+
return Err(NatsrpyError::AsyncStopIteration);
76+
};
77+
let message = message?;
78+
79+
JetStreamMessage::try_from(message)
80+
})
81+
}
82+
83+
pub fn __anext__<'py>(&self, py: Python<'py>) -> NatsrpyResult<Bound<'py, PyAny>> {
84+
self.next(py, None)
85+
}
86+
}
87+
88+
impl Drop for MessagesIterator {
89+
fn drop(&mut self) {
90+
pyo3_async_runtimes::tokio::get_runtime().block_on(async move {
91+
self.messages = None;
92+
});
93+
}
94+
}

‎src/js/message.rs‎

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,30 @@
1-
use pyo3::{Bound, Py, PyAny, Python, types::PyDict};
1+
use pyo3::{
2+
Bound, Py, PyAny, Python,
3+
types::{PyBytes, PyDict},
4+
};
25
use std::sync::Arc;
36
use tokio::sync::RwLock;
47

58
use crate::{
6-
exceptions::rust_err::NatsrpyResult,
7-
utils::{headers::NatsrpyHeadermapExt, natsrpy_future, py_types::TimeValue},
9+
exceptions::rust_err::{NatsrpyError, NatsrpyResult},
10+
utils::{natsrpy_future, py_types::TimeValue},
811
};
912

1013
#[pyo3::pyclass]
1114
pub struct JetStreamMessage {
12-
message: async_nats::Message,
13-
headers: Option<Py<PyDict>>,
15+
message: crate::message::Message,
1416
acker: Arc<RwLock<async_nats::jetstream::message::Acker>>,
1517
}
1618

17-
impl From<async_nats::jetstream::Message> for JetStreamMessage {
18-
fn from(value: async_nats::jetstream::Message) -> Self {
19+
impl TryFrom<async_nats::jetstream::Message> for JetStreamMessage {
20+
type Error = NatsrpyError;
21+
22+
fn try_from(value: async_nats::jetstream::Message) -> Result<Self, Self::Error> {
1923
let (message, acker) = value.split();
20-
Self {
21-
message,
22-
headers: None,
24+
Ok(Self {
25+
message: message.try_into()?,
2326
acker: Arc::new(RwLock::new(acker)),
24-
}
27+
})
2528
}
2629
}
2730

@@ -47,27 +50,23 @@ impl JetStreamMessage {
4750
#[pyo3::pymethods]
4851
impl JetStreamMessage {
4952
#[getter]
50-
pub fn subject(&self) -> &str {
53+
#[must_use]
54+
pub const fn subject(&self) -> &str {
5155
self.message.subject.as_str()
5256
}
5357
#[getter]
54-
pub fn reply(&self) -> Option<&str> {
55-
self.message.reply.as_ref().map(async_nats::Subject::as_str)
58+
#[must_use]
59+
pub const fn reply(&self) -> &Option<String> {
60+
&self.message.reply
5661
}
5762
#[getter]
58-
pub fn payload(&self) -> &[u8] {
63+
#[must_use]
64+
pub const fn payload(&self) -> &Py<PyBytes> {
5965
&self.message.payload
6066
}
6167
#[getter]
62-
pub fn headers(&mut self, py: Python<'_>) -> NatsrpyResult<Py<PyDict>> {
63-
if let Some(headers) = &self.headers {
64-
Ok(headers.clone_ref(py))
65-
} else {
66-
let headermap = self.message.headers.clone().unwrap_or_default();
67-
let headers = headermap.to_pydict(py)?.unbind();
68-
self.headers = Some(headers.clone_ref(py));
69-
Ok(headers)
70-
}
68+
pub const fn headers(&mut self) -> &Py<PyDict> {
69+
&self.message.headers
7170
}
7271

7372
#[pyo3(signature=(double=false))]
@@ -107,4 +106,9 @@ impl JetStreamMessage {
107106
pub fn term<'py>(&self, py: Python<'py>, double: bool) -> NatsrpyResult<Bound<'py, PyAny>> {
108107
self.inner_ack(py, async_nats::jetstream::message::AckKind::Term, double)
109108
}
109+
110+
#[must_use]
111+
pub fn __repr__(&self) -> String {
112+
self.message.__repr__()
113+
}
110114
}

‎src/subscription.rs‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
use futures_util::StreamExt;
2-
use pyo3::exceptions::PyStopAsyncIteration;
3-
use std::{sync::Arc, time::Duration};
2+
use std::sync::Arc;
43

54
use pyo3::{Bound, PyAny, PyRef, Python};
65
use tokio::sync::Mutex;
76

87
use crate::{
98
exceptions::rust_err::{NatsrpyError, NatsrpyResult},
10-
utils::futures::natsrpy_future_with_timeout,
9+
utils::{futures::natsrpy_future_with_timeout, py_types::TimeValue},
1110
};
1211

1312
#[pyo3::pyclass]
@@ -34,14 +33,14 @@ impl Subscription {
3433
pub fn next<'py>(
3534
&self,
3635
py: Python<'py>,
37-
timeout: Option<Duration>,
36+
timeout: Option<TimeValue>,
3837
) -> NatsrpyResult<Bound<'py, PyAny>> {
3938
let Some(inner) = self.inner.clone() else {
4039
return Err(NatsrpyError::NotInitialized);
4140
};
4241
natsrpy_future_with_timeout(py, timeout, async move {
4342
let Some(message) = inner.lock().await.next().await else {
44-
return Err(PyStopAsyncIteration::new_err("End of the stream.").into());
43+
return Err(NatsrpyError::AsyncStopIteration);
4544
};
4645

4746
crate::message::Message::try_from(message)

0 commit comments

Comments
 (0)