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.
This commit is contained in:
parent
991e7fd746
commit
9cfe56b3ce
@ -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<time_entry::ActiveModel> {
|
||||
self.time_entries
|
||||
.iter()
|
||||
|
||||
@ -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<TogglApiClient>,
|
||||
Extension(db): Extension<DatabaseConnection>,
|
||||
Json(query): Json<TogglQuery>,
|
||||
) -> utils::Result<Json<Vec<ReportEntry>>> {
|
||||
) -> utils::Result<Json<Vec<ReportRow>>> {
|
||||
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<ReportEntry>,
|
||||
models: &Vec<ReportRow>,
|
||||
exclusive_on: Option<Condition>,
|
||||
) -> 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<TogglApiClient>,
|
||||
) -> utils::Result<Json<Option<Current>>> {
|
||||
Ok(toggl_client.get_current().await.map(Json)?)
|
||||
Ok(toggl_client.fetch_current_time_entry().await.map(Json)?)
|
||||
}
|
||||
|
||||
#[instrument(skip(toggl_client))]
|
||||
|
||||
@ -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<Response> {
|
||||
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<Vec<Project>, reqwest::Error> {
|
||||
pub async fn fetch_projects(&self) -> crate::Result<Vec<Project>> {
|
||||
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::<Vec<Project>>()
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
|
||||
Ok(res)
|
||||
Ok(projects)
|
||||
}
|
||||
|
||||
pub async fn fetch_clients(&self) -> Result<Vec<ProjectClient>, reqwest::Error> {
|
||||
pub async fn fetch_clients(&self) -> crate::Result<Vec<ProjectClient>> {
|
||||
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::<Vec<ProjectClient>>()
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
|
||||
Ok(res)
|
||||
Ok(clients)
|
||||
}
|
||||
|
||||
pub async fn get_current(&self) -> Result<Option<Current>, reqwest::Error> {
|
||||
pub async fn fetch_current_time_entry(&self) -> crate::Result<Option<Current>> {
|
||||
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::<Option<Current>>()
|
||||
.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<Vec<ReportEntry>> {
|
||||
) -> anyhow::Result<Vec<ReportRow>> {
|
||||
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::<Vec<ReportEntry>>()
|
||||
.json::<Vec<ReportRow>>()
|
||||
.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<String, Value>) -> anyhow::Result<()> {
|
||||
pub async fn start_time_entry(&self, mut body: HashMap<String, Value>) -> 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::<i32>().unwrap().into(),
|
||||
self.workspace_id.parse::<i32>()?.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(())
|
||||
}
|
||||
|
||||
@ -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<u64>,
|
||||
@ -26,10 +17,19 @@ pub struct ReportEntry {
|
||||
pub billable_amount_in_cents: Option<u64>,
|
||||
pub hourly_rate_in_cents: Option<u64>,
|
||||
pub currency: String,
|
||||
pub time_entries: Vec<TimeEntry>,
|
||||
pub time_entries: Vec<ReportRowInnerTimeEntry>,
|
||||
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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user