Stash
This commit is contained in:
parent
c1b8636407
commit
45f1497df2
161
Cargo.lock
generated
161
Cargo.lock
generated
@ -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",
|
||||
|
||||
@ -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"
|
||||
|
||||
11
src/main.rs
11
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);
|
||||
}
|
||||
|
||||
193
src/toggl/mod.rs
193
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
|
||||
}
|
||||
|
||||
mod types {
|
||||
pub async fn search(&self, filters: types::TogglReportFilters) -> Result<Vec<types::ReportEntry>, 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<types::ReportEntry> = 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) }
|
||||
}
|
||||
|
||||
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<DateTime<Utc>>,
|
||||
permissions: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ReportEntry {
|
||||
pub user_id: u32,
|
||||
pub username: String,
|
||||
pub project_id: Option<u64>,
|
||||
pub task_id: Option<u64>,
|
||||
pub billable: bool,
|
||||
pub description: String,
|
||||
pub tag_ids: Vec<u64>,
|
||||
pub billable_amount_in_cents: Option<u64>,
|
||||
pub hourly_rate_in_cents: Option<u64>,
|
||||
pub currency: String,
|
||||
pub time_entries: Vec<ReportEntryTimeDetails>,
|
||||
pub row_number: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct ReportEntryTimeDetails {
|
||||
pub id: u64,
|
||||
pub seconds: u32,
|
||||
pub start: DateTime<Utc>,
|
||||
pub stop: DateTime<Utc>,
|
||||
pub at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||
pub struct TogglReportFilters {
|
||||
pub billable: Option<bool>,
|
||||
pub client_ids: Option<Vec<u64>>,
|
||||
pub description: Option<String>,
|
||||
pub end_date: Option<String>,
|
||||
pub first_id: Option<u64>,
|
||||
pub first_row_number: Option<u64>,
|
||||
pub first_timestamp: Option<u64>,
|
||||
pub group_ids: Option<Vec<u64>>,
|
||||
pub grouped: Option<bool>,
|
||||
pub hide_amounts: Option<bool>,
|
||||
pub max_duration_seconds: Option<u64>,
|
||||
pub min_duration_seconds: Option<u64>,
|
||||
pub order_by: Option<String>,
|
||||
pub order_dir: Option<String>,
|
||||
#[serde(rename = "postedFields")]
|
||||
pub posted_fields: Option<Vec<String>>,
|
||||
pub project_ids: Option<Vec<u64>>,
|
||||
pub rounding: Option<u64>,
|
||||
pub rounding_minutes: Option<u64>,
|
||||
#[serde(rename = "startTime")]
|
||||
pub start_time: Option<String>,
|
||||
pub start_date: Option<String>,
|
||||
pub tag_ids: Option<Vec<u64>>,
|
||||
pub task_ids: Option<Vec<u64>>,
|
||||
pub time_entry_ids: Option<Vec<u64>>,
|
||||
pub user_ids: Option<Vec<u64>>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub rest: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
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)]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user