blob: ed6e2a14f0aad562c7377c6c74bdb3697abb241a [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::{
AutoLogin as RustAutoLogin, Credentials as RustCredentials,
TcpClientConfig as RustTcpClientConfig, TcpClientConfigBuilder,
TcpClientReconnectionConfig as RustTcpClientReconnectionConfig,
};
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::PyDelta;
use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods};
use pyo3_stub_gen::impl_stub_type;
use secrecy::SecretString;
use std::sync::Arc;
use crate::duration::{
duration_repr, iggy_duration_to_py_delta, py_delta_to_iggy_duration, reject_zero,
};
/// The credentials replayed by the client every time it (re)connects.
///
/// `IggyClient` only recovers a lost session when it has credentials to replay,
/// so a long-running consumer should pass one of the enabled variants.
#[gen_stub_pyclass]
#[pyclass(from_py_object)]
#[derive(Clone)]
pub struct AutoLogin {
pub(crate) inner: RustAutoLogin,
}
#[gen_stub_pymethods]
#[pymethods]
impl AutoLogin {
/// No automatic login. `login_user()` must be called by hand after every connect.
#[staticmethod]
fn disabled() -> Self {
Self {
inner: RustAutoLogin::Disabled,
}
}
/// Log in with the given username and password on every connect.
#[staticmethod]
fn username_password(username: String, password: String) -> Self {
Self {
inner: RustAutoLogin::Enabled(RustCredentials::UsernamePassword(
username,
SecretString::from(password),
)),
}
}
/// Log in with the given personal access token on every connect.
#[staticmethod]
fn personal_access_token(token: String) -> Self {
Self {
inner: RustAutoLogin::Enabled(RustCredentials::PersonalAccessToken(
SecretString::from(token),
)),
}
}
/// Whether automatic login is enabled.
#[getter]
fn enabled(&self) -> bool {
matches!(self.inner, RustAutoLogin::Enabled(_))
}
/// The username to log in with, or `None` for the disabled and token variants.
#[gen_stub(override_return_type(type_repr = "builtins.str | None"))]
#[getter]
fn username(&self) -> Option<String> {
match &self.inner {
RustAutoLogin::Enabled(RustCredentials::UsernamePassword(username, _)) => {
Some(username.clone())
}
_ => None,
}
}
fn __repr__(&self) -> String {
match &self.inner {
RustAutoLogin::Disabled => "AutoLogin.disabled()".to_owned(),
RustAutoLogin::Enabled(RustCredentials::UsernamePassword(username, _)) => {
format!("AutoLogin.username_password({username:?}, ...)")
}
RustAutoLogin::Enabled(RustCredentials::PersonalAccessToken(_)) => {
"AutoLogin.personal_access_token(...)".to_owned()
}
}
}
}
/// How the TCP client reconnects after the connection to the server is lost.
#[gen_stub_pyclass]
#[pyclass(from_py_object)]
#[derive(Clone)]
pub struct TcpReconnectionConfig {
pub(crate) inner: RustTcpClientReconnectionConfig,
}
#[gen_stub_pymethods]
#[pymethods]
impl TcpReconnectionConfig {
/// Constructs a reconnection policy.
///
/// Args:
/// enabled: Whether to reconnect at all. Defaults to enabled.
/// max_retries: Passes over the known endpoints after the first, or
/// `None` for unlimited; `0` still makes that first pass. One pass
/// tries the endpoint the client is on, the address it was
/// configured with, and every node the roster named, so this counts
/// passes rather than dials. Defaults
/// to unlimited, which means a call awaited while the server is
/// down never returns: `connect()`, `send_messages()` and
/// `poll_messages()` all wait inside the retry loop. Set a finite
/// number for request/reply style usage, so a call fails instead.
/// interval: Delay between passes. Defaults to 1 second. The first pass
/// runs at once when more than one endpoint is known.
/// reestablish_after: Cooldown before redialing the endpoint of the last
/// successful connection, measured from when it was established, so
/// a session that outlived the interval is redialed at once. Owed to
/// that endpoint alone. Defaults to 5 seconds.
///
/// Raises:
/// ValueError: If a duration is negative, if `max_retries` is outside the
/// range of an unsigned 32-bit integer, or if `interval` is zero.
#[new]
#[pyo3(signature = (*, enabled=None, max_retries=None, interval=None, reestablish_after=None))]
fn new(
#[gen_stub(override_type(type_repr = "builtins.bool | None"))] enabled: Option<bool>,
#[gen_stub(override_type(type_repr = "builtins.int | None"))] max_retries: Option<i64>,
#[gen_stub(override_type(type_repr = "datetime.timedelta | None", imports=("datetime")))]
interval: Option<Py<PyDelta>>,
#[gen_stub(override_type(type_repr = "datetime.timedelta | None", imports=("datetime")))]
reestablish_after: Option<Py<PyDelta>>,
) -> PyResult<Self> {
let defaults = RustTcpClientReconnectionConfig::default();
let enabled = enabled.unwrap_or(defaults.enabled);
let max_retries = max_retries
.map(|max_retries| {
u32::try_from(max_retries).map_err(|_| {
PyValueError::new_err(format!(
"'max_retries' must be between 0 and {}",
u32::MAX
))
})
})
.transpose()?;
let interval = interval
.as_ref()
.map(py_delta_to_iggy_duration)
.transpose()?
.map(|interval| reject_zero(interval, "interval"))
.transpose()?
.unwrap_or(defaults.interval);
Ok(Self {
inner: RustTcpClientReconnectionConfig {
enabled,
max_retries,
interval,
reestablish_after: reestablish_after
.as_ref()
.map(py_delta_to_iggy_duration)
.transpose()?
.unwrap_or(defaults.reestablish_after),
},
})
}
#[getter]
fn enabled(&self) -> bool {
self.inner.enabled
}
#[gen_stub(override_return_type(type_repr = "builtins.int | None"))]
#[getter]
fn max_retries(&self) -> Option<u32> {
self.inner.max_retries
}
#[gen_stub(override_return_type(type_repr = "datetime.timedelta", imports=("datetime")))]
#[getter]
fn interval<'a>(&self, py: Python<'a>) -> PyResult<Bound<'a, PyDelta>> {
iggy_duration_to_py_delta(py, self.inner.interval.get())
}
#[gen_stub(override_return_type(type_repr = "datetime.timedelta", imports=("datetime")))]
#[getter]
fn reestablish_after<'a>(&self, py: Python<'a>) -> PyResult<Bound<'a, PyDelta>> {
iggy_duration_to_py_delta(py, self.inner.reestablish_after)
}
fn __repr__(&self) -> String {
let max_retries = match self.inner.max_retries {
Some(max_retries) => max_retries.to_string(),
None => "None".to_owned(),
};
format!(
"TcpReconnectionConfig(enabled={}, max_retries={max_retries}, interval={}, reestablish_after={})",
python_bool(self.inner.enabled),
duration_repr(self.inner.interval.get()),
duration_repr(self.inner.reestablish_after),
)
}
}
/// Configuration for the TCP transport, accepted by `IggyClient(...)`.
///
/// Every field is keyword-only and optional.
#[gen_stub_pyclass]
#[pyclass(from_py_object)]
#[derive(Clone)]
pub struct TcpConfig {
inner: Arc<RustTcpClientConfig>,
}
impl TcpConfig {
/// The configuration in the shape `TcpClient::create` expects.
pub(crate) fn client_config(&self) -> Arc<RustTcpClientConfig> {
self.inner.clone()
}
}
#[gen_stub_pymethods]
#[pymethods]
impl TcpConfig {
/// Constructs a TCP configuration.
///
/// Args:
/// server_address: `host:port` of the Iggy server. Defaults to `127.0.0.1:8090`.
/// auto_login: Credentials replayed on every connect. Defaults to `AutoLogin.disabled()`.
/// reconnection: Reconnection policy. Defaults to `TcpReconnectionConfig()`.
/// heartbeat_interval: Interval of heartbeats sent by the client. Defaults to 5 seconds.
/// tls_enabled: Whether to connect over TLS. Defaults to disabled.
/// tls_domain: Domain to validate the certificate against. Empty means it is
/// taken from `server_address`.
/// tls_ca_file: Path to the CA file for TLS. Read only when `tls_enabled`
/// and `tls_validate_certificate` are both on; with either one off it
/// is kept but never consulted, so pairing it with
/// `tls_validate_certificate=False` pins nothing.
/// tls_validate_certificate: Whether to validate the server certificate.
/// Defaults to validating. Disabling this accepts any certificate the
/// server presents, including self-signed and mismatched ones, and
/// takes precedence over `tls_ca_file`; intended for local development
/// only.
/// nodelay: Disable the Nagle algorithm for the TCP socket. Defaults to
/// leaving it on.
///
/// Raises:
/// ValueError: If `server_address` is not a valid `host:port` pair, if a
/// duration is negative, or if `heartbeat_interval` is zero.
#[new]
#[pyo3(signature = (
*,
server_address=None,
auto_login=None,
reconnection=None,
heartbeat_interval=None,
tls_enabled=None,
tls_domain=None,
tls_ca_file=None,
tls_validate_certificate=None,
nodelay=None,
))]
#[allow(clippy::too_many_arguments)]
fn new(
#[gen_stub(override_type(type_repr = "builtins.str | None"))] server_address: Option<
String,
>,
#[gen_stub(override_type(type_repr = "AutoLogin | None"))] auto_login: Option<AutoLogin>,
#[gen_stub(override_type(type_repr = "TcpReconnectionConfig | None"))] reconnection: Option<
TcpReconnectionConfig,
>,
#[gen_stub(override_type(type_repr = "datetime.timedelta | None", imports=("datetime")))]
heartbeat_interval: Option<Py<PyDelta>>,
#[gen_stub(override_type(type_repr = "builtins.bool | None"))] tls_enabled: Option<bool>,
#[gen_stub(override_type(type_repr = "builtins.str | None"))] tls_domain: Option<String>,
#[gen_stub(override_type(type_repr = "builtins.str | None"))] tls_ca_file: Option<String>,
#[gen_stub(override_type(type_repr = "builtins.bool | None"))]
tls_validate_certificate: Option<bool>,
#[gen_stub(override_type(type_repr = "builtins.bool | None"))] nodelay: Option<bool>,
) -> PyResult<Self> {
// The builder starts from `TcpClientConfig::default()`, and its `build()`
// trims and validates the address whether or not one was set here.
let mut builder = TcpClientConfigBuilder::new();
if let Some(server_address) = server_address {
builder = builder.with_server_address(server_address);
}
let mut inner = builder
.build()
.map_err(|e| PyValueError::new_err(e.to_string()))?;
if let Some(auto_login) = auto_login {
inner.auto_login = auto_login.inner;
}
if let Some(reconnection) = reconnection {
inner.reconnection = reconnection.inner;
}
if let Some(heartbeat_interval) = heartbeat_interval {
inner.heartbeat_interval = reject_zero(
py_delta_to_iggy_duration(&heartbeat_interval)?,
"heartbeat_interval",
)?;
}
if let Some(tls_enabled) = tls_enabled {
inner.tls_enabled = tls_enabled;
}
if let Some(tls_domain) = tls_domain {
inner.tls_domain = tls_domain;
}
if tls_ca_file.is_some() {
inner.tls_ca_file = tls_ca_file;
}
if let Some(tls_validate_certificate) = tls_validate_certificate {
inner.tls_validate_certificate = tls_validate_certificate;
}
if let Some(nodelay) = nodelay {
inner.nodelay = nodelay;
}
Ok(Self {
inner: Arc::new(inner),
})
}
#[getter]
fn server_address(&self) -> String {
self.inner.server_address.clone()
}
#[getter]
fn auto_login(&self) -> AutoLogin {
AutoLogin {
inner: self.inner.auto_login.clone(),
}
}
#[getter]
fn reconnection(&self) -> TcpReconnectionConfig {
TcpReconnectionConfig {
inner: self.inner.reconnection.clone(),
}
}
#[gen_stub(override_return_type(type_repr = "datetime.timedelta", imports=("datetime")))]
#[getter]
fn heartbeat_interval<'a>(&self, py: Python<'a>) -> PyResult<Bound<'a, PyDelta>> {
iggy_duration_to_py_delta(py, self.inner.heartbeat_interval.get())
}
#[getter]
fn tls_enabled(&self) -> bool {
self.inner.tls_enabled
}
#[getter]
fn tls_domain(&self) -> String {
self.inner.tls_domain.clone()
}
#[gen_stub(override_return_type(type_repr = "builtins.str | None"))]
#[getter]
fn tls_ca_file(&self) -> Option<String> {
self.inner.tls_ca_file.clone()
}
#[getter]
fn tls_validate_certificate(&self) -> bool {
self.inner.tls_validate_certificate
}
#[getter]
fn nodelay(&self) -> bool {
self.inner.nodelay
}
fn __repr__(&self) -> String {
let tls_ca_file = match &self.inner.tls_ca_file {
Some(tls_ca_file) => format!("{tls_ca_file:?}"),
None => "None".to_owned(),
};
format!(
"TcpConfig(server_address={:?}, auto_login={}, reconnection={}, heartbeat_interval={}, tls_enabled={}, tls_domain={:?}, tls_ca_file={tls_ca_file}, tls_validate_certificate={}, nodelay={})",
self.inner.server_address,
self.auto_login().__repr__(),
self.reconnection().__repr__(),
duration_repr(self.inner.heartbeat_interval.get()),
python_bool(self.inner.tls_enabled),
self.inner.tls_domain,
python_bool(self.inner.tls_validate_certificate),
python_bool(self.inner.nodelay),
)
}
}
fn python_bool(value: bool) -> &'static str {
if value { "True" } else { "False" }
}
/// What `IggyClient(...)` accepts: a bare `host:port` or a full `TcpConfig`.
#[derive(FromPyObject)]
pub enum PyClientConfig {
#[pyo3(transparent)]
Config(TcpConfig),
#[pyo3(transparent, annotation = "str")]
ServerAddress(String),
}
impl_stub_type!(PyClientConfig = TcpConfig | String);