Abstract rate limiting away into ApiClient

This commit is contained in:
Matteo Settenvini 2022-08-05 13:20:37 +02:00
parent f2eadbae2b
commit fe363c0e26
Signed by: matteo
GPG key ID: 8576CC1AD97D42DF
8 changed files with 264 additions and 192 deletions

View file

@ -64,8 +64,26 @@ impl ApiClient {
})
}
pub fn rest(&self) -> &reqwest::Client {
&self.rest
pub async fn rest<'a, F, R>(&'a self, submitter: F) -> R::Output
where
F: Fn(&'a reqwest::Client) -> R,
R: std::future::Future<Output = Result<reqwest::Response, anyhow::Error>>,
{
let _ = self.rest_semaphore.acquire().await.unwrap();
let mut retries = 0;
loop {
let result: R::Output = submitter(&self.rest).await;
match result {
Ok(response) => break Ok(response),
Err(_) if retries < 10 => {
retries += 1;
std::thread::sleep(retries * std::time::Duration::from_millis(500));
continue;
}
err => break err,
}
}
}
pub fn base_url(&self) -> &Url {
@ -91,20 +109,21 @@ impl ApiClient {
{
let report_method = reqwest::Method::from_bytes(b"REPORT").unwrap();
let events_xml = self
.rest()
.request(
report_method,
// TODO extract into helper method
self.caldav_base_url.join(&format!(
"calendars/{}/{}",
self.username(),
calendar_name.to_lowercase().replace(" ", "-")
))?,
)
.header("Prefer", "return-minimal")
.header("Content-Type", "application/xml; charset=utf-8")
.header("Depth", 1)
.body(format!(
.rest(|client| async {
let response = client
.request(
report_method.clone(),
// TODO extract into helper method
self.caldav_base_url.join(&format!(
"calendars/{}/{}",
self.username(),
calendar_name.to_lowercase().replace(" ", "-")
))?,
)
.header("Prefer", "return-minimal")
.header("Content-Type", "application/xml; charset=utf-8")
.header("Depth", 1)
.body(format!(
"<c:calendar-query xmlns:d=\"DAV:\" xmlns:c=\"urn:ietf:params:xml:ns:caldav\">
<d:prop>
<c:calendar-data />
@ -135,7 +154,10 @@ impl ApiClient {
.format(constants::ICAL_UTCTIME_FMT)
.to_string()
))
.send()
.send()
.await;
Ok(response?)
})
.await?
.text()
.await?;

View file

@ -63,9 +63,12 @@ where
// TODO code duplicated with schedule_csv::get_all_recipes
let recipe_url = format!("apps/cookbook/api/recipes/{id}");
let response = api_client
.rest()
.get(api_client.base_url().join(&recipe_url).unwrap())
.send()
.rest(|client| async {
Ok(client
.get(api_client.base_url().join(&recipe_url).unwrap())
.send()
.await?)
})
.await
.expect(&format!("Cannot fetch recipe with id {}", id));
@ -119,9 +122,13 @@ async fn save_grocery_list(api_client: &ApiClient, filename: &str, contents: &st
.and_then(|url| {
futures::executor::block_on(async {
let response = api_client
.rest()
.request(Method::from_bytes(b"MKCOL").unwrap(), url.clone())
.send()
.rest(|client| async {
let r = client
.request(Method::from_bytes(b"MKCOL").unwrap(), url.clone())
.send()
.await;
Ok(r?)
})
.await;
match response.map(|r| r.status()) {
@ -141,11 +148,15 @@ async fn save_grocery_list(api_client: &ApiClient, filename: &str, contents: &st
let file_url = dav_base_url.join(filename).unwrap();
log::info!("Saving grocery list to {}", &file_url);
let response = api_client
.rest()
.put(file_url.clone())
.header("Content-Type", "text/markdown; charset=utf-8")
.body(contents.to_owned())
.send()
.rest(|client| async {
let r = client
.put(file_url.clone())
.header("Content-Type", "text/markdown; charset=utf-8")
.body(contents.to_owned())
.send()
.await;
Ok(r?)
})
.await?;
match response.status() {

View file

@ -10,12 +10,16 @@ where
{
for url in urls {
let response = api_client
.rest()
.post(api_client.base_url().join("apps/cookbook/import")?)
.json(&serde_json::json!({
"url": url.as_ref(),
}))
.send()
.rest(|client| async {
let r = client
.post(api_client.base_url().join("apps/cookbook/import")?)
.json(&serde_json::json!({
"url": url.as_ref(),
}))
.send()
.await;
Ok(r?)
})
.await?;
if ![StatusCode::OK, StatusCode::CONFLICT].contains(&response.status()) {
anyhow::bail!(

View file

@ -4,23 +4,24 @@
use {crate::api_client::ApiClient, crate::constants, anyhow::Result, reqwest::Method};
pub async fn with(api_client: &ApiClient, calendar_name: &str) -> Result<()> {
let report_method = Method::from_bytes(b"REPORT")?;
let report_method: Method = Method::from_bytes(b"REPORT")?;
let events_xml = api_client
.rest()
.request(
report_method,
// TODO extract into helper method
api_client.caldav_base_url().join(&format!(
"calendars/{}/{}/",
api_client.username(),
calendar_name.to_lowercase().replace(" ", "-")
))?,
)
.header("Prefer", "return-minimal")
.header("Content-Type", "application/xml; charset=utf-8")
.header("Depth", 1)
.body(format!(
"<c:calendar-query xmlns:d=\"DAV:\" xmlns:c=\"urn:ietf:params:xml:ns:caldav\">
.rest(|client| async {
let response = client
.request(
report_method.clone(),
// TODO extract into helper method
api_client.caldav_base_url().join(&format!(
"calendars/{}/{}/",
api_client.username(),
calendar_name.to_lowercase().replace(" ", "-")
))?,
)
.header("Prefer", "return-minimal")
.header("Content-Type", "application/xml; charset=utf-8")
.header("Depth", 1)
.body(format!(
"<c:calendar-query xmlns:d=\"DAV:\" xmlns:c=\"urn:ietf:params:xml:ns:caldav\">
<d:prop>
<d:getetag/>
</d:prop>
@ -33,9 +34,12 @@ pub async fn with(api_client: &ApiClient, calendar_name: &str) -> Result<()> {
</c:filter>
</c:calendar-query>
",
constants::CALENDAR_PROVIDER,
))
.send()
constants::CALENDAR_PROVIDER,
))
.send()
.await;
Ok(response?)
})
.await?
.text()
.await?;
@ -52,9 +56,12 @@ pub async fn with(api_client: &ApiClient, calendar_name: &str) -> Result<()> {
for url in events_to_purge {
api_client
.rest()
.delete(api_client.base_url().join(&url)?)
.send()
.rest(|client| async {
Ok(client
.delete(api_client.base_url().join(&url)?)
.send()
.await?)
})
.await?;
log::debug!("Purged {}", &url);
}

View file

@ -5,9 +5,13 @@ use {crate::api_client::ApiClient, crate::recipe, anyhow::Result};
pub async fn with(api_client: &ApiClient) -> Result<()> {
let recipes = api_client
.rest()
.get(api_client.base_url().join("apps/cookbook/api/recipes")?)
.send()
.rest(|client| async {
let response = client
.get(api_client.base_url().join("apps/cookbook/api/recipes")?)
.send()
.await;
Ok(response?)
})
.await?;
println!("{:#?}", recipes.json::<Vec<recipe::Metadata>>().await?);
todo!();

View file

@ -81,30 +81,53 @@ where
async fn get_all_recipes(api_client: &ApiClient) -> Result<HashMap<String, Rc<recipe::Recipe>>> {
log::info!("Getting list of all recipes");
let metadata = api_client
.rest()
.get(api_client.base_url().join("apps/cookbook/api/recipes")?)
.send()
.rest(|client| async {
let response = client
.get(api_client.base_url().join("apps/cookbook/api/recipes")?)
.send()
.await;
Ok(response?)
})
.await?
.json::<Vec<recipe::Metadata>>()
.await?;
let recipes = metadata.iter().map(|rm| async {
let response = api_client
.rest()
.get(
api_client
.base_url()
.join(&format!("apps/cookbook/api/recipes/{id}", id = rm.id))
.unwrap(),
)
.send()
.await
.expect(&format!(
"Cannot fetch recipe {} with id {}",
rm.name, rm.id
));
// TODO: wrap this, heavily refactor this mess
let _ = api_client.rest_semaphore.acquire().await.unwrap();
let mut retries = 0;
let response = loop {
let result = api_client
.rest(|client| async {
let response = client
.get(
api_client
.base_url()
.join(&format!("apps/cookbook/api/recipes/{id}", id = rm.id))
.unwrap(),
)
.send()
.await;
Ok(response?)
})
.await;
response.json::<recipe::Recipe>().await.map(|r| Rc::new(r))
match result {
Ok(response) => break response,
Err(_) if retries < 10 => {
retries += 1;
std::thread::sleep(retries * std::time::Duration::from_millis(500));
continue;
}
_ => bail!("Cannot fetch recipe {} with id {}", rm.name, rm.id),
}
};
response
.json::<recipe::Recipe>()
.await
.map(|r| Rc::new(r))
.map_err(|err| anyhow::anyhow!(err))
});
let recipes = try_join_all(recipes).await?;
@ -180,20 +203,19 @@ where
1,
);
// TODO: wrap this
let _ = api_client.rest_semaphore.acquire().await.unwrap();
let response = api_client
.rest()
.put(url)
.header("Content-Type", "text/calendar; charset=utf-8")
.body(cal_as_string)
.send()
.rest(|client| async {
let response = client
.put(url.clone())
.header("Content-Type", "text/calendar; charset=utf-8")
.body(cal_as_string.clone())
.send()
.await;
Ok(response?)
})
.await;
log::info!("{}", info_message);
// TODO: magic numbers are bad...
std::thread::sleep(std::time::Duration::from_millis(300));
response
});