blob: 0d6e35c803be90afc4e364c6e43f5d3b71385b9a [file]
// 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 iggy::prelude::{
IggyByteSize, IggyExpiry as RustIggyExpiry, MaxTopicSize as RustMaxTopicSize,
Partition as RustPartition, ResourceOptions, Topic as RustTopic,
TopicDetails as RustTopicDetails,
};
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::PyDelta;
use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pyclass_complex_enum, gen_stub_pymethods};
use crate::duration::{iggy_duration_to_py_delta, py_delta_to_iggy_duration};
use crate::user_headers::{UserHeaders, rust_user_headers_to_py};
/// The entries of one provenance, as the dictionary message user headers come
/// back as.
///
/// Options ride the user-headers codec, so they are handed back through the
/// same `HeaderKey`/`HeaderValue` types rather than a second shape meaning the
/// same thing: `to_scalar_dict()` works on the result exactly as it does on
/// `ReceiveMessage.user_headers`.
fn options_by_provenance<'a>(
py: Python<'a>,
options: &ResourceOptions,
explicit: bool,
) -> PyResult<Bound<'a, UserHeaders>> {
let selected = options
.iter()
.filter(|(_, option)| option.explicit == explicit)
.map(|(key, option)| (key.clone(), option.value.clone()))
.collect();
rust_user_headers_to_py(py, selected)
}
/// The expiry of the messages in a topic.
#[gen_stub_pyclass_complex_enum]
#[pyclass]
pub enum IggyExpiry {
/// Use the message expiry configured on the server for this topic,
/// rather than an explicit value set by the client.
ServerDefault(),
/// Expire messages this long after they are appended to the topic.
///
/// `duration` must be greater than zero and less than the maximum
/// microsecond count a `u64` can hold (about 584,542 years): those two
/// values are reserved on the wire for `ServerDefault` and `NeverExpire`
/// respectively, so a `duration` at either boundary raises `ValueError`
/// when passed to `create_topic`/`update_topic`. A negative `timedelta`
/// also raises `ValueError`.
ExpireDuration { duration: Py<PyDelta> },
/// Retain messages indefinitely; they never expire.
NeverExpire(),
}
impl TryFrom<RustIggyExpiry> for IggyExpiry {
type Error = PyErr;
fn try_from(expiry: RustIggyExpiry) -> PyResult<Self> {
Ok(match expiry {
RustIggyExpiry::ServerDefault => IggyExpiry::ServerDefault(),
RustIggyExpiry::ExpireDuration(duration) => IggyExpiry::ExpireDuration {
duration: Python::attach(|py| {
iggy_duration_to_py_delta(py, duration).map(|delta| delta.unbind())
})
.map_err(|err| {
PyValueError::new_err(format!(
"topic message expiry duration does not fit within timedelta bounds: {err}"
))
})?,
},
RustIggyExpiry::NeverExpire => IggyExpiry::NeverExpire(),
})
}
}
impl TryFrom<&IggyExpiry> for RustIggyExpiry {
type Error = PyErr;
fn try_from(expiry: &IggyExpiry) -> PyResult<Self> {
Ok(match expiry {
IggyExpiry::ServerDefault() => RustIggyExpiry::ServerDefault,
IggyExpiry::ExpireDuration { duration } => {
let iggy_duration = py_delta_to_iggy_duration(duration)?;
if iggy_duration.is_zero() {
return Err(PyValueError::new_err(
"duration must be greater than zero and less than the maximum \
representable microsecond count; those values are reserved for \
IggyExpiry.ServerDefault() and IggyExpiry.NeverExpire() respectively"
.to_string(),
));
}
if iggy_duration.get_duration().as_micros() >= u64::MAX as u128 {
return Err(PyValueError::new_err(
"duration must be greater than zero and less than the maximum \
representable microsecond count; those values are reserved for \
IggyExpiry.ServerDefault() and IggyExpiry.NeverExpire() respectively"
.to_string(),
));
}
RustIggyExpiry::ExpireDuration(iggy_duration)
}
IggyExpiry::NeverExpire() => RustIggyExpiry::NeverExpire,
})
}
}
/// The maximum size of a topic.
#[gen_stub_pyclass_complex_enum]
#[pyclass]
pub enum MaxTopicSize {
/// Use the maximum topic size configured on the server, rather than an
/// explicit value set by the client.
ServerDefault(),
/// Cap the topic at this many bytes; as the topic approaches this size,
/// the server deletes the oldest sealed segments to make room for new
/// messages.
///
/// `bytes` must be greater than zero and less than the maximum value of
/// an unsigned 64-bit integer: those two values are reserved on the wire
/// for `ServerDefault` and `Unlimited` respectively, so a `Custom` size
/// at either boundary raises `ValueError` when passed to
/// `create_topic`/`update_topic`.
Custom { bytes: u64 },
/// Do not cap the topic size; it may grow without bound.
Unlimited(),
}
impl From<RustMaxTopicSize> for MaxTopicSize {
fn from(max_size: RustMaxTopicSize) -> Self {
match max_size {
RustMaxTopicSize::ServerDefault => MaxTopicSize::ServerDefault(),
RustMaxTopicSize::Custom(size) => MaxTopicSize::Custom {
bytes: size.as_bytes_u64(),
},
RustMaxTopicSize::Unlimited => MaxTopicSize::Unlimited(),
}
}
}
impl TryFrom<&MaxTopicSize> for RustMaxTopicSize {
type Error = PyErr;
fn try_from(max_size: &MaxTopicSize) -> PyResult<Self> {
Ok(match max_size {
MaxTopicSize::ServerDefault() => RustMaxTopicSize::ServerDefault,
MaxTopicSize::Custom { bytes } => {
if *bytes == 0 || *bytes == u64::MAX {
return Err(PyValueError::new_err(
"bytes must be greater than zero and less than u64::MAX".to_string(),
));
}
RustMaxTopicSize::Custom(IggyByteSize::from(*bytes))
}
MaxTopicSize::Unlimited() => RustMaxTopicSize::Unlimited,
})
}
}
#[gen_stub_pyclass]
#[pyclass]
pub struct Topic {
pub(crate) inner: RustTopic,
}
impl From<RustTopic> for Topic {
fn from(topic: RustTopic) -> Self {
Self { inner: topic }
}
}
#[gen_stub_pymethods]
#[pymethods]
impl Topic {
/// The unique identifier (numeric) of the topic.
#[getter]
pub fn id(&self) -> u32 {
self.inner.id
}
/// The unique name of the topic.
#[getter]
pub fn name(&self) -> String {
self.inner.name.to_string()
}
/// The total number of messages in the topic.
#[getter]
pub fn messages_count(&self) -> u64 {
self.inner.messages_count
}
/// The total number of partitions in the topic.
#[getter]
pub fn partitions_count(&self) -> u32 {
self.inner.partitions_count
}
/// The timestamp when the topic was created, in microseconds.
#[getter]
pub fn created_at(&self) -> u64 {
self.inner.created_at.as_micros()
}
/// The total size of the topic in bytes.
#[getter]
pub fn size(&self) -> u64 {
self.inner.size.as_bytes_u64()
}
/// The expiry of the messages in the topic.
#[getter]
pub fn message_expiry(&self) -> PyResult<IggyExpiry> {
self.inner.message_expiry.try_into()
}
/// Compression algorithm for the topic.
#[getter]
pub fn compression_algorithm(&self) -> String {
self.inner.compression_algorithm.to_string()
}
/// The maximum size of the topic.
#[getter]
pub fn max_topic_size(&self) -> MaxTopicSize {
self.inner.max_topic_size.into()
}
/// Options the creating client set explicitly.
///
/// The same `dict[HeaderKey, HeaderValue]` that `ReceiveMessage.user_headers`
/// returns, since options ride that codec; call `to_scalar_dict()` for the
/// plain-scalar form.
#[getter]
pub fn options<'a>(&self, py: Python<'a>) -> PyResult<Bound<'a, UserHeaders>> {
options_by_provenance(py, &self.inner.options, true)
}
/// Options admission resolved for the keys the client did not send.
///
/// Same shape as `options`. These would have resolved differently
/// under another server configuration.
#[getter]
pub fn derived_options<'a>(&self, py: Python<'a>) -> PyResult<Bound<'a, UserHeaders>> {
options_by_provenance(py, &self.inner.options, false)
}
}
#[gen_stub_pyclass]
#[pyclass]
pub struct TopicDetails {
pub(crate) inner: RustTopicDetails,
}
impl From<RustTopicDetails> for TopicDetails {
fn from(topic_details: RustTopicDetails) -> Self {
Self {
inner: topic_details,
}
}
}
#[gen_stub_pymethods]
#[pymethods]
impl TopicDetails {
/// The unique identifier (numeric) of the topic.
#[getter]
pub fn id(&self) -> u32 {
self.inner.id
}
/// The unique name of the topic.
#[getter]
pub fn name(&self) -> String {
self.inner.name.to_string()
}
/// The total number of messages in the topic.
#[getter]
pub fn messages_count(&self) -> u64 {
self.inner.messages_count
}
/// The total number of partitions in the topic.
#[getter]
pub fn partitions_count(&self) -> u32 {
self.inner.partitions_count
}
/// The timestamp when the topic was created, in microseconds.
#[getter]
pub fn created_at(&self) -> u64 {
self.inner.created_at.as_micros()
}
/// The total size of the topic in bytes.
#[getter]
pub fn size(&self) -> u64 {
self.inner.size.as_bytes_u64()
}
/// The expiry of the messages in the topic.
#[getter]
pub fn message_expiry(&self) -> PyResult<IggyExpiry> {
self.inner.message_expiry.try_into()
}
/// Compression algorithm for the topic.
#[getter]
pub fn compression_algorithm(&self) -> String {
self.inner.compression_algorithm.to_string()
}
/// The maximum size of the topic.
#[getter]
pub fn max_topic_size(&self) -> MaxTopicSize {
self.inner.max_topic_size.into()
}
/// Options the creating client set explicitly.
///
/// The same `dict[HeaderKey, HeaderValue]` that `ReceiveMessage.user_headers`
/// returns, since options ride that codec; call `to_scalar_dict()` for the
/// plain-scalar form.
#[getter]
pub fn options<'a>(&self, py: Python<'a>) -> PyResult<Bound<'a, UserHeaders>> {
options_by_provenance(py, &self.inner.options, true)
}
/// Options admission resolved for the keys the client did not send.
///
/// Same shape as `options`. These would have resolved differently
/// under another server configuration.
#[getter]
pub fn derived_options<'a>(&self, py: Python<'a>) -> PyResult<Bound<'a, UserHeaders>> {
options_by_provenance(py, &self.inner.options, false)
}
/// The collection of partitions in the topic.
///
/// Rebuilds the list from scratch on every access; cache the result
/// rather than reading this repeatedly in a loop.
#[getter]
pub fn partitions(&self) -> Vec<Partition> {
self.inner
.partitions
.iter()
.cloned()
.map(Partition::from)
.collect()
}
}
#[gen_stub_pyclass]
#[pyclass]
pub struct Partition {
pub(crate) inner: RustPartition,
}
impl From<RustPartition> for Partition {
fn from(partition: RustPartition) -> Self {
Self { inner: partition }
}
}
#[gen_stub_pymethods]
#[pymethods]
impl Partition {
/// The unique identifier (numeric) of the partition.
#[getter]
pub fn id(&self) -> u32 {
self.inner.id
}
/// The timestamp of the partition creation, in microseconds.
#[getter]
pub fn created_at(&self) -> u64 {
self.inner.created_at.as_micros()
}
/// The number of segments in the partition.
#[getter]
pub fn segments_count(&self) -> u32 {
self.inner.segments_count
}
/// The current offset of the partition.
#[getter]
pub fn current_offset(&self) -> u64 {
self.inner.current_offset
}
/// The size of the partition in bytes.
#[getter]
pub fn size(&self) -> u64 {
self.inner.size.as_bytes_u64()
}
/// The number of messages in the partition.
#[getter]
pub fn messages_count(&self) -> u64 {
self.inner.messages_count
}
}