Switch to async reqwest and colour function all the way up

This commit is contained in:
2026-05-03 10:36:43 +02:00
parent deda785c2a
commit 128768755f
4 changed files with 54 additions and 26 deletions
Generated
+13
View File
@@ -1446,9 +1446,21 @@ dependencies = [
"mio", "mio",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2",
"tokio-macros",
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "tokio-macros"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "tokio-rustls" name = "tokio-rustls"
version = "0.26.4" version = "0.26.4"
@@ -1954,6 +1966,7 @@ dependencies = [
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"tokio",
] ]
[[package]] [[package]]
+1
View File
@@ -11,3 +11,4 @@ directories = "6.0.0"
reqwest = { version = "0.13.3", features = ["blocking"] } reqwest = { version = "0.13.3", features = ["blocking"] }
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149" serde_json = "1.0.149"
tokio = { version = "1.52.1", features = ["macros", "rt-multi-thread"] }
+7 -6
View File
@@ -111,7 +111,7 @@ fn read_transactions_from(
vec![Box::new(stdin()) as Box<dyn Read>] vec![Box::new(stdin()) as Box<dyn Read>]
} else { } else {
inputs inputs
.into_iter() .iter()
.map(|p| -> Result<Box<dyn Read>, _> { Ok(Box::new(File::open(p)?)) }) .map(|p| -> Result<Box<dyn Read>, _> { Ok(Box::new(File::open(p)?)) })
.collect::<Result<Vec<_>, std::io::Error>>()? .collect::<Result<Vec<_>, std::io::Error>>()?
} }
@@ -123,12 +123,13 @@ fn read_transactions_from(
.collect()) .collect())
} }
fn main() -> Result<(), Box<dyn Error>> { #[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let cli = Cli::parse(); let cli = Cli::parse();
match cli.command { match cli.command {
Command::Plans { token } => { Command::Plans { token } => {
let plans = Ynab::new(&token).list_plans()?; let plans = Ynab::new(&token).list_plans().await?;
println!("Available plans:"); println!("Available plans:");
for plan in plans { for plan in plans {
println!(" - {}", plan); println!(" - {}", plan);
@@ -141,7 +142,7 @@ fn main() -> Result<(), Box<dyn Error>> {
plan_id, plan_id,
} => { } => {
let plan = Lookup::from_options(plan, plan_id)?; let plan = Lookup::from_options(plan, plan_id)?;
let accounts = Ynab::list_accounts(&Ynab::new(&token), plan)?; let accounts = Ynab::list_accounts(&Ynab::new(&token), plan).await?;
println!("Available accounts in plan:"); println!("Available accounts in plan:");
for account in accounts { for account in accounts {
println!(" - {}", account); println!(" - {}", account);
@@ -169,7 +170,7 @@ fn main() -> Result<(), Box<dyn Error>> {
Some(Lookup::from_options(name, id)?) Some(Lookup::from_options(name, id)?)
} }
}; };
let transactions = Ynab::new(&token).list_transactions(plan, account)?; let transactions = Ynab::new(&token).list_transactions(plan, account).await?;
println!("Transactions{} in the last 30 days:", accountstr); println!("Transactions{} in the last 30 days:", accountstr);
for t in transactions { for t in transactions {
println!("{},{},{}", t.date, t.payee, t.amount); println!("{},{},{}", t.date, t.payee, t.amount);
@@ -191,7 +192,7 @@ fn main() -> Result<(), Box<dyn Error>> {
let transactions = read_transactions_from(&inputs, &format)?; let transactions = read_transactions_from(&inputs, &format)?;
Ynab::new(&token).upload(&transactions, plan, account) Ynab::new(&token).upload(&transactions, plan, account).await
} }
Command::Convert { format, inputs } => { Command::Convert { format, inputs } => {
let transactions = read_transactions_from(&inputs, &format)?; let transactions = read_transactions_from(&inputs, &format)?;
+33 -20
View File
@@ -5,24 +5,27 @@ use crate::Transaction;
pub struct Ynab { pub struct Ynab {
token: String, token: String,
webclient: reqwest::blocking::Client, webclient: reqwest::Client,
} }
impl Ynab { impl Ynab {
pub fn new(token: &str) -> Ynab { pub fn new(token: &str) -> Ynab {
Ynab { Ynab {
token: token.to_owned(), token: token.to_owned(),
webclient: reqwest::blocking::Client::new(), webclient: reqwest::Client::new(),
} }
} }
fn resolve_plan(&self, plan: Lookup) -> Result<String, String> { async fn resolve_plan(&self, plan: Lookup) -> Result<String, String> {
match plan { match plan {
Lookup::Id(id) => Ok(id), Lookup::Id(id) => Ok(id),
Lookup::Name(name) => { Lookup::Name(name) => {
let url = "plans"; let url = "plans";
let res: types::YnabResponse<types::YnabPlanList> = serde_json::from_str( let res: types::YnabResponse<types::YnabPlanList> = serde_json::from_str(
&self.get(url).map_err(|e| format!("request error: {}", e))?, &self
.get(url)
.await
.map_err(|e| format!("request error: {}", e))?,
) )
.map_err(|e| format!("parse error: {}", e))?; .map_err(|e| format!("parse error: {}", e))?;
let plans = res.data.plans; let plans = res.data.plans;
@@ -35,7 +38,7 @@ impl Ynab {
} }
} }
fn resolve_account(&self, plan_id: &str, account: Lookup) -> Result<String, String> { async fn resolve_account(&self, plan_id: &str, account: Lookup) -> Result<String, String> {
match account { match account {
Lookup::Id(id) => Ok(id), Lookup::Id(id) => Ok(id),
Lookup::Name(name) => { Lookup::Name(name) => {
@@ -43,6 +46,7 @@ impl Ynab {
let res: types::YnabResponse<types::YnabAccountList> = serde_json::from_str( let res: types::YnabResponse<types::YnabAccountList> = serde_json::from_str(
&self &self
.get(&url) .get(&url)
.await
.map_err(|e| format!("request error: {}", e))?, .map_err(|e| format!("request error: {}", e))?,
) )
.map_err(|e| format!("parse error: {}", e))?; .map_err(|e| format!("parse error: {}", e))?;
@@ -56,19 +60,20 @@ impl Ynab {
} }
} }
pub(crate) fn get(&self, request: &str) -> Result<String, reqwest::Error> { pub(crate) async fn get(&self, request: &str) -> Result<String, reqwest::Error> {
const URL: &str = "https://api.ynab.com/v1/"; const URL: &str = "https://api.ynab.com/v1/";
let request = format!("{}{}", URL, request); let request = format!("{}{}", URL, request);
let res = self let res = self
.webclient .webclient
.get(request) .get(request)
.bearer_auth(&self.token) .bearer_auth(&self.token)
.send()?; .send()
let text = res.text()?; .await?;
let text = res.text().await?;
Ok(text) Ok(text)
} }
fn post(&self, request: &str, body: &str) -> Result<(), String> { async fn post(&self, request: &str, body: &str) -> Result<(), String> {
const URL: &str = "https://api.ynab.com/v1/"; const URL: &str = "https://api.ynab.com/v1/";
let request = format!("{}{}", URL, request); let request = format!("{}{}", URL, request);
let res = self let res = self
@@ -78,12 +83,14 @@ impl Ynab {
.bearer_auth(&self.token) .bearer_auth(&self.token)
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.send() .send()
.await
.map_err(|e| format!("post error: {}", e))?; .map_err(|e| format!("post error: {}", e))?;
if res.status().is_success() { if res.status().is_success() {
println!( println!(
"successful api response {}: {}", "successful api response {}: {}",
res.status(), res.status(),
res.text() res.text()
.await
.map_err(|e| format!("error retrieving api error: {}", e))? .map_err(|e| format!("error retrieving api error: {}", e))?
); );
Ok(()) Ok(())
@@ -92,48 +99,54 @@ impl Ynab {
"api error: {}: {}", "api error: {}: {}",
res.status(), res.status(),
res.text() res.text()
.await
.map_err(|e| format!("error retrieving api error: {}", e))? .map_err(|e| format!("error retrieving api error: {}", e))?
)) ))
} }
} }
pub fn list_plans(&self) -> Result<Vec<String>, String> { pub async fn list_plans(&self) -> Result<Vec<String>, String> {
let url = String::from("plans"); let url = String::from("plans");
let res: types::YnabResponse<types::YnabPlanList> = serde_json::from_str( let res: types::YnabResponse<types::YnabPlanList> = serde_json::from_str(
&self &self
.get(&url) .get(&url)
.await
.map_err(|e| format!("request error: {}", e))?, .map_err(|e| format!("request error: {}", e))?,
) )
.map_err(|e| format!("parse error: {}", e))?; .map_err(|e| format!("parse error: {}", e))?;
Ok(res.data.plans.into_iter().map(|p| p.name).collect()) Ok(res.data.plans.into_iter().map(|p| p.name).collect())
} }
pub fn list_accounts(&self, plan: Lookup) -> Result<Vec<String>, String> { pub async fn list_accounts(&self, plan: Lookup) -> Result<Vec<String>, String> {
let plan_id = self.resolve_plan(plan)?; let plan_id = self.resolve_plan(plan).await?;
let url = format!("plans/{}/accounts", plan_id); let url = format!("plans/{}/accounts", plan_id);
let res: types::YnabResponse<types::YnabAccountList> = serde_json::from_str( let res: types::YnabResponse<types::YnabAccountList> = serde_json::from_str(
&self &self
.get(&url) .get(&url)
.await
.map_err(|e| format!("request error: {}", e))?, .map_err(|e| format!("request error: {}", e))?,
) )
.map_err(|e| format!("parse error: {}", e))?; .map_err(|e| format!("parse error: {}", e))?;
Ok(res.data.accounts.into_iter().map(|p| p.name).collect()) Ok(res.data.accounts.into_iter().map(|p| p.name).collect())
} }
pub fn list_transactions( pub async fn list_transactions(
&self, &self,
plan: Lookup, plan: Lookup,
account: Option<Lookup>, account: Option<Lookup>,
) -> Result<Vec<Transaction>, String> { ) -> Result<Vec<Transaction>, String> {
let plan_id = self.resolve_plan(plan)?; let plan_id = self.resolve_plan(plan).await?;
let account_id = match account { let account_id = match account {
Some(account) => Some(self.resolve_account(&plan_id, account)?), Some(account) => Some(self.resolve_account(&plan_id, account).await?),
None => None, None => None,
}; };
let since_date = chrono::Local::now().date_naive() - chrono::Duration::days(30); let since_date = chrono::Local::now().date_naive() - chrono::Duration::days(30);
let since_date = since_date.format("%Y-%m-%d"); let since_date = since_date.format("%Y-%m-%d");
let url = format!("plans/{}/transactions?since_date={}", plan_id, since_date); let url = format!("plans/{}/transactions?since_date={}", plan_id, since_date);
let raw = self.get(&url).map_err(|e| format!("request error {}", e))?; let raw = self
.get(&url)
.await
.map_err(|e| format!("request error {}", e))?;
let res: types::YnabResponse<types::YnabTransactionList> = let res: types::YnabResponse<types::YnabTransactionList> =
serde_json::from_str(&raw).map_err(|e| format!("parse error: {}", e))?; serde_json::from_str(&raw).map_err(|e| format!("parse error: {}", e))?;
Ok(transform::from_ynab_transactions( Ok(transform::from_ynab_transactions(
@@ -142,20 +155,20 @@ impl Ynab {
)) ))
} }
pub fn upload( pub async fn upload(
&self, &self,
transactions: &[Transaction], transactions: &[Transaction],
plan: Lookup, plan: Lookup,
account: Lookup, account: Lookup,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let plan_id = self.resolve_plan(plan)?; let plan_id = self.resolve_plan(plan).await?;
let account_id = self.resolve_account(&plan_id, account)?; let account_id = self.resolve_account(&plan_id, account).await?;
let request = format!("plans/{}/transactions", plan_id); let request = format!("plans/{}/transactions", plan_id);
let body = serde_json::to_string(&types::YnabTransactionList { let body = serde_json::to_string(&types::YnabTransactionList {
transactions: transform::to_ynab_transactions(transactions, &account_id), transactions: transform::to_ynab_transactions(transactions, &account_id),
}) })
.map_err(|e| format!("transaction format error: {}", e))?; .map_err(|e| format!("transaction format error: {}", e))?;
self.post(&request, &body)?; self.post(&request, &body).await?;
Ok(()) Ok(())
} }
} }