From 45f1497df23a1ff4e2ec6b0aeaada8662086006d Mon Sep 17 00:00:00 2001 From: Joshua Coles Date: Tue, 16 Jul 2024 08:30:02 +0100 Subject: [PATCH] Stash --- Cargo.lock | 161 ++++++++++++++++++++++++++++++++++++++- Cargo.toml | 3 +- src/main.rs | 11 +-- src/toggl/mod.rs | 193 +++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 353 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c7c07eb..c033b85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -210,6 +210,41 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "dashmap" version = "5.5.3" @@ -217,12 +252,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown", + "hashbrown 0.14.5", "lock_api", "once_cell", "parking_lot_core 0.9.10", ] +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + [[package]] name = "encoding_rs" version = "0.8.34" @@ -430,13 +475,19 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -449,6 +500,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "http" version = "1.1.0" @@ -592,6 +649,12 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.5.0" @@ -602,6 +665,17 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.2.6" @@ -609,7 +683,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.5", + "serde", ] [[package]] @@ -740,6 +815,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-traits" version = "0.2.19" @@ -916,6 +997,12 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1301,6 +1388,36 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.2.6", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -1350,6 +1467,12 @@ dependencies = [ "lock_api", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -1432,6 +1555,37 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -1462,6 +1616,7 @@ dependencies = [ "serde", "serde_json", "serde_json_path_to_error", + "serde_with", "thiserror", "tokio", "url", diff --git a/Cargo.toml b/Cargo.toml index 31a872b..ce9d6f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ chrono = { version = "0.4.38", features = ["serde"] } governor = "0.6.3" reqwest = { version = "0.12.5", features = ["json"] } reqwest-ratelimit = "0.2.0" -reqwest-middleware = "0.3" +reqwest-middleware = { version = "0.3", features = ["json"] } reqwest-retry = "0.6" thiserror = "1.0.62" tokio = { version = "1.38.0", features = ["full"] } @@ -18,3 +18,4 @@ serde = { version = "1.0.204", features = ["derive"] } serde_json = "1.0.120" serde_json_path_to_error = "0.1.4" url = "2.5.2" +serde_with = "3.9.0" diff --git a/src/main.rs b/src/main.rs index b6a2277..f610677 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ use std::ops::Sub; -use chrono::{TimeDelta, Utc}; +use chrono::NaiveDate; use toggl::TogglApi; mod toggl; @@ -12,8 +12,9 @@ async fn main() { sensitive::WORKSPACE_ID, ); - dbg!(api.get_time_user_entries_between( - Utc::now().sub(TimeDelta::days(2)), - Utc::now(), - ).await); + dbg!(api.search(toggl::types::TogglReportFilters { + start_date: Some(NaiveDate::from_ymd_opt(2024, 07, 10).unwrap()), + end_date: Some(NaiveDate::from_ymd_opt(2024, 07, 16).unwrap()), + ..Default::default() + }).await); } diff --git a/src/toggl/mod.rs b/src/toggl/mod.rs index 39c254f..27f8cb1 100644 --- a/src/toggl/mod.rs +++ b/src/toggl/mod.rs @@ -95,13 +95,12 @@ impl TogglApi { base_url = BASE_URL, ); - let mut url = Url::parse(&url).unwrap(); - url.query_pairs_mut() - .append_pair("start_date", &start.to_rfc3339_opts(SecondsFormat::Secs, true)) - .append_pair("end_date", &until.to_rfc3339_opts(SecondsFormat::Secs, true)); - Self::parse(self.client.get(url) .headers(self.headers.clone()) + .query(&[ + ("start_date", start.to_rfc3339_opts(SecondsFormat::Secs, true)), + ("end_date", until.to_rfc3339_opts(SecondsFormat::Secs, true)), + ]) .send().await?).await } @@ -166,11 +165,47 @@ impl TogglApi { .headers(self.headers.clone()) .send().await?).await } + + fn paginate_filters(original_filters: &types::TogglReportFilters, last_row_id: u64) -> types::TogglReportFilters { + let mut filters = original_filters.clone(); + filters.first_row_number = Some(last_row_id + 1); + filters + } + + pub async fn search(&self, filters: types::TogglReportFilters) -> Result, TogglError> { + let url = format!( + "{base_url}/workspace/{workspace_id}/search/time_entries", + base_url = &REPORTS_BASE_URL, + workspace_id = self.workspace_id, + ); + + let mut last_row_number = Some(0); + let mut results = vec![]; + + while let Some(last_row_number_n) = last_row_number { + // 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_secs(1)).await; + } + + let data: Vec = Self::parse(self.client.post(&url) + .headers(self.headers.clone()) + .json(&Self::paginate_filters(&filters, last_row_number_n)) + .send().await?).await?; + + last_row_number = data.last().map(|e| e.row_number as u64); + + data.into_iter().for_each(|e| results.push(e)); + } + + Ok(results) } } -mod types { +pub mod types { + use std::collections::HashMap; use chrono::{DateTime, NaiveDate, Utc}; use serde::{Deserialize, Serialize}; + use serde_with::skip_serializing_none; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct TimeEntry { @@ -297,6 +332,152 @@ mod types { deleted_at: Option>, permissions: Option, } + + #[derive(Serialize, Deserialize, Debug, Clone)] + pub struct ReportEntry { + pub user_id: u32, + pub username: String, + pub project_id: Option, + pub task_id: Option, + pub billable: bool, + pub description: String, + pub tag_ids: Vec, + pub billable_amount_in_cents: Option, + pub hourly_rate_in_cents: Option, + pub currency: String, + pub time_entries: Vec, + pub row_number: u32, + } + + #[derive(Clone, Serialize, Deserialize, Debug)] + pub struct ReportEntryTimeDetails { + pub id: u64, + pub seconds: u32, + pub start: DateTime, + pub stop: DateTime, + pub at: DateTime, + } + + #[skip_serializing_none] + #[derive(Serialize, Deserialize, Clone, Default)] + pub struct TogglReportFilters { + pub billable: Option, + pub client_ids: Option>, + pub description: Option, + pub end_date: Option, + pub first_id: Option, + pub first_row_number: Option, + pub first_timestamp: Option, + pub group_ids: Option>, + pub grouped: Option, + pub hide_amounts: Option, + pub max_duration_seconds: Option, + pub min_duration_seconds: Option, + pub order_by: Option, + pub order_dir: Option, + #[serde(rename = "postedFields")] + pub posted_fields: Option>, + pub project_ids: Option>, + pub rounding: Option, + pub rounding_minutes: Option, + #[serde(rename = "startTime")] + pub start_time: Option, + pub start_date: Option, + pub tag_ids: Option>, + pub task_ids: Option>, + pub time_entry_ids: Option>, + pub user_ids: Option>, + + #[serde(flatten)] + pub rest: HashMap, + } + + use std::fmt; + + impl fmt::Debug for TogglReportFilters { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut ds = f.debug_struct("TogglQuery"); + + if let Some(billable) = &self.billable { + ds.field("billable", billable); + } + if let Some(client_ids) = &self.client_ids { + ds.field("client_ids", client_ids); + } + if let Some(description) = &self.description { + ds.field("description", description); + } + if let Some(end_date) = &self.end_date { + ds.field("end_date", end_date); + } + if let Some(first_id) = &self.first_id { + ds.field("first_id", first_id); + } + if let Some(first_row_number) = &self.first_row_number { + ds.field("first_row_number", first_row_number); + } + if let Some(first_timestamp) = &self.first_timestamp { + ds.field("first_timestamp", first_timestamp); + } + if let Some(group_ids) = &self.group_ids { + ds.field("group_ids", group_ids); + } + if let Some(grouped) = &self.grouped { + ds.field("grouped", grouped); + } + if let Some(hide_amounts) = &self.hide_amounts { + ds.field("hide_amounts", hide_amounts); + } + if let Some(max_duration_seconds) = &self.max_duration_seconds { + ds.field("max_duration_seconds", max_duration_seconds); + } + if let Some(min_duration_seconds) = &self.min_duration_seconds { + ds.field("min_duration_seconds", min_duration_seconds); + } + if let Some(order_by) = &self.order_by { + ds.field("order_by", order_by); + } + if let Some(order_dir) = &self.order_dir { + ds.field("order_dir", order_dir); + } + if let Some(posted_fields) = &self.posted_fields { + ds.field("postedFields", posted_fields); + } + if let Some(project_ids) = &self.project_ids { + ds.field("project_ids", project_ids); + } + if let Some(rounding) = &self.rounding { + ds.field("rounding", rounding); + } + if let Some(rounding_minutes) = &self.rounding_minutes { + ds.field("rounding_minutes", rounding_minutes); + } + if let Some(start_time) = &self.start_time { + ds.field("startTime", start_time); + } + if let Some(start_date) = &self.start_date { + ds.field("start_date", start_date); + } + if let Some(tag_ids) = &self.tag_ids { + ds.field("tag_ids", tag_ids); + } + if let Some(task_ids) = &self.task_ids { + ds.field("task_ids", task_ids); + } + if let Some(time_entry_ids) = &self.time_entry_ids { + ds.field("time_entry_ids", time_entry_ids); + } + if let Some(user_ids) = &self.user_ids { + ds.field("user_ids", user_ids); + } + + if !self.rest.is_empty() { + ds.field("rest", &self.rest); + } + + ds.finish() + } + } } #[derive(Debug, thiserror::Error)]