use pyo3::prelude::*;
use std::time::Duration;
#[pyclass(name = "RetryPolicy")]
#[derive(Clone)]
pub struct PyRetryPolicy {
inner: crate::retry::RetryPolicy,
}
#[pyclass(name = "BackoffStrategy")]
#[derive(Clone)]
pub struct PyBackoffStrategy {
inner: crate::retry::BackoffStrategy,
}
#[pyclass(name = "RetryCondition")]
#[derive(Clone)]
pub struct PyRetryCondition {
inner: crate::retry::RetryCondition,
}
#[pyclass(name = "RetryPolicyBuilder")]
#[derive(Clone, Debug)]
pub struct PyRetryPolicyBuilder {
max_attempts: Option<i32>,
backoff_strategy: Option<crate::retry::BackoffStrategy>,
initial_delay: Option<Duration>,
max_delay: Option<Duration>,
retry_condition: Option<crate::retry::RetryCondition>,
with_jitter: Option<bool>,
}
#[pymethods]
impl PyRetryPolicy {
#[staticmethod]
pub fn builder() -> PyRetryPolicyBuilder {
PyRetryPolicyBuilder {
max_attempts: None,
backoff_strategy: None,
initial_delay: None,
max_delay: None,
retry_condition: None,
with_jitter: None,
}
}
#[staticmethod]
#[allow(clippy::should_implement_trait)]
pub fn default() -> Self {
Self {
inner: crate::retry::RetryPolicy::default(),
}
}
pub fn should_retry(&self, attempt: i32, _error_type: &str) -> bool {
attempt < self.inner.max_attempts
}
pub fn calculate_delay(&self, attempt: i32) -> f64 {
let duration = self.inner.calculate_delay(attempt);
duration.as_secs_f64()
}
#[getter]
pub fn max_attempts(&self) -> i32 {
self.inner.max_attempts
}
#[getter]
pub fn initial_delay(&self) -> f64 {
self.inner.initial_delay.as_secs_f64()
}
#[getter]
pub fn max_delay(&self) -> f64 {
self.inner.max_delay.as_secs_f64()
}
#[getter]
pub fn with_jitter(&self) -> bool {
self.inner.jitter
}
pub fn __repr__(&self) -> String {
format!(
"RetryPolicy(max_attempts={}, initial_delay={}s, max_delay={}s, jitter={})",
self.max_attempts(),
self.initial_delay(),
self.max_delay(),
self.with_jitter()
)
}
}
#[pymethods]
impl PyBackoffStrategy {
#[staticmethod]
pub fn fixed() -> Self {
Self {
inner: crate::retry::BackoffStrategy::Fixed,
}
}
#[staticmethod]
pub fn linear(multiplier: f64) -> Self {
Self {
inner: crate::retry::BackoffStrategy::Linear { multiplier },
}
}
#[staticmethod]
pub fn exponential(base: f64, multiplier: Option<f64>) -> Self {
Self {
inner: crate::retry::BackoffStrategy::Exponential {
base,
multiplier: multiplier.unwrap_or(1.0),
},
}
}
pub fn __repr__(&self) -> String {
match &self.inner {
crate::retry::BackoffStrategy::Fixed => "BackoffStrategy.Fixed".to_string(),
crate::retry::BackoffStrategy::Linear { multiplier } => {
format!("BackoffStrategy.Linear(multiplier={})", multiplier)
}
crate::retry::BackoffStrategy::Exponential { base, multiplier } => {
format!(
"BackoffStrategy.Exponential(base={}, multiplier={})",
base, multiplier
)
}
crate::retry::BackoffStrategy::Custom { function_name } => {
format!("BackoffStrategy.Custom(function_name='{}')", function_name)
}
}
}
}
#[pymethods]
impl PyRetryCondition {
#[staticmethod]
pub fn never() -> Self {
Self {
inner: crate::retry::RetryCondition::Never,
}
}
#[staticmethod]
pub fn transient_only() -> Self {
Self {
inner: crate::retry::RetryCondition::TransientOnly,
}
}
#[staticmethod]
pub fn all_errors() -> Self {
Self {
inner: crate::retry::RetryCondition::AllErrors,
}
}
#[staticmethod]
pub fn error_pattern(patterns: Vec<String>) -> Self {
Self {
inner: crate::retry::RetryCondition::ErrorPattern { patterns },
}
}
pub fn __repr__(&self) -> String {
match &self.inner {
crate::retry::RetryCondition::Never => "RetryCondition.Never".to_string(),
crate::retry::RetryCondition::TransientOnly => {
"RetryCondition.TransientOnly".to_string()
}
crate::retry::RetryCondition::AllErrors => "RetryCondition.AllErrors".to_string(),
crate::retry::RetryCondition::ErrorPattern { patterns } => {
format!("RetryCondition.ErrorPattern(patterns={:?})", patterns)
}
}
}
}
#[pymethods]
impl PyRetryPolicyBuilder {
pub fn max_attempts(&self, attempts: i32) -> Self {
let mut new_builder = self.clone();
new_builder.max_attempts = Some(attempts);
new_builder
}
pub fn initial_delay(&self, delay_seconds: f64) -> Self {
let mut new_builder = self.clone();
new_builder.initial_delay = Some(Duration::from_secs_f64(delay_seconds));
new_builder
}
pub fn max_delay(&self, delay_seconds: f64) -> Self {
let mut new_builder = self.clone();
new_builder.max_delay = Some(Duration::from_secs_f64(delay_seconds));
new_builder
}
pub fn backoff_strategy(&self, strategy: PyBackoffStrategy) -> Self {
let mut new_builder = self.clone();
new_builder.backoff_strategy = Some(strategy.inner);
new_builder
}
pub fn retry_condition(&self, condition: PyRetryCondition) -> Self {
let mut new_builder = self.clone();
new_builder.retry_condition = Some(condition.inner);
new_builder
}
pub fn with_jitter(&self, jitter: bool) -> Self {
let mut new_builder = self.clone();
new_builder.with_jitter = Some(jitter);
new_builder
}
pub fn build(&self) -> PyRetryPolicy {
let mut builder = crate::retry::RetryPolicy::builder();
if let Some(attempts) = self.max_attempts {
builder = builder.max_attempts(attempts);
}
if let Some(strategy) = &self.backoff_strategy {
builder = builder.backoff_strategy(strategy.clone());
}
if let Some(delay) = self.initial_delay {
builder = builder.initial_delay(delay);
}
if let Some(delay) = self.max_delay {
builder = builder.max_delay(delay);
}
if let Some(condition) = &self.retry_condition {
builder = builder.retry_condition(condition.clone());
}
if let Some(jitter) = self.with_jitter {
builder = builder.with_jitter(jitter);
}
PyRetryPolicy {
inner: builder.build(),
}
}
}
impl PyRetryPolicy {
pub fn from_rust(policy: crate::retry::RetryPolicy) -> Self {
Self { inner: policy }
}
pub fn to_rust(&self) -> crate::retry::RetryPolicy {
self.inner.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_policy() {
pyo3::prepare_freethreaded_python();
let policy = PyRetryPolicy::default();
assert!(policy.max_attempts() > 0);
assert!(policy.initial_delay() > 0.0);
assert!(policy.max_delay() >= policy.initial_delay());
}
#[test]
fn test_builder_defaults() {
pyo3::prepare_freethreaded_python();
let policy = PyRetryPolicy::builder().build();
assert!(policy.max_attempts() > 0);
assert!(policy.initial_delay() > 0.0);
}
#[test]
fn test_builder_chain() {
pyo3::prepare_freethreaded_python();
let policy = PyRetryPolicy::builder()
.max_attempts(5)
.initial_delay(2.0)
.max_delay(60.0)
.with_jitter(true)
.build();
assert_eq!(policy.max_attempts(), 5);
assert!((policy.initial_delay() - 2.0).abs() < f64::EPSILON);
assert!((policy.max_delay() - 60.0).abs() < f64::EPSILON);
assert!(policy.with_jitter());
}
#[test]
fn test_should_retry() {
pyo3::prepare_freethreaded_python();
let policy = PyRetryPolicy::builder().max_attempts(3).build();
assert!(policy.should_retry(0, "some_error"));
assert!(policy.should_retry(1, "some_error"));
assert!(policy.should_retry(2, "some_error"));
assert!(!policy.should_retry(3, "some_error"));
assert!(!policy.should_retry(4, "some_error"));
}
#[test]
fn test_calculate_delay() {
pyo3::prepare_freethreaded_python();
let policy = PyRetryPolicy::builder()
.initial_delay(1.0)
.with_jitter(false)
.build();
let delay = policy.calculate_delay(0);
assert!(delay > 0.0);
}
#[test]
fn test_retry_policy_repr() {
pyo3::prepare_freethreaded_python();
let policy = PyRetryPolicy::default();
let repr = policy.__repr__();
assert!(repr.starts_with("RetryPolicy("));
assert!(repr.contains("max_attempts="));
assert!(repr.contains("initial_delay="));
}
#[test]
fn test_backoff_strategy_fixed() {
pyo3::prepare_freethreaded_python();
let s = PyBackoffStrategy::fixed();
assert_eq!(s.__repr__(), "BackoffStrategy.Fixed");
}
#[test]
fn test_backoff_strategy_linear() {
pyo3::prepare_freethreaded_python();
let s = PyBackoffStrategy::linear(2.0);
let repr = s.__repr__();
assert!(repr.contains("Linear"));
assert!(repr.contains("2"));
}
#[test]
fn test_backoff_strategy_exponential() {
pyo3::prepare_freethreaded_python();
let s = PyBackoffStrategy::exponential(2.0, Some(1.5));
let repr = s.__repr__();
assert!(repr.contains("Exponential"));
assert!(repr.contains("2"));
}
#[test]
fn test_retry_condition_never() {
pyo3::prepare_freethreaded_python();
let c = PyRetryCondition::never();
assert_eq!(c.__repr__(), "RetryCondition.Never");
}
#[test]
fn test_retry_condition_transient_only() {
pyo3::prepare_freethreaded_python();
let c = PyRetryCondition::transient_only();
assert_eq!(c.__repr__(), "RetryCondition.TransientOnly");
}
#[test]
fn test_retry_condition_all_errors() {
pyo3::prepare_freethreaded_python();
let c = PyRetryCondition::all_errors();
assert_eq!(c.__repr__(), "RetryCondition.AllErrors");
}
#[test]
fn test_retry_condition_error_pattern() {
pyo3::prepare_freethreaded_python();
let c = PyRetryCondition::error_pattern(vec!["timeout".to_string(), "conn".to_string()]);
let repr = c.__repr__();
assert!(repr.contains("ErrorPattern"));
assert!(repr.contains("timeout"));
}
#[test]
fn test_from_rust_to_rust_roundtrip() {
pyo3::prepare_freethreaded_python();
let rust_policy = crate::retry::RetryPolicy::default();
let py_policy = PyRetryPolicy::from_rust(rust_policy.clone());
let roundtripped = py_policy.to_rust();
assert_eq!(rust_policy.max_attempts, roundtripped.max_attempts);
assert_eq!(rust_policy.initial_delay, roundtripped.initial_delay);
assert_eq!(rust_policy.max_delay, roundtripped.max_delay);
}
}