some stuff IDK how to do ts
This commit is contained in:
16 files changed
+1107
-220
No files matched your search
+47
-1
@@ -1 +1,47 @@
|
||||
//! this deals with authentication. validate, refresh, and quota check.
|
||||
//! this deals with authentication. validate, refresh, and quota check.
|
||||
|
||||
use crate::store::Account;
|
||||
use crate::store::AccountStore;
|
||||
|
||||
pub async fn validate_account<S: AccountStore>(store: &S, id: &str) -> anyhow::Result<()> {
|
||||
let account = store.load(id)?;
|
||||
let (auth_manager, _) = codexport::auth_mgr_from_auth_json(account.auth_json).await?;
|
||||
auth_manager
|
||||
.auth()
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("not logged in!"))?;
|
||||
Ok(())
|
||||
}
|
||||
// auth manager hides path
|
||||
// path gets auth json
|
||||
// auth json plus path gets auth manager
|
||||
// so you lowk only need path and auth json
|
||||
pub async fn refresh_account<S: AccountStore>(store: &mut S, id: &str) -> anyhow::Result<()> {
|
||||
let account = store.load(id)?;
|
||||
let (auth_manager, path) = codexport::auth_mgr_from_auth_json(account.auth_json).await?;
|
||||
codexport::renew(&auth_manager).await?;
|
||||
let new_auth_json = codexport::ephemeral_auth_json(&path)?;
|
||||
let new_account = Account {
|
||||
id: account.id,
|
||||
nickname: account.nickname,
|
||||
auth_json: new_auth_json,
|
||||
};
|
||||
store.upsert(new_account)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_account<S: AccountStore>(store: &mut S, id: &str) -> anyhow::Result<()> {
|
||||
let account = store.load(id)?;
|
||||
let (auth_manager, _path) = codexport::auth_mgr_from_auth_json(account.auth_json).await?;
|
||||
auth_manager.logout_with_revoke().await?;
|
||||
store.delete(id)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn check_quota<S: AccountStore>(store: &S, id: &str) -> anyhow::Result<codexport::RateLimit> {
|
||||
let account = store.load(id)?;
|
||||
let (auth_manager, _path) = codexport::auth_mgr_from_auth_json(account.auth_json).await?;
|
||||
let client = codexport::backend_client_from_auth_mgr(&auth_manager).await?;
|
||||
let quota = codexport::quota(&client).await?;
|
||||
Ok(quota)
|
||||
}
|
||||
+38
-57
@@ -1,9 +1,5 @@
|
||||
#[derive(clap::Parser, Debug)]
|
||||
#[clap(
|
||||
name = "csa",
|
||||
version,
|
||||
about = "A CLI tool to manage multiple Codex accounts"
|
||||
)]
|
||||
#[clap(version, about = "A CLI tool to manage multiple Codex accounts")]
|
||||
pub struct Cli {
|
||||
#[clap(subcommand)]
|
||||
command: Commands,
|
||||
@@ -14,7 +10,28 @@ pub enum Commands {
|
||||
Show(ShowCommand),
|
||||
Add(AddCommand),
|
||||
Swap(SwapCommand),
|
||||
Account(AccountCommand),
|
||||
|
||||
Refresh {
|
||||
/// specify to refresh the account. Else refresh all accounts.
|
||||
// this and AccountCommand.account at the same time is an error.
|
||||
// chat how do i enforce above rule?
|
||||
acc: Option<String>,
|
||||
/// specify to only refresh the account if it's expired. Else refresh anyway.
|
||||
#[clap(long)]
|
||||
expired: bool,
|
||||
},
|
||||
// also logout
|
||||
Remove {
|
||||
/// specify to remove the account
|
||||
// do NOT support remove all accounts.
|
||||
acc: String,
|
||||
},
|
||||
Rename {
|
||||
/// specify to rename the account
|
||||
acc: String,
|
||||
/// the new name for the account
|
||||
new_name: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, clap::Parser)]
|
||||
@@ -34,7 +51,7 @@ pub struct AddCommand {
|
||||
pub name: Option<String>,
|
||||
}
|
||||
/// `csa swap acc1` would swap to acc1 if it exists. Else error out. `csa swap` would swap to the most capable account, which is determined by the internal logic of csa, and is usually the best valid account with the best availability.
|
||||
///
|
||||
///
|
||||
/// to use something right after add/swap, use your favourite shell feature, the semicolon `;`.
|
||||
// that looks like an emoji yo (`;`) wth
|
||||
#[derive(Clone, Debug, clap::Parser)]
|
||||
@@ -43,41 +60,6 @@ pub struct SwapCommand {
|
||||
pub acc: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, clap::Parser)]
|
||||
pub struct AccountCommand {
|
||||
#[clap(subcommand)]
|
||||
subcommand: AccountSubCommand,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, clap::Subcommand)]
|
||||
pub enum AccountSubCommand {
|
||||
Refresh {
|
||||
/// specify to refresh the account. Else refresh all accounts.
|
||||
// this and AccountCommand.account at the same time is an error.
|
||||
// chat how do i enforce above rule?
|
||||
acc: Option<String>,
|
||||
/// specify to only refresh the account if it's expired. Else refresh anyway.
|
||||
expired: bool,
|
||||
},
|
||||
// also logout
|
||||
Remove {
|
||||
/// specify to remove the account
|
||||
// do NOT support remove all accounts.
|
||||
acc: String,
|
||||
},
|
||||
Rename {
|
||||
/// specify to rename the account
|
||||
acc: String,
|
||||
/// the new name for the account
|
||||
new_name: String,
|
||||
},
|
||||
// same thing as Show above
|
||||
Show {
|
||||
/// specify to show the account. Else show all accounts.
|
||||
acc: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -114,8 +96,8 @@ mod tests {
|
||||
}
|
||||
#[test]
|
||||
fn add_use_name() {
|
||||
let cli = Cli::try_parse_from(["test", "add", "--use", "--name", "myacc"])
|
||||
.expect("csa add --use --name myacc");
|
||||
let cli =
|
||||
Cli::try_parse_from(["test", "add", "--use", "myacc"]).expect("csa add --use myacc");
|
||||
println!("{cli:#?}");
|
||||
assert!(
|
||||
matches!(cli.command, Commands::Add(AddCommand { use_rn: true, name: Some(name) }) if name == "myacc")
|
||||
@@ -123,8 +105,7 @@ mod tests {
|
||||
}
|
||||
#[test]
|
||||
fn add_name() {
|
||||
let cli =
|
||||
Cli::try_parse_from(["test", "add", "--name", "myacc"]).expect("csa add --name myacc");
|
||||
let cli = Cli::try_parse_from(["test", "add", "myacc"]).expect("csa add myacc");
|
||||
println!("{cli:#?}");
|
||||
assert!(
|
||||
matches!(cli.command, Commands::Add(AddCommand { use_rn: false, name: Some(name) }) if name == "myacc")
|
||||
@@ -153,20 +134,20 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn refresh_acc() {
|
||||
let cli = Cli::try_parse_from(["test", "refresh", "--acc", "acc1"])
|
||||
.expect("csa refresh --acc acc1");
|
||||
let cli = Cli::try_parse_from(["test", "refresh", "acc1"])
|
||||
.expect("csa refresh acc1");
|
||||
println!("{cli:#?}");
|
||||
assert!(
|
||||
matches!(cli.command, Commands::Account(AccountCommand { subcommand: AccountSubCommand::Refresh { acc: Some(acc), expired }, .. }) if acc == "acc1" && !expired)
|
||||
matches!(cli.command, Commands::Refresh { acc: Some(acc), expired } if acc == "acc1" && !expired)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn refresh_acc_expired() {
|
||||
let cli = Cli::try_parse_from(["test", "refresh", "--acc", "acc1", "--expired"])
|
||||
.expect("csa refresh --acc acc1 --expired");
|
||||
let cli = Cli::try_parse_from(["test", "refresh", "acc1", "--expired"])
|
||||
.expect("csa refresh acc1 --expired");
|
||||
println!("{cli:#?}");
|
||||
assert!(
|
||||
matches!(cli.command, Commands::Account(AccountCommand { subcommand: AccountSubCommand::Refresh { acc: Some(acc), expired }, .. }) if acc == "acc1" && expired)
|
||||
matches!(cli.command, Commands::Refresh { acc: Some(acc), expired }if acc == "acc1" && expired)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
@@ -175,17 +156,17 @@ mod tests {
|
||||
Cli::try_parse_from(["test", "refresh", "--expired"]).expect("csa refresh --expired");
|
||||
println!("{cli:#?}");
|
||||
assert!(
|
||||
matches!(cli.command, Commands::Account(AccountCommand { subcommand: AccountSubCommand::Refresh { acc: None, expired }, .. }) if expired)
|
||||
matches!(cli.command, Commands::Refresh { acc: None, expired } if expired)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove() {
|
||||
let cli = Cli::try_parse_from(["test", "remove", "--acc", "acc1"])
|
||||
.expect("csa remove --acc acc1");
|
||||
let cli = Cli::try_parse_from(["test", "remove", "acc1"])
|
||||
.expect("csa remove acc1");
|
||||
println!("{cli:#?}");
|
||||
assert!(
|
||||
matches!(cli.command, Commands::Account(AccountCommand { subcommand: AccountSubCommand::Remove { acc }, .. }) if acc == "acc1")
|
||||
matches!(cli.command, Commands::Remove { acc } if acc == "acc1")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -202,7 +183,7 @@ mod tests {
|
||||
.expect("csa rename acc1 acc_new");
|
||||
println!("{cli:#?}");
|
||||
assert!(
|
||||
matches!(cli.command, Commands::Account(AccountCommand { subcommand: AccountSubCommand::Rename { acc, new_name }, .. }) if acc == "acc1" && new_name == "acc_new")
|
||||
matches!(cli.command, Commands::Rename { acc, new_name } if acc == "acc1" && new_name == "acc_new")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
pub mod auth;
|
||||
pub mod cli;
|
||||
pub mod store;
|
||||
pub mod util;
|
||||
pub mod auth;
|
||||
|
||||
pub mod logic;
|
||||
+374
@@ -0,0 +1,374 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::store::Account;
|
||||
use anyhow::Context;
|
||||
use codexport::AccountPlanType;
|
||||
use codexport::RateLimit;
|
||||
use codexport::RateLimitWindow;
|
||||
|
||||
/// what is the best account?
|
||||
pub fn best_account(accounts: &[Account]) -> anyhow::Result<&Account> {
|
||||
// Placeholder implementation - replace with actual logic to determine the best account
|
||||
accounts
|
||||
.first()
|
||||
.ok_or_else(|| anyhow::anyhow!("No accounts available"))
|
||||
}
|
||||
|
||||
/// plan weight.
|
||||
/// this is going to be the weight per week. i just made ts up.
|
||||
/// To get weight per session, divide by 6 (or 100/16)
|
||||
///
|
||||
/// this may align with my idea of if there is an account with api key, DON'T switch to it.
|
||||
// that might mean we need to have a priority system.
|
||||
pub fn plan_weight(plan: &AccountPlanType) -> anyhow::Result<u32> {
|
||||
// Placeholder implementation - replace with actual logic to determine the weight of a plan
|
||||
match plan {
|
||||
// weekly faster than pluses session for some reason, so this may get LESS weight, idk
|
||||
AccountPlanType::Free | AccountPlanType::Go => Ok(1),
|
||||
// i think these are the same
|
||||
AccountPlanType::Plus | AccountPlanType::Team | AccountPlanType::Business => Ok(10),
|
||||
// pro 5x
|
||||
AccountPlanType::ProLite => Ok(50),
|
||||
// OG pro, pro 20x
|
||||
AccountPlanType::Pro => Ok(200),
|
||||
// idk below
|
||||
AccountPlanType::Enterprise => anyhow::bail!("Enterprise plan is not supported"),
|
||||
AccountPlanType::SelfServeBusinessUsageBased => {
|
||||
anyhow::bail!("Self-Serve Business Usage-Based plan is not supported")
|
||||
}
|
||||
AccountPlanType::EnterpriseCbpUsageBased => {
|
||||
anyhow::bail!("Enterprise CBP Usage-Based plan is not supported")
|
||||
}
|
||||
AccountPlanType::Edu => anyhow::bail!("Edu plan is not supported"),
|
||||
AccountPlanType::Unknown => anyhow::bail!("Unknown plan type"),
|
||||
}
|
||||
}
|
||||
|
||||
fn now() -> std::time::SystemTime {
|
||||
std::time::SystemTime::now()
|
||||
}
|
||||
|
||||
fn now_u64() -> u64 {
|
||||
now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.expect("failed to calculate duration since UNIX epoch")
|
||||
.as_secs()
|
||||
}
|
||||
|
||||
fn stale_window(window: &RateLimitWindow, now: u64) -> Option<bool> {
|
||||
window.resets_at.map(|resets_at| (resets_at as u64) < now)
|
||||
}
|
||||
|
||||
/// how to interpret the rate limit window:
|
||||
///
|
||||
/// ```rust
|
||||
/// # fn main() -> anyhow::Result<()> {
|
||||
/// # use lda_codex_account::logic::window_weight;
|
||||
/// # use codexport::RateLimitWindow;
|
||||
/// # fn now_u64() -> u64 {
|
||||
/// # std::time::SystemTime::now()
|
||||
/// # .duration_since(std::time::UNIX_EPOCH)
|
||||
/// # .expect("failed to calculate duration since UNIX epoch")
|
||||
/// # .as_secs()
|
||||
/// # }
|
||||
/// # let window = RateLimitWindow {
|
||||
/// # window_minutes: Some(60),
|
||||
/// # used_percent: 50.0,
|
||||
/// # resets_at: Some(now_u64() as i64 + 30 * 60),
|
||||
/// # };
|
||||
/// let (window_duration_minutes, window_weight) = window_weight(window)?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// the weight is determined by the percentage of the window used and the time left until it resets.
|
||||
/// the less of the window that is used and the less time left until it resets, the higher the weight.
|
||||
///
|
||||
/// this only runs if all the Options in the struct are Some.
|
||||
/// If not, i advise not ranking the window at all.
|
||||
///
|
||||
/// Match the window with certain constants (300, 10080) and use that in tandem with plan weight
|
||||
/// to get the final weight for the window.
|
||||
/// [try_match_window]
|
||||
pub fn window_weight(window: &RateLimitWindow) -> anyhow::Result<(u64, f64)> {
|
||||
let now = now();
|
||||
let window_duration = window
|
||||
.window_minutes
|
||||
.context("missing field 'window_minutes'")? as u64;
|
||||
let used = if (0.0..=100.0).contains(&window.used_percent) {
|
||||
window.used_percent
|
||||
} else {
|
||||
anyhow::bail!("invalid 'used_percent' value: {}", window.used_percent);
|
||||
};
|
||||
let left = 100.0 - used;
|
||||
let resets_at = window.resets_at.context("missing field 'resets_at'")?;
|
||||
let resets_at = std::time::UNIX_EPOCH + Duration::from_secs(resets_at as u64);
|
||||
let time_left = resets_at
|
||||
.duration_since(now)
|
||||
.context("stale window")?
|
||||
.as_secs();
|
||||
|
||||
Ok((window_duration, (left / time_left as f64)))
|
||||
}
|
||||
|
||||
/// small function to match the window with certain constants (300, 10080) and use that in tandem with plan weight
|
||||
pub fn try_match_window(window: &RateLimitWindow) -> anyhow::Result<Option<f64>> {
|
||||
let (window_duration, weight) = window_weight(window)?;
|
||||
match window_duration {
|
||||
300 => Ok(Some(weight / 6.25)), // 5 hours
|
||||
10080 => Ok(Some(weight)), // 1 week
|
||||
_ => Ok(None), // unknown window duration, can not determine weight
|
||||
}
|
||||
}
|
||||
|
||||
pub fn account_weight(quota: &RateLimit) -> anyhow::Result<f64> {
|
||||
let plan = quota.plan_type.context("missing field 'plan_type'")?;
|
||||
let plan_weight = plan_weight(&plan)?;
|
||||
let mut total_weight: f64 = 1.0;
|
||||
if let Some(ref primary_window) = quota.primary
|
||||
&& let Some(window_weight) = try_match_window(primary_window)?
|
||||
{
|
||||
total_weight *= window_weight;
|
||||
}
|
||||
if let Some(ref secondary_window) = quota.secondary
|
||||
&& let Some(window_weight) = try_match_window(secondary_window)?
|
||||
{
|
||||
total_weight *= window_weight;
|
||||
}
|
||||
total_weight *= plan_weight as f64;
|
||||
Ok(total_weight)
|
||||
}
|
||||
|
||||
// lets step back and define how the BEST account is determined. we want to consider the following factors:
|
||||
// 1. plan type (pro > plus > free)
|
||||
// 2. rate limit window (how much of the window is used and how much time is left until it resets)
|
||||
// - the primary window vs the secondary window (if the primary window is close to being used up,
|
||||
// we might want to switch to an account with a less used primary window, even if it has a worse plan type)
|
||||
// - a window weight is based on window duration. If it is equal to 5 hours, the weight is 1/6 of the plan weight,
|
||||
// and if its 1 week, the weight is 1 of the plan weight.
|
||||
// - the window weight is determined by the percentage of the window used and the time left until it resets.
|
||||
// the logic could complicate if this account is at the end of a window, which means it will reset soon,
|
||||
// so we might want to switch to it even if it has a worse plan type, because it will reset soon and we can use it for a long time after that.
|
||||
// but if its window is just reset, we might want to switch away from it, because it will be used up soon.
|
||||
// so we need to consider the time left until the window resets, and the percentage of the window that is used.
|
||||
// the logic could complicate further if the secondary window is also close to being used up, or just started.
|
||||
|
||||
// imagine big Lda using this thing in an hour. An hour often depletes the plus 5h window. Because openai is just so bad.
|
||||
// an account that has 50% of its primary window used, but it resets in 30 minutes.
|
||||
// its going to be better than a fresh one but resets in 5 hours.
|
||||
// so we need to consider the time left until the window resets, and the percentage of the window that is used.
|
||||
|
||||
/// standard tests
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
/// i lowk need this function outside
|
||||
fn w(window_minutes: u64, used_percent: f64, resets_at: i64) -> codexport::RateLimitWindow {
|
||||
codexport::RateLimitWindow {
|
||||
window_minutes: Some(window_minutes as i64),
|
||||
used_percent,
|
||||
resets_at: Some(resets_at),
|
||||
}
|
||||
}
|
||||
|
||||
fn rl(
|
||||
plan_type: AccountPlanType,
|
||||
primary: Option<RateLimitWindow>,
|
||||
secondary: Option<RateLimitWindow>,
|
||||
) -> codexport::RateLimit {
|
||||
codexport::RateLimit {
|
||||
plan_type: Some(plan_type),
|
||||
primary,
|
||||
secondary,
|
||||
limit_id: None,
|
||||
limit_name: None,
|
||||
rate_limit_reached_type: None,
|
||||
credits: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// these are gonna test some of my theories
|
||||
mod lda_tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn weekly_better_than_five_hours() {
|
||||
let five_hours_window = w(300, 50.0, now_u64() as i64 + 30 * 60); // 5 hours window, 50% used, resets in 30 minutes
|
||||
let weekly_window = w(10080, 50.0, now_u64() as i64 + 30 * 60); // 1 week window, 50% used, resets in 30 minutes
|
||||
let five_hours_weight = crate::logic::try_match_window(&five_hours_window)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let weekly_weight = crate::logic::try_match_window(&weekly_window)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
println!(
|
||||
"Five hours weight: {}, Weekly weight: {}",
|
||||
five_hours_weight, weekly_weight
|
||||
);
|
||||
assert!(weekly_weight > five_hours_weight);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn five_hours_worse_than_30_minutes() {
|
||||
let window = 300; // 5 hours window
|
||||
let five_hours_window = w(window, 0.0, now_u64() as i64 + 300 * 60); // 5 hours window, 0% used, resets in 5 hours
|
||||
let thirty_minutes_window = w(window, 0.0, now_u64() as i64 + 30 * 60); // 5 hours window, 0% used, resets in 30 minutes
|
||||
let five_hours_weight = crate::logic::try_match_window(&five_hours_window)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let thirty_minutes_weight = crate::logic::try_match_window(&thirty_minutes_window)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
println!(
|
||||
"Five hours weight: {}, Thirty minutes weight: {}",
|
||||
five_hours_weight, thirty_minutes_weight
|
||||
);
|
||||
assert!(five_hours_weight < thirty_minutes_weight);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pro_20x_better_than_plus() {
|
||||
let pro_window = w(300, 0.0, now_u64() as i64 + 30 * 60); // 5 hours window, 0% used, resets in 30 minutes
|
||||
let plus_window = w(300, 0.0, now_u64() as i64 + 30 * 60); // 5 hours window, 0% used, resets in 30 minutes
|
||||
let pro_quota = rl(AccountPlanType::Pro, Some(pro_window), None);
|
||||
let plus_quota = rl(AccountPlanType::Plus, Some(plus_window), None);
|
||||
let pro_weight = crate::logic::account_weight(&pro_quota).unwrap();
|
||||
let plus_weight = crate::logic::account_weight(&plus_quota).unwrap();
|
||||
println!("Pro weight: {}, Plus weight: {}", pro_weight, plus_weight);
|
||||
assert!(pro_weight > plus_weight);
|
||||
assert!(pro_weight > 19.9 * plus_weight);
|
||||
assert!(pro_weight < 20.1 * plus_weight); // lmao
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn weekly_0_left_is_dead() {
|
||||
let now = now_u64() as i64;
|
||||
let five_hours_window = w(300, 30.0, now + 30 * 60); // 5 hours window, 30% used, resets in 30 minutes
|
||||
let weekly_window = w(10080, 100.0, now + 2 * 24 * 60); // 1 week window, 100% used, resets in 2 days
|
||||
|
||||
let mut rate_limit = rl(
|
||||
AccountPlanType::Pro,
|
||||
Some(five_hours_window),
|
||||
Some(weekly_window),
|
||||
);
|
||||
rate_limit.rate_limit_reached_type = // this simulates the case where the account has hit the weekly limit
|
||||
Some(codexport::RateLimitReachedType::RateLimitReached);
|
||||
|
||||
let total_weight = crate::logic::account_weight(&rate_limit).unwrap();
|
||||
println!("Total weight: {}", total_weight);
|
||||
assert!(total_weight <= 0.0); // its dead
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn weekly_low_doesnt_care_about_5h() {
|
||||
let now = now_u64() as i64;
|
||||
let fh_left_1 = 90.0; // 90% left in 5 hours window
|
||||
let fh_left_2 = 50.0; // 50% left in 5 hours window
|
||||
let w_left = 4.0; // 4% left in weekly window
|
||||
|
||||
assert!(w_left * 6.25 < fh_left_1);
|
||||
assert!(w_left * 6.25 < fh_left_2);
|
||||
// so the weekly window is going to go out before the 5 hours window
|
||||
|
||||
let five_hours_window = w(300, fh_left_1, now + 30 * 60); // 5 hours window, 90% used, resets in 30 minutes
|
||||
let five_hours_window_2 = w(300, fh_left_2, now + 30 * 60); // 5 hours window, 50% used, resets in 30 minutes
|
||||
let weekly_window = w(10080, w_left, now + 2 * 24 * 60); // 1 week window, 4% used, resets in 2 days
|
||||
|
||||
let rate_limit = rl(
|
||||
AccountPlanType::Pro,
|
||||
Some(five_hours_window),
|
||||
Some(weekly_window.clone()),
|
||||
);
|
||||
let rate_limit_2 = rl(
|
||||
AccountPlanType::Pro,
|
||||
Some(five_hours_window_2),
|
||||
Some(weekly_window),
|
||||
);
|
||||
|
||||
let total_weight = crate::logic::account_weight(&rate_limit).unwrap();
|
||||
let total_weight_2 = crate::logic::account_weight(&rate_limit_2).unwrap();
|
||||
println!("Total weight: {}", total_weight);
|
||||
println!("Total weight 2: {}", total_weight_2);
|
||||
assert!(total_weight > 0.0); // its not dead
|
||||
assert!(total_weight_2 > 0.0); // its not dead either
|
||||
// assert!(total_weight == total_weight_2); // its just as dead tho
|
||||
assert!(total_weight >= total_weight_2);
|
||||
// should i go through with that comparison?
|
||||
// the 5 hours window is not going to go out before the weekly
|
||||
// window, so it doesn't matter how much is left in the 5 hours
|
||||
// window, because the weekly window is going to go out first
|
||||
// however, for comparisons like this, should one still be lower than the other?
|
||||
// in this case, they are the same, because the weekly window is the bottleneck.
|
||||
// The API generously lets us finish turns even if there is 0% left.
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_window_weight() {
|
||||
let now = now_u64() as i64;
|
||||
let window = w(300, 50.0, now + 30 * 60); // 5 hours window, 50% used, resets in 30 minutes
|
||||
let (duration, weight) = crate::logic::window_weight(&window).unwrap();
|
||||
println!("Duration: {} minutes, Weight: {}", duration, weight);
|
||||
assert_eq!(duration, 300);
|
||||
assert!(weight > 0.0);
|
||||
}
|
||||
#[test]
|
||||
fn test_try_match_window() {
|
||||
let now = now_u64() as i64;
|
||||
let window = w(300, 50.0, now + 30 * 60); // 5 hours window, 50% used, resets in 30 minutes
|
||||
let weight = crate::logic::try_match_window(&window).unwrap();
|
||||
println!("Weight: {:?}", weight);
|
||||
assert!(weight.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_try_match_window_unknown() {
|
||||
let now = now_u64() as i64;
|
||||
let window = w(12345, 50.0, now + 30 * 60); // unknown window duration
|
||||
let weight = crate::logic::try_match_window(&window).unwrap();
|
||||
println!("Weight: {:?}", weight);
|
||||
assert!(weight.is_none());
|
||||
}
|
||||
#[test]
|
||||
fn test_plan_weight() {
|
||||
use codexport::AccountPlanType::*;
|
||||
let plans = [Free, Go, Plus, Team, Business, ProLite, Pro]; // supported plans
|
||||
for plan in plans {
|
||||
let weight = crate::logic::plan_weight(&plan).unwrap();
|
||||
println!("Plan: {:?}, Weight: {}", plan, weight);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_window_weight_invalid_used_percent() {
|
||||
let now = now_u64() as i64;
|
||||
// SUPA EDGES
|
||||
for i in [150.0, 0.0_f64.next_down(), 100.0_f64.next_up()] {
|
||||
let window = w(300, i, now + 30 * 60);
|
||||
let result = crate::logic::window_weight(&window);
|
||||
println!("Result for used_percent {}: {:?}", i, result);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_window_weight_valid_used_percent() {
|
||||
let now = now_u64() as i64;
|
||||
for i in [0.0, 50.0, 100.0] {
|
||||
let window = w(300, i, now + 30 * 60);
|
||||
let result = crate::logic::window_weight(&window);
|
||||
println!("Result for used_percent {}: {:?}", i, result);
|
||||
assert!(result.is_ok());
|
||||
if i == 100.0 {
|
||||
assert_eq!(result.unwrap().1, 0.0); // if 100% used, weight should be 0
|
||||
} else if i < 100.0 {
|
||||
assert!(result.unwrap().1 > 0.0); // if not fully used, weight should be greater than 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_window_weight_stale() {
|
||||
let now = now_u64() as i64;
|
||||
let window = w(300, 50.0, now - 30 * 60); // stale window, reset time is in the past
|
||||
let result = crate::logic::window_weight(&window);
|
||||
println!("Result: {:?}", result);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
+69
-1
@@ -1,3 +1,71 @@
|
||||
//! deals with storage of accounts.
|
||||
//! updates, pull out, standard crud... all here.
|
||||
//! 0 logic to do with the actual accounts, just storage and retrieval.
|
||||
//! 0 logic to do with the actual accounts, just storage and retrieval.
|
||||
|
||||
use codexport::AuthDotJson;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Account {
|
||||
pub id: String,
|
||||
pub nickname: String,
|
||||
pub auth_json: AuthDotJson,
|
||||
}
|
||||
|
||||
/// this is the interface for storing accounts. We can have different
|
||||
/// implementations, such as in-memory, file-based, or even a database. The key
|
||||
/// is that the rest of the code should not care about how the accounts are
|
||||
/// stored, as long as it can save, load, delete, list, and rename accounts.
|
||||
pub trait AccountStore {
|
||||
fn upsert(&mut self, account: Account) -> anyhow::Result<()>;
|
||||
fn load(&self, id: &str) -> anyhow::Result<Account>;
|
||||
fn delete(&mut self, id: &str) -> anyhow::Result<()>;
|
||||
fn list(&self) -> anyhow::Result<Vec<Account>>;
|
||||
fn rename(&mut self, id: &str, new_nickname: &str) -> anyhow::Result<()>;
|
||||
fn has(&self, id: &str) -> anyhow::Result<bool> {
|
||||
Ok(self.load(id).is_ok())
|
||||
}
|
||||
fn get_all(&self) -> anyhow::Result<Vec<Account>> {
|
||||
self.list()
|
||||
}
|
||||
fn clear(&mut self) -> anyhow::Result<()> {
|
||||
for account in self.list()? {
|
||||
self.delete(&account.id)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn count(&self) -> anyhow::Result<usize> {
|
||||
Ok(self.list()?.len())
|
||||
}
|
||||
fn is_empty(&self) -> anyhow::Result<bool> {
|
||||
Ok(self.count()? == 0)
|
||||
}
|
||||
fn load_by_nickname(&self, nickname: &str) -> anyhow::Result<Account> {
|
||||
let accounts = self.list()?;
|
||||
for account in accounts {
|
||||
if account.nickname == nickname {
|
||||
return Ok(account);
|
||||
}
|
||||
}
|
||||
Err(anyhow::anyhow!(
|
||||
"Account with nickname '{}' not found",
|
||||
nickname
|
||||
))
|
||||
}
|
||||
fn delete_by_nickname(&mut self, nickname: &str) -> anyhow::Result<()> {
|
||||
let account = self.load_by_nickname(nickname)?;
|
||||
self.delete(&account.id)
|
||||
}
|
||||
fn rename_by_nickname(&mut self, old_nickname: &str, new_nickname: &str) -> anyhow::Result<()> {
|
||||
let account = self.load_by_nickname(old_nickname)?;
|
||||
self.rename(&account.id, new_nickname)
|
||||
}
|
||||
}
|
||||
|
||||
pub mod file;
|
||||
pub mod in_memory;
|
||||
pub mod file2;
|
||||
|
||||
// ooh! account strore has a lil PROBLEM. we should not let name dictate the key, of the inmem store. the key, is the account ID!
|
||||
// the name is a nickname, which can be changed however. And it is not, the key!
|
||||
// but nicknames are useful. we should support these.
|
||||
@@ -0,0 +1,79 @@
|
||||
//! file store!
|
||||
|
||||
use crate::store::Account;
|
||||
|
||||
use super::AccountStore;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FileStore {
|
||||
dir: PathBuf,
|
||||
}
|
||||
|
||||
impl FileStore {
|
||||
pub fn new<P: AsRef<Path>>(dir: P) -> Self {
|
||||
Self {
|
||||
dir: dir.as_ref().to_path_buf(),
|
||||
}
|
||||
}
|
||||
|
||||
fn account_path(&self, id: &str) -> PathBuf {
|
||||
self.dir.join(format!("{}.json", id))
|
||||
}
|
||||
|
||||
pub fn ensure_dir(&self) -> anyhow::Result<()> {
|
||||
if !self.dir.exists() {
|
||||
fs::create_dir_all(&self.dir)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn dir(&self) -> &Path {
|
||||
&self.dir
|
||||
}
|
||||
}
|
||||
|
||||
impl AccountStore for FileStore {
|
||||
fn upsert(&mut self, account: Account) -> anyhow::Result<()> {
|
||||
let path = self.account_path(&account.id);
|
||||
let data = serde_json::to_string_pretty(&account)?;
|
||||
fs::write(path, data)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load(&self, id: &str) -> anyhow::Result<Account> {
|
||||
let path = self.account_path(id);
|
||||
let data = fs::read_to_string(path)?;
|
||||
let account: Account = serde_json::from_str(&data)?;
|
||||
Ok(account)
|
||||
}
|
||||
|
||||
fn delete(&mut self, id: &str) -> anyhow::Result<()> {
|
||||
let path = self.account_path(id);
|
||||
fs::remove_file(path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn list(&self) -> anyhow::Result<Vec<Account>> {
|
||||
let mut accounts = Vec::new();
|
||||
for entry in fs::read_dir(&self.dir)? {
|
||||
let entry = entry?;
|
||||
if entry.path().is_file()
|
||||
&& let Some(ext) = entry.path().extension()
|
||||
&& ext == "json"
|
||||
{
|
||||
let data = fs::read_to_string(entry.path())?;
|
||||
let account: Account = serde_json::from_str(&data)?;
|
||||
accounts.push(account);
|
||||
}
|
||||
}
|
||||
Ok(accounts)
|
||||
}
|
||||
|
||||
fn rename(&mut self, id: &str, new_nickname: &str) -> anyhow::Result<()> {
|
||||
let mut account = self.load(id)?;
|
||||
account.nickname = new_nickname.to_string();
|
||||
self.upsert(account)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
//! this setup gives you a codex home if you want to also
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::store::{Account, AccountStore};
|
||||
|
||||
/// this is a file-based implementation of the AccountStore trait. it stores the accounts in a json file, and the auth.json files in the codex home directories.
|
||||
pub struct FileAccountStore {
|
||||
pub db: Db,
|
||||
pub homes: Homes,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Db {
|
||||
#[serde(skip)]
|
||||
pub path: std::path::PathBuf,
|
||||
/// this is id: nickname. search for nicknames is O(n) but whatever, we are not expecting a huge number of accounts. and this is just a demo.
|
||||
#[serde(flatten)]
|
||||
store: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Db {
|
||||
fn new(path: std::path::PathBuf) -> Self {
|
||||
Self {
|
||||
path,
|
||||
store: HashMap::new(),
|
||||
}
|
||||
}
|
||||
pub fn load(path: std::path::PathBuf) -> anyhow::Result<Self> {
|
||||
if !path.exists() {
|
||||
return Ok(Self::new(path));
|
||||
}
|
||||
let data = std::fs::read_to_string(&path)?;
|
||||
let store: HashMap<_, _> = serde_json::from_str(&data)?;
|
||||
let db = Self { path, store };
|
||||
Ok(db)
|
||||
}
|
||||
/// call this after you make changes to the store, to save it to disk. this is separate from the upsert/delete functions, so you can make multiple changes and then save once.
|
||||
pub fn save(&self) -> anyhow::Result<()> {
|
||||
let data = serde_json::to_string_pretty(&self)?;
|
||||
std::fs::write(&self.path, data)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Homes {
|
||||
path: std::path::PathBuf,
|
||||
}
|
||||
|
||||
impl Homes {
|
||||
/// CODEX_HOME, not auth.json
|
||||
pub fn get_home(&self, id: &str) -> std::path::PathBuf {
|
||||
self.path.join(id)
|
||||
}
|
||||
|
||||
/// the file that will deserialize into [`AuthDotJson`](codexport::AuthDotJson)
|
||||
pub fn get_auth_json_path(&self, id: &str) -> std::path::PathBuf {
|
||||
self.get_home(id).join("auth.json") // codex expects this file name
|
||||
}
|
||||
}
|
||||
|
||||
impl AccountStore for FileAccountStore {
|
||||
fn upsert(&mut self, account: Account) -> anyhow::Result<()> {
|
||||
{
|
||||
let codex_home = self.homes.get_home(&account.id);
|
||||
std::fs::create_dir_all(&codex_home)?;
|
||||
codexport::save_auth_json_file(&codex_home, account.auth_json)?;
|
||||
}
|
||||
{
|
||||
self.db.store.insert(account.id, account.nickname);
|
||||
self.db.save()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load(&self, id: &str) -> anyhow::Result<Account> {
|
||||
let nickname = self
|
||||
.db
|
||||
.store
|
||||
.get(id)
|
||||
.ok_or_else(|| anyhow::anyhow!("Account with id '{}' not found", id))?
|
||||
.clone();
|
||||
let auth_json = codexport::file_auth_json(&self.homes.get_auth_json_path(id))?;
|
||||
Ok(Account {
|
||||
id: id.to_string(),
|
||||
nickname,
|
||||
auth_json,
|
||||
})
|
||||
}
|
||||
|
||||
/// this will not delete the directory of the account, if in case you want to login again
|
||||
fn delete(&mut self, id: &str) -> anyhow::Result<()> {
|
||||
self.db.store.remove(id);
|
||||
self.db.save()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn list(&self) -> anyhow::Result<Vec<Account>> {
|
||||
let mut accounts = Vec::new();
|
||||
for (id, nickname) in &self.db.store {
|
||||
let auth_json = codexport::file_auth_json(&self.homes.get_auth_json_path(id))?;
|
||||
accounts.push(Account {
|
||||
id: id.clone(),
|
||||
nickname: nickname.clone(),
|
||||
auth_json,
|
||||
});
|
||||
}
|
||||
Ok(accounts)
|
||||
}
|
||||
|
||||
fn rename(&mut self, id: &str, new_nickname: &str) -> anyhow::Result<()> {
|
||||
if let Some(nickname) = self.db.store.get_mut(id) {
|
||||
*nickname = new_nickname.to_string();
|
||||
self.db.save()?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Account with id '{}' not found", id))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
//! in memory store!
|
||||
|
||||
|
||||
use crate::store::Account;
|
||||
|
||||
use super::AccountStore;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct InMemoryStore {
|
||||
/// id to account map!
|
||||
accounts: std::collections::HashMap<String, Account>,
|
||||
}
|
||||
|
||||
impl AccountStore for InMemoryStore {
|
||||
fn upsert(&mut self, account: Account) -> anyhow::Result<()> {
|
||||
self.accounts.insert(account.id.clone(), account);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load(&self, id: &str) -> anyhow::Result<Account> {
|
||||
self.accounts
|
||||
.get(id)
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow::anyhow!("Account with id '{}' not found", id))
|
||||
}
|
||||
|
||||
fn delete(&mut self, id: &str) -> anyhow::Result<()> {
|
||||
self.accounts
|
||||
.remove(id)
|
||||
.ok_or_else(|| anyhow::anyhow!("Account with id '{}' not found", id))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn list(&self) -> anyhow::Result<Vec<Account>> {
|
||||
Ok(self.accounts.values().cloned().collect())
|
||||
}
|
||||
|
||||
fn rename(&mut self, id: &str, new_nickname: &str) -> anyhow::Result<()> {
|
||||
let account = self
|
||||
.accounts
|
||||
.get_mut(id)
|
||||
.ok_or_else(|| anyhow::anyhow!("Account with id '{}' not found", id))?;
|
||||
account.nickname = new_nickname.to_string();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1 +1 @@
|
||||
//! whatever looks like an general utility goes here.
|
||||
//! whatever looks like an general utility goes here.
|
||||
Reference in new issue
Block a user