use std::collections::HashSet;
use std::time::Duration;
use pyo3::prelude::*;
#[pyclass(name = "Retry", frozen)]
#[derive(Clone)]
pub struct PyRetry {
inner: eggfetch_core::RetryPolicy,
}
#[pymethods]
impl PyRetry {
#[new]
#[pyo3(signature = (
max_attempts=1,
backoff_factor=0.5,
max_delay=30.0,
initial_delay=0.5,
statuses=None,
respect_retry_after=false,
allow_post=false,
allow_put=false,
allow_delete=false,
allow_patch=false,
max_elapsed=None,
))]
#[allow(
clippy::too_many_arguments,
clippy::needless_pass_by_value,
clippy::unnecessary_wraps,
clippy::fn_params_excessive_bools
)]
fn new(
max_attempts: usize,
backoff_factor: f64,
max_delay: f64,
initial_delay: f64,
statuses: Option<HashSet<u16>>,
respect_retry_after: bool,
allow_post: bool,
allow_put: bool,
allow_delete: bool,
allow_patch: bool,
max_elapsed: Option<f64>,
) -> PyResult<Self> {
for (name, val) in [
("max_delay", Some(max_delay)),
("initial_delay", Some(initial_delay)),
("max_elapsed", max_elapsed),
] {
if let Some(v) = val {
if !v.is_finite() || v < 0.0 {
return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
"{name} must be a finite, non-negative number"
)));
}
}
}
let mut builder = eggfetch_core::RetryPolicy::builder()
.max_attempts(max_attempts)
.backoff_factor(backoff_factor)
.max_delay(Duration::from_secs_f64(max_delay))
.initial_delay(Duration::from_secs_f64(initial_delay))
.respect_retry_after(respect_retry_after);
if let Some(s) = &statuses {
builder = builder.retry_statuses(s.iter().copied());
}
if allow_post {
builder = builder.allow_post_retry();
}
if allow_put {
builder = builder.allow_put_retry();
}
if allow_delete {
builder = builder.allow_delete_retry();
}
if allow_patch {
builder = builder.allow_patch_retry();
}
if let Some(elapsed) = max_elapsed {
builder = builder.max_elapsed(Duration::from_secs_f64(elapsed));
}
let inner = builder.build();
Ok(Self { inner })
}
fn __repr__(&self) -> String {
format!(
"Retry(max_attempts={}, backoff_factor={})",
self.inner.max_attempts(),
self.inner.backoff().factor(),
)
}
#[getter]
fn max_attempts(&self) -> usize {
self.inner.max_attempts()
}
#[getter]
fn backoff_factor(&self) -> f64 {
self.inner.backoff().factor()
}
#[getter]
fn initial_delay(&self) -> f64 {
self.inner.backoff().initial_delay().as_secs_f64()
}
#[getter]
fn max_delay(&self) -> f64 {
self.inner.backoff().max_delay().as_secs_f64()
}
#[getter]
fn respect_retry_after(&self) -> bool {
self.inner.respect_retry_after()
}
#[getter]
fn statuses(&self) -> Vec<u16> {
self.inner.status_policy().statuses().to_vec()
}
#[getter]
fn max_elapsed(&self) -> Option<f64> {
self.inner.max_elapsed().map(|d| d.as_secs_f64())
}
#[getter]
fn allow_post(&self) -> bool {
self.inner.method_policy().is_retryable(&http::Method::POST)
}
#[getter]
fn allow_put(&self) -> bool {
self.inner.method_policy().is_retryable(&http::Method::PUT)
}
#[getter]
fn allow_delete(&self) -> bool {
self.inner
.method_policy()
.is_retryable(&http::Method::DELETE)
}
#[getter]
fn allow_patch(&self) -> bool {
self.inner
.method_policy()
.is_retryable(&http::Method::PATCH)
}
}
impl PyRetry {
pub(crate) fn policy(&self) -> eggfetch_core::RetryPolicy {
self.inner.clone()
}
pub(crate) fn default_policy() -> eggfetch_core::RetryPolicy {
eggfetch_core::RetryPolicy::builder()
.max_attempts(3)
.build()
}
}
pub(crate) fn parse_retry_option(
val: Option<&Bound<'_, PyAny>>,
) -> PyResult<Option<eggfetch_core::RetryPolicy>> {
match val {
None => Ok(None),
Some(v) => {
if v.is_none() {
return Ok(None);
}
if let Ok(flag) = v.extract::<bool>() {
return if flag {
Ok(Some(PyRetry::default_policy()))
} else {
Ok(Some(eggfetch_core::RetryPolicy::default()))
};
}
if let Ok(retry) = v.extract::<PyRetry>() {
return Ok(Some(retry.policy()));
}
Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
"retries must be True, False, None, or a Retry instance",
))
}
}
}