From 9cfe56b3ce7c77c5755df2d868eaaad58bd0c8d8 Mon Sep 17 00:00:00 2001 From: Joshua Coles Date: Sat, 2 Mar 2024 10:00:15 +0000 Subject: [PATCH] Refactor ReportEntry to ReportRow and enhance request builder Changed the name of 'ReportEntry' to 'ReportRow' for clarity. Also simplified the request builder in the TogglApiClient by implementing a 'make_request' function which helps in handling rate limiting. The function is then used in various methods of the TogglApiClient. --- src/db.rs | 4 +- src/routes.rs | 10 ++--- src/toggl_api/api_client.rs | 85 ++++++++++++++++++------------------- src/toggl_api/types.rs | 22 +++++----- 4 files changed, 60 insertions(+), 61 deletions(-) diff --git a/src/db.rs b/src/db.rs index d8ab685..b34c03a 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,9 +1,9 @@ use crate::entity::{client, project, time_entry}; -use crate::toggl_api::types::{Project, ProjectClient, ReportEntry}; +use crate::toggl_api::types::{Project, ProjectClient, ReportRow}; use sea_orm::sea_query::OnConflict; use sea_orm::{NotSet, Set}; -impl ReportEntry { +impl ReportRow { pub(crate) fn as_models(&self) -> Vec { self.time_entries .iter() diff --git a/src/routes.rs b/src/routes.rs index 821a8d2..7e7951d 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -12,7 +12,7 @@ use chrono::{NaiveDate}; use serde::Deserialize; use crate::entity::time_entry::{Entity as TimeEntry}; use crate::toggl_api::TogglApiClient; -use crate::toggl_api::types::{Current, Project, ProjectClient, ReportEntry, TogglQuery}; +use crate::toggl_api::types::{Current, Project, ProjectClient, ReportRow, TogglQuery}; use crate::{entity, utils}; use crate::entity::time_entry; @@ -21,7 +21,7 @@ pub async fn report( Extension(toggl_client): Extension, Extension(db): Extension, Json(query): Json, -) -> utils::Result>> { +) -> utils::Result>> { let report = toggl_client.full_report(&query).await?; debug!("Returned results: {:?}", report); @@ -35,7 +35,7 @@ pub async fn report( #[instrument(skip_all)] pub async fn cache_report( db: &DatabaseConnection, - models: &Vec, + models: &Vec, exclusive_on: Option, ) -> utils::Result<()> { let models = models.iter().flat_map(|entry| entry.as_models()); @@ -52,7 +52,7 @@ pub async fn cache_report( } TimeEntry::insert_many(models) - .on_conflict(ReportEntry::grafting_conflict_statement()) + .on_conflict(ReportRow::grafting_conflict_statement()) .exec(db) .await?; @@ -74,7 +74,7 @@ pub async fn cache_report( pub async fn current( Extension(toggl_client): Extension, ) -> utils::Result>> { - Ok(toggl_client.get_current().await.map(Json)?) + Ok(toggl_client.fetch_current_time_entry().await.map(Json)?) } #[instrument(skip(toggl_client))] diff --git a/src/toggl_api/api_client.rs b/src/toggl_api/api_client.rs index 56b0c4a..1784d6b 100644 --- a/src/toggl_api/api_client.rs +++ b/src/toggl_api/api_client.rs @@ -1,14 +1,16 @@ -use reqwest::Client; +use reqwest::{Client, RequestBuilder, Response}; use serde_json::Value; use std::collections::HashMap; use std::time::Duration; +use anyhow::anyhow; +use axum::http::StatusCode; use base64::Engine; use base64::engine::general_purpose::STANDARD; use hyper::HeaderMap; use reqwest::header::HeaderValue; use tracing::instrument; use tracing::log::debug; -use crate::toggl_api::types::{Current, Project, ProjectClient, ReportEntry, TogglQuery}; +use crate::toggl_api::types::{Current, Project, ProjectClient, ReportRow, TogglQuery}; #[derive(Debug, Clone)] pub struct TogglApiClient { @@ -16,11 +18,23 @@ pub struct TogglApiClient { workspace_id: String, base_url: String, reports_base_url: String, - - headers: HeaderMap, } impl TogglApiClient { + async fn make_request(&self, request_builder: RequestBuilder) -> crate::Result { + loop { + let builder = request_builder.try_clone().ok_or(anyhow!("Failed to clone request builder"))?; + let response = self.client.execute(builder.build()?).await?; + + // If we are rate limited, wait a bit and try again + if response.status() == StatusCode::TOO_MANY_REQUESTS { + tokio::time::sleep(Duration::from_secs(5)).await; + } else { + return Ok(response); + } + } + } + pub async fn check_health(&self) -> bool { true } @@ -38,11 +52,10 @@ impl TogglApiClient { workspace_id: workspace_id.to_string(), base_url: "https://api.track.toggl.com/api/v9".to_string(), reports_base_url: "https://api.track.toggl.com/reports/api/v3".to_string(), - headers: Self::default_headers(toggl_auth), } } - fn default_headers(toggl_auth: &str) -> reqwest::header::HeaderMap { + fn default_headers(toggl_auth: &str) -> HeaderMap { let mut headers = HeaderMap::new(); let mut value = HeaderValue::from_str(&format!("Basic {}", toggl_auth)).unwrap(); value.set_sensitive(true); @@ -53,65 +66,57 @@ impl TogglApiClient { headers } - pub async fn fetch_projects(&self) -> Result, reqwest::Error> { + pub async fn fetch_projects(&self) -> crate::Result> { let url = format!( "{base_url}/workspaces/{}/projects", self.workspace_id, base_url = self.base_url, ); - let res = self + let projects = self.make_request(self .client - .get(&url) - .headers(self.headers.clone()) - .send() + .get(&url)) .await? .json::>() - .await - .unwrap(); + .await?; - Ok(res) + Ok(projects) } - pub async fn fetch_clients(&self) -> Result, reqwest::Error> { + pub async fn fetch_clients(&self) -> crate::Result> { let url = format!( "{base_url}/workspaces/{}/clients", self.workspace_id, base_url = self.base_url, ); - let res = self + let clients = self.make_request(self .client - .get(&url) - .headers(self.headers.clone()) - .send() + .get(&url)) .await? .json::>() - .await - .unwrap(); + .await?; - Ok(res) + Ok(clients) } - pub async fn get_current(&self) -> Result, reqwest::Error> { + pub async fn fetch_current_time_entry(&self) -> crate::Result> { let url = format!( "{base_url}/me/time_entries/current", base_url = self.base_url ); - let res = self + let res = self.make_request(self .client - .get(url) - .send() + .get(url)) .await? .json::>() - .await - .unwrap(); + .await?; Ok(res) } - fn create_filters(original_filters: &TogglQuery, last_row_id: u64) -> TogglQuery { + fn paginate_filters(original_filters: &TogglQuery, last_row_id: u64) -> TogglQuery { let mut filters: TogglQuery = original_filters.clone(); filters.first_row_number = Some(last_row_id + 1); filters @@ -121,7 +126,7 @@ impl TogglApiClient { pub async fn full_report( &self, filters: &TogglQuery, - ) -> anyhow::Result> { + ) -> anyhow::Result> { let url = format!( "{base_url}/workspace/{workspace_id}/search/time_entries", base_url = self.reports_base_url, @@ -135,20 +140,19 @@ impl TogglApiClient { debug!("Fetching page starting with {}", last_row_number_n); // If we are not on the first page, wait a bit to avoid rate limiting if last_row_number_n != 0 { - tokio::time::sleep(Duration::from_millis(1000)).await; + tokio::time::sleep(Duration::from_secs(1)).await; } // TODO: Implement rate limiting let response = self .client .post(&url) - .headers(self.headers.clone()) - .json(&Self::create_filters(&filters, last_row_number_n)) + .json(&Self::paginate_filters(&filters, last_row_number_n)) .send() .await?; let data = response - .json::>() + .json::>() .await?; last_row_number = data.last().map(|e| e.row_number as u64); @@ -159,7 +163,7 @@ impl TogglApiClient { Ok(results) } - pub async fn start_time_entry(&self, mut body: HashMap) -> anyhow::Result<()> { + pub async fn start_time_entry(&self, mut body: HashMap) -> crate::Result<()> { let url = format!( "{base_url}/workspaces/{workspace_id}/time_entries", base_url = self.base_url, @@ -168,17 +172,12 @@ impl TogglApiClient { body.insert( "workspace_id".to_string(), - self.workspace_id.parse::().unwrap().into(), + self.workspace_id.parse::()?.into(), ); - dbg!(self.client + self.make_request(self.client .post(url) - .headers(self.headers.clone()) - .json(&body) - .send() - .await? - .text() - .await?); + .json(&body)).await?; Ok(()) } diff --git a/src/toggl_api/types.rs b/src/toggl_api/types.rs index 6b8898d..d757952 100644 --- a/src/toggl_api/types.rs +++ b/src/toggl_api/types.rs @@ -6,16 +6,7 @@ use std::option::Option; use chrono::{DateTime, Utc}; #[derive(Clone, Serialize, Deserialize, Debug)] -pub struct TimeEntry { - pub id: u64, - pub seconds: u32, - pub start: String, - pub stop: String, - pub at: String, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct ReportEntry { +pub struct ReportRow { pub user_id: u32, pub username: String, pub project_id: Option, @@ -26,10 +17,19 @@ pub struct ReportEntry { pub billable_amount_in_cents: Option, pub hourly_rate_in_cents: Option, pub currency: String, - pub time_entries: Vec, + pub time_entries: Vec, pub row_number: u32, } +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct ReportRowInnerTimeEntry { + pub id: u64, + pub seconds: u32, + pub start: String, + pub stop: String, + pub at: String, +} + #[derive(Debug, Serialize, Deserialize)] pub struct Current { pub id: u64,