From b42d1de1fd0cb0a770c05dfe8f3aa53439264264 Mon Sep 17 00:00:00 2001 From: KercyDing Date: Mon, 17 Nov 2025 03:15:10 +0800 Subject: [PATCH 1/9] feat: add optional Python version override for copy command Add support for specifying a custom Python version when copying environments via the new `--python` flag. If not provided, the command will continue to use the source environment's Python version as before. --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/cli.rs | 8 +++++++- src/commands/copy.rs | 10 +++++++--- src/main.rs | 6 +++++- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bdd9ae9..0c1a042 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -932,7 +932,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uvup" -version = "0.1.2" +version = "0.1.4" dependencies = [ "clap", "dirs", diff --git a/Cargo.toml b/Cargo.toml index fa91e2b..b669c65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uvup" -version = "0.1.2" +version = "0.1.4" edition = "2024" rust-version = "1.85" authors = ["KercyDing"] diff --git a/src/cli.rs b/src/cli.rs index 9cfa55b..9e9eba8 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -38,12 +38,18 @@ pub(crate) enum Commands { check: bool, }, - #[command(about = "Copy an environment to a new environment")] + #[command( + about = "Copy an environment to a new environment", + override_usage = "uvup copy [OPTIONS] --name " + )] Copy { #[arg(help = "Source environment name")] source: String, #[arg(short, long, help = "Target environment name")] name: String, + + #[arg(short, long, help = "Python version for target environment (optional)")] + python: Option, }, } diff --git a/src/commands/copy.rs b/src/commands/copy.rs index 1ee55eb..f5feec8 100644 --- a/src/commands/copy.rs +++ b/src/commands/copy.rs @@ -8,7 +8,7 @@ use std::fs; use std::path::Path; use std::process::Command; -pub(crate) fn run(source: String, target: String) -> Result<()> { +pub(crate) fn run(source: String, target: String, python: Option<&str>) -> Result<()> { validate_env_name(&source)?; validate_env_name(&target)?; @@ -28,8 +28,12 @@ pub(crate) fn run(source: String, target: String) -> Result<()> { println!("Exporting packages from '{source}'..."); let requirements = export_packages(&source_path)?; - // Detect Python version from source environment - let python_version = get_python_version(&source_path)?; + // Determine Python version: use provided version or detect from source + let python_version = if let Some(version) = python { + version.to_string() + } else { + get_python_version(&source_path)? + }; // Create target environment println!("Creating environment '{target}' with Python {python_version}..."); diff --git a/src/main.rs b/src/main.rs index 50421f0..962cd60 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,7 +28,11 @@ fn run() -> Result<()> { Commands::List => commands::list::run()?, Commands::Remove { name } => commands::remove::run(name)?, Commands::Update { check } => commands::update::run(check)?, - Commands::Copy { source, name } => commands::copy::run(source, name)?, + Commands::Copy { + source, + name, + python, + } => commands::copy::run(source, name, python.as_deref())?, } Ok(()) From be359423332e373973bfbf5ca174a0f9583cdca6 Mon Sep 17 00:00:00 2001 From: KercyDing Date: Mon, 17 Nov 2025 03:59:36 +0800 Subject: [PATCH 2/9] feat(copy): add package filtering and local venv support Add new options to the `copy` command for more flexible environment copying: - Add `--local` flag to copy environment to `.venv` in current directory - Add `--exclude` option to filter out specific packages (comma-separated) - Add `--include` option to copy only specified packages (comma-separated) - Make `--name` optional and conflict with `--local` flag - Add user confirmation prompt when creating local `.venv` These changes enable more granular control over environment copying, allowing users to create project-local virtual environments and selectively copy packages based on their needs. --- src/cli.rs | 26 +++++++- src/commands/copy.rs | 139 +++++++++++++++++++++++++++++++++++++++---- src/error.rs | 4 ++ src/main.rs | 12 +++- 4 files changed, 167 insertions(+), 14 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 9e9eba8..8c8baf3 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -46,10 +46,32 @@ pub(crate) enum Commands { #[arg(help = "Source environment name")] source: String, - #[arg(short, long, help = "Target environment name")] - name: String, + #[arg( + short, + long, + help = "Target environment name (or use --local)", + conflicts_with = "local" + )] + name: Option, #[arg(short, long, help = "Python version for target environment (optional)")] python: Option, + + #[arg( + long, + value_delimiter = ',', + help = "Exclude packages (comma-separated)" + )] + exclude: Option>, + + #[arg( + long, + value_delimiter = ',', + help = "Include only these packages (comma-separated)" + )] + include: Option>, + + #[arg(short, long, help = "Copy to .venv in current directory")] + local: bool, }, } diff --git a/src/commands/copy.rs b/src/commands/copy.rs index f5feec8..586ebed 100644 --- a/src/commands/copy.rs +++ b/src/commands/copy.rs @@ -4,29 +4,71 @@ use crate::env::paths::{get_env_path, get_envs_dir, validate_env_name}; use crate::error::{Result, UvupError}; use crate::utils::print_success; +use std::env; use std::fs; -use std::path::Path; +use std::io::{self, Write}; +use std::path::{Path, PathBuf}; use std::process::Command; -pub(crate) fn run(source: String, target: String, python: Option<&str>) -> Result<()> { +pub(crate) fn run( + source: String, + name: Option, + python: Option<&str>, + exclude: Option<&[String]>, + include: Option<&[String]>, + local: bool, +) -> Result<()> { validate_env_name(&source)?; - validate_env_name(&target)?; + + // Determine target path and name + let (target_name, target_path) = if local { + let current_dir = env::current_dir() + .map_err(|e| UvupError::PathError(format!("Failed to get current directory: {e}")))?; + let venv_path = current_dir.join(".venv"); + + // Prompt user for confirmation + print!("Create .venv in '{}'? [Y/n] ", current_dir.display()); + io::stdout().flush().unwrap(); + + let mut input = String::new(); + io::stdin() + .read_line(&mut input) + .map_err(UvupError::IoError)?; + + let input = input.trim().to_lowercase(); + if !input.is_empty() && input != "y" && input != "yes" { + println!("Cancelled."); + return Ok(()); + } + + (".venv".to_string(), venv_path) + } else { + let target = name + .ok_or_else(|| UvupError::InvalidInput("Must provide --name or --local".to_string()))?; + validate_env_name(&target)?; + let path = get_env_path(&target)?; + (target, path) + }; let source_path = get_env_path(&source)?; if !source_path.exists() { return Err(UvupError::EnvNotFound(source)); } - let target_path = get_env_path(&target)?; if target_path.exists() { - return Err(UvupError::EnvAlreadyExists(target)); + return Err(UvupError::EnvAlreadyExists(target_name.clone())); } - println!("Copying environment '{source}' to '{target}'..."); + println!("Copying environment '{source}' to '{target_name}'..."); // Export packages from source environment println!("Exporting packages from '{source}'..."); - let requirements = export_packages(&source_path)?; + let mut requirements = export_packages(&source_path)?; + + // Apply filters if specified + if exclude.is_some() || include.is_some() { + requirements = filter_packages(&requirements, exclude, include); + } // Determine Python version: use provided version or detect from source let python_version = if let Some(version) = python { @@ -36,18 +78,22 @@ pub(crate) fn run(source: String, target: String, python: Option<&str>) -> Resul }; // Create target environment - println!("Creating environment '{target}' with Python {python_version}..."); - create_environment(&target, &python_version)?; + println!("Creating environment '{target_name}' with Python {python_version}..."); + if local { + create_project_environment(&target_path, &python_version)?; + } else { + create_environment(&target_name, &python_version)?; + } // Sync packages to target environment if requirements.trim().is_empty() { println!("Source environment has no packages installed"); - print_success(&format!("Created empty environment '{target}'")); + print_success(&format!("Created empty environment '{target_name}'")); } else { println!("Installing packages..."); sync_packages(&target_path, &requirements)?; print_success(&format!( - "Successfully copied environment '{source}' to '{target}'" + "Successfully copied environment '{source}' to '{target_name}'" )); } @@ -126,6 +172,77 @@ fn create_environment(name: &str, python_version: &str) -> Result<()> { Ok(()) } +/// Create a project .venv environment using uv venv +fn create_project_environment(venv_path: &PathBuf, python_version: &str) -> Result<()> { + let status = Command::new("uv") + .arg("venv") + .arg(venv_path) + .arg("--python") + .arg(python_version) + .status() + .map_err(|e| UvupError::CommandExecutionFailed(format!("Failed to execute uv: {e}")))?; + + if !status.success() { + return Err(UvupError::CommandExecutionFailed( + "Failed to create project environment".to_string(), + )); + } + + Ok(()) +} + +/// Filter packages based on exclude/include patterns +fn filter_packages( + requirements: &str, + exclude: Option<&[String]>, + include: Option<&[String]>, +) -> String { + let lines: Vec<&str> = requirements.lines().collect(); + + let filtered: Vec<&str> = lines + .into_iter() + .filter(|line| { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + return true; + } + + // Extract package name (before ==, >=, <=, etc.) + let package_name = line + .split(&['=', '>', '<', '~', '!'][..]) + .next() + .unwrap_or(line) + .trim() + .to_lowercase(); + + // If include list is specified, only keep packages in the list + if let Some(include_list) = include { + let included = include_list + .iter() + .any(|inc| package_name.contains(&inc.to_lowercase())); + if !included { + return false; + } + } + + // If exclude list is specified, remove matching packages + if let Some(exclude_list) = exclude { + let excluded = exclude_list + .iter() + .any(|exc| package_name.contains(&exc.to_lowercase())); + if excluded { + println!(" Excluding: {package_name}"); + return false; + } + } + + true + }) + .collect(); + + filtered.join("\n") +} + /// Sync packages to target environment using uv pip sync fn sync_packages(env_path: &Path, requirements: &str) -> Result<()> { // Create temporary requirements file diff --git a/src/error.rs b/src/error.rs index 984c5ae..7442e0d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -7,6 +7,7 @@ pub(crate) enum UvupError { EnvAlreadyExists(String), EnvNotFound(String), InvalidEnvName(String), + InvalidInput(String), ShellDetectionFailed, IoError(io::Error), PathError(String), @@ -38,6 +39,9 @@ impl fmt::Display for UvupError { "Environment names must contain only alphanumeric characters, hyphens, and underscores" ) } + UvupError::InvalidInput(msg) => { + write!(f, "Error: {msg}") + } UvupError::ShellDetectionFailed => { writeln!(f, "Error: Could not detect your shell")?; write!(f, "Supported shells: bash, zsh, fish, powershell") diff --git a/src/main.rs b/src/main.rs index 962cd60..24b81f4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,7 +32,17 @@ fn run() -> Result<()> { source, name, python, - } => commands::copy::run(source, name, python.as_deref())?, + exclude, + include, + local, + } => commands::copy::run( + source, + name, + python.as_deref(), + exclude.as_deref(), + include.as_deref(), + local, + )?, } Ok(()) From 05bc0608fb9cfe035efba031c2439964e3389337 Mon Sep 17 00:00:00 2001 From: KercyDing Date: Mon, 17 Nov 2025 04:10:46 +0800 Subject: [PATCH 3/9] feat(copy): use pip install for cross-version compatibility Switch from `uv pip sync` to `uv pip install` when copying environments with different Python versions to allow automatic dependency resolution. --- src/commands/copy.rs | 46 ++++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/src/commands/copy.rs b/src/commands/copy.rs index 586ebed..d0cbf64 100644 --- a/src/commands/copy.rs +++ b/src/commands/copy.rs @@ -72,6 +72,8 @@ pub(crate) fn run( // Determine Python version: use provided version or detect from source let python_version = if let Some(version) = python { + println!("Note: Switching Python version may cause package compatibility issues."); + println!(" Using 'uv pip install' to automatically resolve compatible versions."); version.to_string() } else { get_python_version(&source_path)? @@ -91,7 +93,8 @@ pub(crate) fn run( print_success(&format!("Created empty environment '{target_name}'")); } else { println!("Installing packages..."); - sync_packages(&target_path, &requirements)?; + let use_sync = python.is_none(); // Use sync only when no Python version specified + sync_packages(&target_path, &requirements, use_sync)?; print_success(&format!( "Successfully copied environment '{source}' to '{target_name}'" )); @@ -243,8 +246,8 @@ fn filter_packages( filtered.join("\n") } -/// Sync packages to target environment using uv pip sync -fn sync_packages(env_path: &Path, requirements: &str) -> Result<()> { +/// Sync packages to target environment using uv pip sync or install +fn sync_packages(env_path: &Path, requirements: &str, use_sync: bool) -> Result<()> { // Create temporary requirements file let temp_file = tempfile::NamedTempFile::new().map_err(|e| { UvupError::CommandExecutionFailed(format!("Failed to create temp file: {e}")) @@ -260,17 +263,32 @@ fn sync_packages(env_path: &Path, requirements: &str) -> Result<()> { env_path.join("bin").join("python") }; - // Use uv pip sync for precise environment reproduction - let status = Command::new("uv") - .arg("pip") - .arg("sync") - .arg("--python") - .arg(&python_bin) - .arg(temp_file.path()) - .status() - .map_err(|e| { - UvupError::CommandExecutionFailed(format!("Failed to execute uv pip sync: {e}")) - })?; + let status = if use_sync { + // Use pip sync for exact version replication (same Python version) + Command::new("uv") + .arg("pip") + .arg("sync") + .arg("--python") + .arg(&python_bin) + .arg(temp_file.path()) + .status() + .map_err(|e| { + UvupError::CommandExecutionFailed(format!("Failed to execute uv pip sync: {e}")) + })? + } else { + // Use pip install for cross-version compatibility (different Python version) + Command::new("uv") + .arg("pip") + .arg("install") + .arg("--python") + .arg(&python_bin) + .arg("-r") + .arg(temp_file.path()) + .status() + .map_err(|e| { + UvupError::CommandExecutionFailed(format!("Failed to execute uv pip install: {e}")) + })? + }; if !status.success() { return Err(UvupError::CommandExecutionFailed( From 5076f62699e31a215aedcfc28863e02e5e9e11bb Mon Sep 17 00:00:00 2001 From: KercyDing Date: Mon, 17 Nov 2025 05:45:18 +0800 Subject: [PATCH 4/9] chore(release): bump version to 0.2.0 and refactor copy command - Bump version from 0.1.4 to 0.2.0 in Cargo.toml and Cargo.lock - Refactor copy command to distinguish between local and project environments - Rename `create_environment` to `create_project_environment` and add `create_local_environment` - Use `get_venv_path` helper to correctly locate virtual environment directories - Fix path handling to properly target `.venv` subdirectory for project environments - Improve clarity by tracking environment type with `is_local` flag This refactoring improves the separation of concerns between local (.venv in current directory) and project (managed in envs directory) environments, ensuring correct path resolution for package operations. --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/commands/copy.rs | 93 ++++++++++++++++++++++++++--------------- src/commands/create.rs | 36 +++++++++++++--- src/commands/list.rs | 7 +++- src/env/paths.rs | 6 +++ src/shell/bash.rs | 2 +- src/shell/fish.rs | 2 +- src/shell/powershell.rs | 2 +- 9 files changed, 107 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0c1a042..1d62fd5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -932,7 +932,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uvup" -version = "0.1.4" +version = "0.2.0" dependencies = [ "clap", "dirs", diff --git a/Cargo.toml b/Cargo.toml index b669c65..08acc87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uvup" -version = "0.1.4" +version = "0.2.0" edition = "2024" rust-version = "1.85" authors = ["KercyDing"] diff --git a/src/commands/copy.rs b/src/commands/copy.rs index d0cbf64..432c82d 100644 --- a/src/commands/copy.rs +++ b/src/commands/copy.rs @@ -1,7 +1,7 @@ // Allow println! in this module as it's used for user-facing output #![allow(clippy::print_stdout)] -use crate::env::paths::{get_env_path, get_envs_dir, validate_env_name}; +use crate::env::paths::{get_env_path, get_envs_dir, get_venv_path, validate_env_name}; use crate::error::{Result, UvupError}; use crate::utils::print_success; use std::env; @@ -21,7 +21,7 @@ pub(crate) fn run( validate_env_name(&source)?; // Determine target path and name - let (target_name, target_path) = if local { + let (target_name, target_path, is_local) = if local { let current_dir = env::current_dir() .map_err(|e| UvupError::PathError(format!("Failed to get current directory: {e}")))?; let venv_path = current_dir.join(".venv"); @@ -41,13 +41,13 @@ pub(crate) fn run( return Ok(()); } - (".venv".to_string(), venv_path) + (".venv".to_string(), venv_path, true) } else { let target = name .ok_or_else(|| UvupError::InvalidInput("Must provide --name or --local".to_string()))?; validate_env_name(&target)?; let path = get_env_path(&target)?; - (target, path) + (target, path, false) }; let source_path = get_env_path(&source)?; @@ -63,7 +63,8 @@ pub(crate) fn run( // Export packages from source environment println!("Exporting packages from '{source}'..."); - let mut requirements = export_packages(&source_path)?; + let source_venv_path = get_venv_path(&source)?; + let mut requirements = export_packages(&source_venv_path)?; // Apply filters if specified if exclude.is_some() || include.is_some() { @@ -76,15 +77,15 @@ pub(crate) fn run( println!(" Using 'uv pip install' to automatically resolve compatible versions."); version.to_string() } else { - get_python_version(&source_path)? + get_python_version(&source_venv_path)? }; // Create target environment println!("Creating environment '{target_name}' with Python {python_version}..."); - if local { - create_project_environment(&target_path, &python_version)?; + if is_local { + create_local_environment(&target_path, &python_version)?; } else { - create_environment(&target_name, &python_version)?; + create_project_environment(&target_path, &python_version)?; } // Sync packages to target environment @@ -93,8 +94,13 @@ pub(crate) fn run( print_success(&format!("Created empty environment '{target_name}'")); } else { println!("Installing packages..."); - let use_sync = python.is_none(); // Use sync only when no Python version specified - sync_packages(&target_path, &requirements, use_sync)?; + let target_venv_path = if is_local { + target_path.clone() + } else { + target_path.join(".venv") + }; + let use_sync = python.is_none(); + sync_packages(&target_venv_path, &requirements, use_sync)?; print_success(&format!( "Successfully copied environment '{source}' to '{target_name}'" )); @@ -104,11 +110,11 @@ pub(crate) fn run( } /// Export packages from an environment using uv pip freeze -fn export_packages(env_path: &Path) -> Result { +fn export_packages(venv_path: &Path) -> Result { let python_bin = if cfg!(windows) { - env_path.join("Scripts").join("python.exe") + venv_path.join("Scripts").join("python.exe") } else { - env_path.join("bin").join("python") + venv_path.join("bin").join("python") }; let output = Command::new("uv") @@ -133,8 +139,8 @@ fn export_packages(env_path: &Path) -> Result { } /// Get Python version from pyvenv.cfg -fn get_python_version(env_path: &Path) -> Result { - let cfg_path = env_path.join("pyvenv.cfg"); +fn get_python_version(venv_path: &Path) -> Result { + let cfg_path = venv_path.join("pyvenv.cfg"); let cfg_content = fs::read_to_string(&cfg_path) .map_err(|e| UvupError::PathError(format!("Failed to read pyvenv.cfg: {e}")))?; @@ -152,31 +158,52 @@ fn get_python_version(env_path: &Path) -> Result { Ok("3.12".to_string()) } -/// Create a new environment using uv venv -fn create_environment(name: &str, python_version: &str) -> Result<()> { - let env_path = get_env_path(name)?; +/// Create a new project environment using uv init + uv venv +fn create_project_environment(project_path: &Path, python_version: &str) -> Result<()> { let envs_dir = get_envs_dir()?; fs::create_dir_all(&envs_dir)?; + fs::create_dir_all(project_path)?; - let status = Command::new("uv") - .arg("venv") - .arg(&env_path) + // Initialize uv project + let init_status = Command::new("uv") + .arg("init") + .arg("--no-readme") .arg("--python") .arg(python_version) + .current_dir(project_path) .status() - .map_err(|e| UvupError::CommandExecutionFailed(format!("Failed to execute uv: {e}")))?; + .map_err(|e| { + UvupError::CommandExecutionFailed(format!("Failed to execute uv init: {e}")) + })?; - if !status.success() { - return Err(UvupError::CommandExecutionFailed(format!( - "Failed to create environment '{name}'" - ))); + if !init_status.success() { + let _ = fs::remove_dir_all(project_path); + return Err(UvupError::CommandExecutionFailed( + "Failed to initialize project".to_string(), + )); + } + + // Create virtual environment + let venv_status = Command::new("uv") + .arg("venv") + .current_dir(project_path) + .status() + .map_err(|e| { + UvupError::CommandExecutionFailed(format!("Failed to execute uv venv: {e}")) + })?; + + if !venv_status.success() { + let _ = fs::remove_dir_all(project_path); + return Err(UvupError::CommandExecutionFailed( + "Failed to create virtual environment".to_string(), + )); } Ok(()) } -/// Create a project .venv environment using uv venv -fn create_project_environment(venv_path: &PathBuf, python_version: &str) -> Result<()> { +/// Create a local .venv environment using uv venv +fn create_local_environment(venv_path: &PathBuf, python_version: &str) -> Result<()> { let status = Command::new("uv") .arg("venv") .arg(venv_path) @@ -187,7 +214,7 @@ fn create_project_environment(venv_path: &PathBuf, python_version: &str) -> Resu if !status.success() { return Err(UvupError::CommandExecutionFailed( - "Failed to create project environment".to_string(), + "Failed to create local environment".to_string(), )); } @@ -247,7 +274,7 @@ fn filter_packages( } /// Sync packages to target environment using uv pip sync or install -fn sync_packages(env_path: &Path, requirements: &str, use_sync: bool) -> Result<()> { +fn sync_packages(venv_path: &Path, requirements: &str, use_sync: bool) -> Result<()> { // Create temporary requirements file let temp_file = tempfile::NamedTempFile::new().map_err(|e| { UvupError::CommandExecutionFailed(format!("Failed to create temp file: {e}")) @@ -258,9 +285,9 @@ fn sync_packages(env_path: &Path, requirements: &str, use_sync: bool) -> Result< })?; let python_bin = if cfg!(windows) { - env_path.join("Scripts").join("python.exe") + venv_path.join("Scripts").join("python.exe") } else { - env_path.join("bin").join("python") + venv_path.join("bin").join("python") }; let status = if use_sync { diff --git a/src/commands/create.rs b/src/commands/create.rs index 475ee1d..9022e80 100644 --- a/src/commands/create.rs +++ b/src/commands/create.rs @@ -22,18 +22,44 @@ pub(crate) fn run(name: String, python_version: Option<&str>) -> Result<()> { let py_version = python_version.unwrap_or(DEFAULT_PYTHON_VERSION); - let status = Command::new("uv") - .arg("venv") - .arg(&env_path) + // Create project directory + fs::create_dir_all(&env_path)?; + + // Initialize uv project + let init_status = Command::new("uv") + .arg("init") + .arg("--no-readme") .arg("--python") .arg(py_version) + .current_dir(&env_path) + .status() + .map_err(|e| { + UvupError::CommandExecutionFailed(format!("Failed to execute uv init: {e}")) + })?; + + if !init_status.success() { + // Clean up on failure + let _ = fs::remove_dir_all(&env_path); + return Err(UvupError::CommandExecutionFailed(format!( + "Failed to initialize project for environment '{name}'" + ))); + } + + // Create virtual environment + let venv_status = Command::new("uv") + .arg("venv") + .current_dir(&env_path) .status() - .map_err(|e| UvupError::CommandExecutionFailed(format!("Failed to execute uv: {e}")))?; + .map_err(|e| { + UvupError::CommandExecutionFailed(format!("Failed to execute uv venv: {e}")) + })?; - if status.success() { + if venv_status.success() { print_success(&format!("Environment '{name}' created successfully")); Ok(()) } else { + // Clean up on failure + let _ = fs::remove_dir_all(&env_path); Err(UvupError::CommandExecutionFailed(format!( "Failed to create environment '{name}'" ))) diff --git a/src/commands/list.rs b/src/commands/list.rs index ecbcc84..cf399b2 100644 --- a/src/commands/list.rs +++ b/src/commands/list.rs @@ -43,13 +43,16 @@ pub(crate) fn run() -> Result<()> { } fn is_valid_env(path: &std::path::Path) -> bool { + // Check for .venv subdirectory structure (new format) + let venv_path = path.join(".venv"); + #[cfg(target_os = "windows")] { - path.join("Scripts").join("Activate.ps1").exists() + venv_path.join("Scripts").join("Activate.ps1").exists() } #[cfg(not(target_os = "windows"))] { - path.join("bin").join("activate").exists() + venv_path.join("bin").join("activate").exists() } } diff --git a/src/env/paths.rs b/src/env/paths.rs index 94ffb1b..43f9189 100644 --- a/src/env/paths.rs +++ b/src/env/paths.rs @@ -10,11 +10,17 @@ pub(crate) fn get_envs_dir() -> Result { Ok(get_home_dir()?.join(".uvup")) } +/// Get the project directory path for an environment (contains pyproject.toml) pub(crate) fn get_env_path(name: &str) -> Result { validate_env_name(name)?; Ok(get_envs_dir()?.join(name)) } +/// Get the .venv directory path for an environment (contains the actual venv) +pub(crate) fn get_venv_path(name: &str) -> Result { + Ok(get_env_path(name)?.join(".venv")) +} + pub(crate) fn validate_env_name(name: &str) -> Result<()> { if name.is_empty() { return Err(UvupError::InvalidEnvName( diff --git a/src/shell/bash.rs b/src/shell/bash.rs index 22d5595..777c02b 100644 --- a/src/shell/bash.rs +++ b/src/shell/bash.rs @@ -7,7 +7,7 @@ uvup() { return 1 fi - local env_path="$HOME/.uvup/$2" + local env_path="$HOME/.uvup/$2/.venv" local activate_script="$env_path/bin/activate" if [ ! -f "$activate_script" ]; then diff --git a/src/shell/fish.rs b/src/shell/fish.rs index 204f985..00b7890 100644 --- a/src/shell/fish.rs +++ b/src/shell/fish.rs @@ -7,7 +7,7 @@ function uvup return 1 end - set -l env_path "$HOME/.uvup/$argv[2]" + set -l env_path "$HOME/.uvup/$argv[2]/.venv" set -l activate_script "$env_path/bin/activate.fish" if not test -f "$activate_script" diff --git a/src/shell/powershell.rs b/src/shell/powershell.rs index add5ef4..f437266 100644 --- a/src/shell/powershell.rs +++ b/src/shell/powershell.rs @@ -9,7 +9,7 @@ function uvup { return } - $envPath = "$env:USERPROFILE\.uvup\$($Arguments[1])" + $envPath = "$env:USERPROFILE\.uvup\$($Arguments[1])\.venv" $activateScript = "$envPath\Scripts\Activate.ps1" if (-not (Test-Path $activateScript)) { From efdaba681f8618608c12040c3b5be462310b0931 Mon Sep 17 00:00:00 2001 From: KercyDing Date: Mon, 17 Nov 2025 07:04:17 +0800 Subject: [PATCH 5/9] refactor(copy): switch from pip-based to pyproject.toml workflow Replace pip freeze/sync approach with direct pyproject.toml manipulation: - Read and modify source pyproject.toml instead of using pip freeze - Filter dependencies directly in TOML (including optional dependencies) - Use uv lock + uv sync for dependency resolution - Add --override flag to handle existing pyproject.toml in local mode - Create automatic backups when overriding existing configs - Add toml_edit dependency for TOML manipulation - Remove unused get_venv_path function --- Cargo.lock | 47 ++++ Cargo.toml | 1 + src/cli.rs | 3 + src/commands/copy.rs | 547 ++++++++++++++++++++++++++----------------- src/env/paths.rs | 5 - src/main.rs | 2 + 6 files changed, 384 insertions(+), 221 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1d62fd5..f0b3d91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -856,6 +856,43 @@ dependencies = [ "zerovec", ] +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" + [[package]] name = "unicode-ident" version = "1.0.22" @@ -939,6 +976,7 @@ dependencies = [ "self-replace", "serde_json", "tempfile", + "toml_edit", "ureq", ] @@ -1134,6 +1172,15 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.46.0" diff --git a/Cargo.toml b/Cargo.toml index 08acc87..07f7c87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ ureq = { version = "3.0", features = ["json"] } serde_json = "1.0" self-replace = "1.5" tempfile = "3.0" +toml_edit = "0.23" [dev-dependencies] diff --git a/src/cli.rs b/src/cli.rs index 8c8baf3..a22e379 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -73,5 +73,8 @@ pub(crate) enum Commands { #[arg(short, long, help = "Copy to .venv in current directory")] local: bool, + + #[arg(long, help = "Override existing pyproject.toml (creates backup)")] + r#override: bool, }, } diff --git a/src/commands/copy.rs b/src/commands/copy.rs index 432c82d..1ac64d7 100644 --- a/src/commands/copy.rs +++ b/src/commands/copy.rs @@ -1,14 +1,14 @@ // Allow println! in this module as it's used for user-facing output #![allow(clippy::print_stdout)] -use crate::env::paths::{get_env_path, get_envs_dir, get_venv_path, validate_env_name}; +use crate::env::paths::{get_env_path, validate_env_name}; use crate::error::{Result, UvupError}; use crate::utils::print_success; use std::env; use std::fs; use std::io::{self, Write}; -use std::path::{Path, PathBuf}; use std::process::Command; +use toml_edit::{DocumentMut, Item, Value}; pub(crate) fn run( source: String, @@ -17,183 +17,229 @@ pub(crate) fn run( exclude: Option<&[String]>, include: Option<&[String]>, local: bool, + override_existing: bool, ) -> Result<()> { validate_env_name(&source)?; - // Determine target path and name - let (target_name, target_path, is_local) = if local { - let current_dir = env::current_dir() - .map_err(|e| UvupError::PathError(format!("Failed to get current directory: {e}")))?; - let venv_path = current_dir.join(".venv"); - - // Prompt user for confirmation - print!("Create .venv in '{}'? [Y/n] ", current_dir.display()); - io::stdout().flush().unwrap(); - - let mut input = String::new(); - io::stdin() - .read_line(&mut input) - .map_err(UvupError::IoError)?; - - let input = input.trim().to_lowercase(); - if !input.is_empty() && input != "y" && input != "yes" { - println!("Cancelled."); - return Ok(()); - } - - (".venv".to_string(), venv_path, true) - } else { - let target = name - .ok_or_else(|| UvupError::InvalidInput("Must provide --name or --local".to_string()))?; - validate_env_name(&target)?; - let path = get_env_path(&target)?; - (target, path, false) - }; - let source_path = get_env_path(&source)?; if !source_path.exists() { return Err(UvupError::EnvNotFound(source)); } - if target_path.exists() { - return Err(UvupError::EnvAlreadyExists(target_name.clone())); - } + // Determine target configuration + let target_config = determine_target_config(name, local, override_existing)?; + check_target_exists(&target_config)?; - println!("Copying environment '{source}' to '{target_name}'..."); + println!( + "Copying environment '{source}' to '{}'...", + target_config.name + ); - // Export packages from source environment - println!("Exporting packages from '{source}'..."); - let source_venv_path = get_venv_path(&source)?; - let mut requirements = export_packages(&source_venv_path)?; + // Process pyproject.toml + let mut doc = read_and_parse_toml(&source_path)?; - // Apply filters if specified if exclude.is_some() || include.is_some() { - requirements = filter_packages(&requirements, exclude, include); + filter_dependencies(&mut doc, exclude, include)?; } - // Determine Python version: use provided version or detect from source let python_version = if let Some(version) = python { println!("Note: Switching Python version may cause package compatibility issues."); - println!(" Using 'uv pip install' to automatically resolve compatible versions."); + update_python_version(&mut doc, version)?; version.to_string() } else { - get_python_version(&source_venv_path)? + get_python_version_from_toml(&doc)? }; - // Create target environment - println!("Creating environment '{target_name}' with Python {python_version}..."); - if is_local { - create_local_environment(&target_path, &python_version)?; - } else { - create_project_environment(&target_path, &python_version)?; - } + // Create environment + println!( + "Creating environment '{}' with Python {python_version}...", + target_config.name + ); + create_environment(&target_config, &source_path, &doc)?; - // Sync packages to target environment - if requirements.trim().is_empty() { - println!("Source environment has no packages installed"); - print_success(&format!("Created empty environment '{target_name}'")); - } else { - println!("Installing packages..."); - let target_venv_path = if is_local { - target_path.clone() - } else { - target_path.join(".venv") - }; - let use_sync = python.is_none(); - sync_packages(&target_venv_path, &requirements, use_sync)?; - print_success(&format!( - "Successfully copied environment '{source}' to '{target_name}'" - )); - } + // Sync packages + println!("Installing packages..."); + sync_environment(&target_config)?; + + print_success(&format!( + "Successfully copied environment '{source}' to '{}'", + target_config.name + )); Ok(()) } -/// Export packages from an environment using uv pip freeze -fn export_packages(venv_path: &Path) -> Result { - let python_bin = if cfg!(windows) { - venv_path.join("Scripts").join("python.exe") +struct TargetConfig { + name: String, + path: std::path::PathBuf, + is_local: bool, +} + +/// Determine target configuration based on user input +fn determine_target_config( + name: Option, + local: bool, + override_existing: bool, +) -> Result { + if local { + let current_dir = env::current_dir() + .map_err(|e| UvupError::PathError(format!("Failed to get current directory: {e}")))?; + + let pyproject_path = current_dir.join("pyproject.toml"); + + // Check if pyproject.toml exists + if pyproject_path.exists() { + if !override_existing { + // No --override flag: tell user to add it + return Err(UvupError::InvalidInput( + "Current directory already has pyproject.toml.\nUse --override to replace it (will create backup as pyproject.toml.backup).".to_string() + )); + } + + // With --override flag: ask for confirmation + print!( + "Replace existing pyproject.toml with environment configuration? [y/N] " + ); + io::stdout().flush().unwrap(); + + let mut input = String::new(); + io::stdin() + .read_line(&mut input) + .map_err(UvupError::IoError)?; + + let input = input.trim().to_lowercase(); + if input != "y" && input != "yes" { + println!("Cancelled."); + return Err(UvupError::InvalidInput("Cancelled by user".to_string())); + } + + // Backup existing pyproject.toml + let backup_path = current_dir.join("pyproject.toml.backup"); + fs::copy(&pyproject_path, &backup_path).map_err(|e| { + UvupError::PathError(format!("Failed to create backup: {e}")) + })?; + println!("Backed up existing pyproject.toml to pyproject.toml.backup"); + } else { + // No pyproject.toml: simple confirmation + print!("Create .venv in '{}'? [Y/n] ", current_dir.display()); + io::stdout().flush().unwrap(); + + let mut input = String::new(); + io::stdin() + .read_line(&mut input) + .map_err(UvupError::IoError)?; + + let input = input.trim().to_lowercase(); + if !input.is_empty() && input != "y" && input != "yes" { + println!("Cancelled."); + return Err(UvupError::InvalidInput("Cancelled by user".to_string())); + } + } + + Ok(TargetConfig { + name: ".venv".to_string(), + path: current_dir, + is_local: true, + }) } else { - venv_path.join("bin").join("python") - }; + let target = name + .ok_or_else(|| UvupError::InvalidInput("Must provide --name or --local".to_string()))?; + validate_env_name(&target)?; + let path = get_env_path(&target)?; + Ok(TargetConfig { + name: target, + path, + is_local: false, + }) + } +} - let output = Command::new("uv") - .arg("pip") - .arg("freeze") - .arg("--python") - .arg(&python_bin) - .output() - .map_err(|e| { - UvupError::CommandExecutionFailed(format!("Failed to run uv pip freeze: {e}")) - })?; +/// Check if target already exists +fn check_target_exists(config: &TargetConfig) -> Result<()> { + if !config.is_local && config.path.exists() { + return Err(UvupError::EnvAlreadyExists(config.name.clone())); + } - if !output.status.success() { - return Err(UvupError::CommandExecutionFailed( - "Failed to export packages".to_string(), - )); + if config.is_local && config.path.join(".venv").exists() { + return Err(UvupError::EnvAlreadyExists(".venv".to_string())); } - String::from_utf8(output.stdout).map_err(|e| { - UvupError::CommandExecutionFailed(format!("Invalid UTF-8 in package list: {e}")) + Ok(()) +} + +/// Read and parse source pyproject.toml +fn read_and_parse_toml(source_path: &std::path::Path) -> Result { + let source_toml_path = source_path.join("pyproject.toml"); + let toml_content = fs::read_to_string(&source_toml_path).map_err(|e| { + UvupError::PathError(format!("Failed to read source pyproject.toml: {e}")) + })?; + + toml_content.parse::().map_err(|e| { + UvupError::CommandExecutionFailed(format!("Failed to parse pyproject.toml: {e}")) }) } -/// Get Python version from pyvenv.cfg -fn get_python_version(venv_path: &Path) -> Result { - let cfg_path = venv_path.join("pyvenv.cfg"); - let cfg_content = fs::read_to_string(&cfg_path) - .map_err(|e| UvupError::PathError(format!("Failed to read pyvenv.cfg: {e}")))?; - - // Parse version_info line (e.g., "version_info = 3.12.11") - for line in cfg_content.lines() { - if let Some(version) = line.strip_prefix("version_info = ") { - let parts: Vec<&str> = version.split('.').collect(); - if parts.len() >= 2 { - return Ok(format!("{}.{}", parts[0], parts[1])); - } - } +/// Create environment (local or project) +fn create_environment( + config: &TargetConfig, + source_path: &std::path::Path, + doc: &DocumentMut, +) -> Result<()> { + if config.is_local { + create_local_environment(config, doc) + } else { + create_project_environment(config, source_path, doc) } - - // Fallback to default version - Ok("3.12".to_string()) } -/// Create a new project environment using uv init + uv venv -fn create_project_environment(project_path: &Path, python_version: &str) -> Result<()> { - let envs_dir = get_envs_dir()?; - fs::create_dir_all(&envs_dir)?; - fs::create_dir_all(project_path)?; - - // Initialize uv project - let init_status = Command::new("uv") - .arg("init") - .arg("--no-readme") - .arg("--python") - .arg(python_version) - .current_dir(project_path) +/// Create local .venv environment +fn create_local_environment(config: &TargetConfig, doc: &DocumentMut) -> Result<()> { + fs::write(config.path.join("pyproject.toml"), doc.to_string()).map_err(|e| { + UvupError::PathError(format!("Failed to write pyproject.toml: {e}")) + })?; + + let venv_status = Command::new("uv") + .arg("venv") + .current_dir(&config.path) .status() - .map_err(|e| { - UvupError::CommandExecutionFailed(format!("Failed to execute uv init: {e}")) - })?; + .map_err(|e| UvupError::CommandExecutionFailed(format!("Failed to execute uv venv: {e}")))?; - if !init_status.success() { - let _ = fs::remove_dir_all(project_path); + if !venv_status.success() { + let _ = fs::remove_file(config.path.join("pyproject.toml")); return Err(UvupError::CommandExecutionFailed( - "Failed to initialize project".to_string(), + "Failed to create virtual environment".to_string(), )); } - // Create virtual environment + Ok(()) +} + +/// Create project environment +fn create_project_environment( + config: &TargetConfig, + source_path: &std::path::Path, + doc: &DocumentMut, +) -> Result<()> { + fs::create_dir_all(&config.path)?; + fs::write(config.path.join("pyproject.toml"), doc.to_string()).map_err(|e| { + UvupError::PathError(format!("Failed to write pyproject.toml: {e}")) + })?; + + // Copy hello.py if exists + let source_hello = source_path.join("hello.py"); + if source_hello.exists() { + fs::copy(&source_hello, config.path.join("hello.py")) + .map_err(|e| UvupError::PathError(format!("Failed to copy hello.py: {e}")))?; + } + let venv_status = Command::new("uv") .arg("venv") - .current_dir(project_path) + .current_dir(&config.path) .status() - .map_err(|e| { - UvupError::CommandExecutionFailed(format!("Failed to execute uv venv: {e}")) - })?; + .map_err(|e| UvupError::CommandExecutionFailed(format!("Failed to execute uv venv: {e}")))?; if !venv_status.success() { - let _ = fs::remove_dir_all(project_path); + let _ = fs::remove_dir_all(&config.path); return Err(UvupError::CommandExecutionFailed( "Failed to create virtual environment".to_string(), )); @@ -202,126 +248,195 @@ fn create_project_environment(project_path: &Path, python_version: &str) -> Resu Ok(()) } -/// Create a local .venv environment using uv venv -fn create_local_environment(venv_path: &PathBuf, python_version: &str) -> Result<()> { - let status = Command::new("uv") - .arg("venv") - .arg(venv_path) - .arg("--python") - .arg(python_version) +/// Lock and sync packages using explicit uv lock + uv sync +fn sync_environment(config: &TargetConfig) -> Result<()> { + // Step 1: Explicitly lock dependencies + println!(" Resolving and locking dependencies..."); + let lock_status = Command::new("uv") + .arg("lock") + .current_dir(&config.path) .status() - .map_err(|e| UvupError::CommandExecutionFailed(format!("Failed to execute uv: {e}")))?; + .map_err(|e| UvupError::CommandExecutionFailed(format!("Failed to execute uv lock: {e}")))?; - if !status.success() { + if !lock_status.success() { + cleanup_failed_environment(config); return Err(UvupError::CommandExecutionFailed( - "Failed to create local environment".to_string(), + "Failed to resolve and lock dependencies (possible version conflicts)".to_string(), + )); + } + + // Step 2: Explicitly sync environment with locked dependencies + println!(" Installing locked packages..."); + let sync_status = Command::new("uv") + .arg("sync") + .current_dir(&config.path) + .status() + .map_err(|e| UvupError::CommandExecutionFailed(format!("Failed to execute uv sync: {e}")))?; + + if !sync_status.success() { + cleanup_failed_environment(config); + return Err(UvupError::CommandExecutionFailed( + "Failed to install locked packages (possible network or permission issues)".to_string(), )); } Ok(()) } -/// Filter packages based on exclude/include patterns -fn filter_packages( - requirements: &str, +/// Cleanup failed environment files/directories +fn cleanup_failed_environment(config: &TargetConfig) { + if config.is_local { + let _ = fs::remove_file(config.path.join("pyproject.toml")); + let _ = fs::remove_file(config.path.join("uv.lock")); + let _ = fs::remove_dir_all(config.path.join(".venv")); + } else { + let _ = fs::remove_dir_all(&config.path); + } +} + + + +/// Filter dependencies in pyproject.toml based on exclude/include patterns +fn filter_dependencies( + doc: &mut DocumentMut, exclude: Option<&[String]>, include: Option<&[String]>, -) -> String { - let lines: Vec<&str> = requirements.lines().collect(); - - let filtered: Vec<&str> = lines - .into_iter() - .filter(|line| { - let line = line.trim(); - if line.is_empty() || line.starts_with('#') { - return true; +) -> Result<()> { + // Filter main dependencies + if let Some(dependencies) = doc + .get_mut("project") + .and_then(|p| p.get_mut("dependencies")) + { + let deps_array = dependencies.as_array_mut().ok_or_else(|| { + UvupError::CommandExecutionFailed("Invalid dependencies format".to_string()) + })?; + + let filtered = filter_dependency_array(deps_array, exclude, include); + *deps_array = toml_edit::Array::from_iter(filtered); + } + + // Filter optional-dependencies (if exists) + if let Some(project) = doc.get_mut("project") { + if let Some(optional_deps) = project.get_mut("optional-dependencies") { + if let Some(optional_table) = optional_deps.as_table_mut() { + let mut empty_groups = Vec::new(); + + for (group_name, group_deps) in optional_table.iter_mut() { + if let Some(deps_array) = group_deps.as_array_mut() { + let filtered = filter_dependency_array(deps_array, exclude, include); + + if filtered.is_empty() { + println!(" Note: Optional group '{group_name}' is now empty after filtering"); + empty_groups.push(group_name.to_string()); + } else { + *deps_array = toml_edit::Array::from_iter(filtered); + } + } + } + + // Remove empty groups + for group in empty_groups { + optional_table.remove(&group); + } } + } + } + + Ok(()) +} + +/// Filter a single dependency array based on exclude/include patterns +fn filter_dependency_array( + deps_array: &toml_edit::Array, + exclude: Option<&[String]>, + include: Option<&[String]>, +) -> Vec { + let mut filtered_deps = Vec::new(); - // Extract package name (before ==, >=, <=, etc.) - let package_name = line - .split(&['=', '>', '<', '~', '!'][..]) - .next() - .unwrap_or(line) - .trim() - .to_lowercase(); + for dep in deps_array { + if let Some(dep_str) = dep.as_str() { + let package_name = extract_package_name(dep_str); - // If include list is specified, only keep packages in the list + // Apply include filter first if let Some(include_list) = include { let included = include_list .iter() - .any(|inc| package_name.contains(&inc.to_lowercase())); + .any(|inc| package_name == inc.to_lowercase()); if !included { - return false; + continue; } } - // If exclude list is specified, remove matching packages + // Apply exclude filter if let Some(exclude_list) = exclude { let excluded = exclude_list .iter() - .any(|exc| package_name.contains(&exc.to_lowercase())); + .any(|exc| package_name == exc.to_lowercase()); if excluded { println!(" Excluding: {package_name}"); - return false; + continue; } } - true - }) - .collect(); + filtered_deps.push(dep.clone()); + } + } - filtered.join("\n") + filtered_deps } -/// Sync packages to target environment using uv pip sync or install -fn sync_packages(venv_path: &Path, requirements: &str, use_sync: bool) -> Result<()> { - // Create temporary requirements file - let temp_file = tempfile::NamedTempFile::new().map_err(|e| { - UvupError::CommandExecutionFailed(format!("Failed to create temp file: {e}")) - })?; +/// Extract core package name from dependency string +/// +/// Handles various formats: +/// - "requests>=2.31.0" -> "requests" +/// - "requests[http3]>=2.0" -> "requests" +/// - "requests~=2.31.0" -> "requests" +/// - "my-package>=1.0" -> "my-package" +fn extract_package_name(dep_str: &str) -> String { + // Find the first occurrence of version specifier or bracket + let end_pos = dep_str + .find(['=', '>', '<', '~', '!', '[']) + .unwrap_or(dep_str.len()); + + dep_str[..end_pos].trim().to_lowercase() +} - fs::write(&temp_file, requirements).map_err(|e| { - UvupError::CommandExecutionFailed(format!("Failed to write requirements: {e}")) - })?; - let python_bin = if cfg!(windows) { - venv_path.join("Scripts").join("python.exe") - } else { - venv_path.join("bin").join("python") - }; +/// Get Python version from pyproject.toml +fn get_python_version_from_toml(doc: &DocumentMut) -> Result { + let version_str = doc + .get("project") + .and_then(|p| p.get("requires-python")) + .and_then(|v| v.as_str()) + .ok_or_else(|| { + UvupError::CommandExecutionFailed( + "No requires-python found in pyproject.toml".to_string(), + ) + })?; - let status = if use_sync { - // Use pip sync for exact version replication (same Python version) - Command::new("uv") - .arg("pip") - .arg("sync") - .arg("--python") - .arg(&python_bin) - .arg(temp_file.path()) - .status() - .map_err(|e| { - UvupError::CommandExecutionFailed(format!("Failed to execute uv pip sync: {e}")) - })? - } else { - // Use pip install for cross-version compatibility (different Python version) - Command::new("uv") - .arg("pip") - .arg("install") - .arg("--python") - .arg(&python_bin) - .arg("-r") - .arg(temp_file.path()) - .status() - .map_err(|e| { - UvupError::CommandExecutionFailed(format!("Failed to execute uv pip install: {e}")) - })? - }; + // Parse version string like ">=3.12" to "3.12" + let version = version_str + .trim_start_matches(|c: char| !c.is_ascii_digit()) + .split('.') + .take(2) + .collect::>() + .join("."); - if !status.success() { - return Err(UvupError::CommandExecutionFailed( - "Failed to install packages".to_string(), - )); - } + Ok(version) +} + +/// Update Python version in pyproject.toml +fn update_python_version(doc: &mut DocumentMut, version: &str) -> Result<()> { + let requires_python = doc + .get_mut("project") + .and_then(|p| p.get_mut("requires-python")) + .ok_or_else(|| { + UvupError::CommandExecutionFailed( + "No requires-python found in pyproject.toml".to_string(), + ) + })?; + + *requires_python = Item::Value(Value::from(format!(">={version}"))); Ok(()) } diff --git a/src/env/paths.rs b/src/env/paths.rs index 43f9189..74cdf6e 100644 --- a/src/env/paths.rs +++ b/src/env/paths.rs @@ -16,11 +16,6 @@ pub(crate) fn get_env_path(name: &str) -> Result { Ok(get_envs_dir()?.join(name)) } -/// Get the .venv directory path for an environment (contains the actual venv) -pub(crate) fn get_venv_path(name: &str) -> Result { - Ok(get_env_path(name)?.join(".venv")) -} - pub(crate) fn validate_env_name(name: &str) -> Result<()> { if name.is_empty() { return Err(UvupError::InvalidEnvName( diff --git a/src/main.rs b/src/main.rs index 24b81f4..f5bca3b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,6 +35,7 @@ fn run() -> Result<()> { exclude, include, local, + r#override, } => commands::copy::run( source, name, @@ -42,6 +43,7 @@ fn run() -> Result<()> { exclude.as_deref(), include.as_deref(), local, + r#override, )?, } From 8cafed9c2b197b8832a6b8a079f86b919db8b3d7 Mon Sep 17 00:00:00 2001 From: KercyDing Date: Mon, 17 Nov 2025 07:11:59 +0800 Subject: [PATCH 6/9] feat(copy): add dry-run flag to preview environment copy changes Add a `--dry-run` flag to the copy command that allows users to preview changes before applying them. --- src/cli.rs | 3 + src/commands/copy.rs | 233 ++++++++++++++++++++++++++++++++++++++++--- src/main.rs | 2 + 3 files changed, 223 insertions(+), 15 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index a22e379..32b1357 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -76,5 +76,8 @@ pub(crate) enum Commands { #[arg(long, help = "Override existing pyproject.toml (creates backup)")] r#override: bool, + + #[arg(long, help = "Preview changes without applying them")] + dry_run: bool, }, } diff --git a/src/commands/copy.rs b/src/commands/copy.rs index 1ac64d7..bff4762 100644 --- a/src/commands/copy.rs +++ b/src/commands/copy.rs @@ -10,6 +10,7 @@ use std::io::{self, Write}; use std::process::Command; use toml_edit::{DocumentMut, Item, Value}; +#[allow(clippy::too_many_arguments)] pub(crate) fn run( source: String, name: Option, @@ -18,6 +19,7 @@ pub(crate) fn run( include: Option<&[String]>, local: bool, override_existing: bool, + dry_run: bool, ) -> Result<()> { validate_env_name(&source)?; @@ -30,32 +32,56 @@ pub(crate) fn run( let target_config = determine_target_config(name, local, override_existing)?; check_target_exists(&target_config)?; - println!( - "Copying environment '{source}' to '{}'...", - target_config.name - ); - - // Process pyproject.toml - let mut doc = read_and_parse_toml(&source_path)?; + // Read and process pyproject.toml + let source_doc = read_and_parse_toml(&source_path)?; + let mut target_doc = source_doc.clone(); - if exclude.is_some() || include.is_some() { - filter_dependencies(&mut doc, exclude, include)?; + // Apply filters + let has_filters = exclude.is_some() || include.is_some(); + if has_filters { + filter_dependencies(&mut target_doc, exclude, include)?; } - let python_version = if let Some(version) = python { - println!("Note: Switching Python version may cause package compatibility issues."); - update_python_version(&mut doc, version)?; + // Get Python versions + let source_python = get_python_version_from_toml(&source_doc)?; + let target_python = if let Some(version) = python { + update_python_version(&mut target_doc, version)?; version.to_string() } else { - get_python_version_from_toml(&doc)? + source_python.clone() }; + // Dry-run mode: preview changes and exit + if dry_run { + print_dry_run_preview( + &source, + &target_config, + &source_doc, + &target_doc, + &source_python, + &target_python, + exclude, + include, + ); + return Ok(()); + } + + // Normal mode: execute copy + println!( + "Copying environment '{source}' to '{}'...", + target_config.name + ); + + if python.is_some() { + println!("Note: Switching Python version may cause package compatibility issues."); + } + // Create environment println!( - "Creating environment '{}' with Python {python_version}...", + "Creating environment '{}' with Python {target_python}...", target_config.name ); - create_environment(&target_config, &source_path, &doc)?; + create_environment(&target_config, &source_path, &target_doc)?; // Sync packages println!("Installing packages..."); @@ -440,3 +466,180 @@ fn update_python_version(doc: &mut DocumentMut, version: &str) -> Result<()> { Ok(()) } + +/// Print dry-run preview of changes +#[allow(clippy::too_many_arguments)] +fn print_dry_run_preview( + source: &str, + target_config: &TargetConfig, + source_doc: &DocumentMut, + target_doc: &DocumentMut, + source_python: &str, + target_python: &str, + exclude: Option<&[String]>, + include: Option<&[String]>, +) { + println!("-- Dry Run Mode --"); + println!(); + println!("Source: '{source}' (Python {source_python})"); + println!("Target: '{}' (Python {target_python})", target_config.name); + if target_config.is_local { + println!("Mode: Local (.venv in current directory)"); + } else { + println!("Mode: Global environment"); + } + println!(); + + // Show Python version change + if source_python != target_python { + println!("Python version change:"); + println!(" {source_python} → {target_python}"); + println!(); + } + + // Show filter configuration + if exclude.is_some() || include.is_some() { + println!("Filters applied:"); + if let Some(exc) = exclude { + println!(" Exclude: {}", exc.join(", ")); + } + if let Some(inc) = include { + println!(" Include: {}", inc.join(", ")); + } + println!(); + } + + // Compare dependencies + println!("Dependency changes:"); + compare_dependencies(source_doc, target_doc); + println!(); + + // Compare optional-dependencies + compare_optional_dependencies(source_doc, target_doc); + + println!("To apply these changes, run the same command without --dry-run"); +} + +/// Compare and show dependency changes +fn compare_dependencies(source_doc: &DocumentMut, target_doc: &DocumentMut) { + let source_deps = extract_dependencies(source_doc); + let target_deps = extract_dependencies(target_doc); + + let mut added = Vec::new(); + let mut removed = Vec::new(); + let mut kept = Vec::new(); + + for dep in &source_deps { + if target_deps.contains(dep) { + kept.push(dep); + } else { + removed.push(dep); + } + } + + for dep in &target_deps { + if !source_deps.contains(dep) { + added.push(dep); + } + } + + if removed.is_empty() && added.is_empty() { + println!(" No changes to main dependencies"); + } else { + if !removed.is_empty() { + println!(" Removed ({}):", removed.len()); + for dep in &removed { + println!(" - {dep}"); + } + } + if !added.is_empty() { + println!(" Added ({}):", added.len()); + for dep in &added { + println!(" + {dep}"); + } + } + if !kept.is_empty() && (!removed.is_empty() || !added.is_empty()) { + println!(" Kept ({}):", kept.len()); + } + } +} + +/// Compare and show optional-dependencies changes +fn compare_optional_dependencies(source_doc: &DocumentMut, target_doc: &DocumentMut) { + let source_optional = extract_optional_dependencies(source_doc); + let target_optional = extract_optional_dependencies(target_doc); + + if source_optional.is_empty() && target_optional.is_empty() { + return; + } + + println!("Optional dependencies:"); + + let mut all_groups: std::collections::HashSet = source_optional.keys().cloned().collect(); + all_groups.extend(target_optional.keys().cloned()); + + let mut groups: Vec<_> = all_groups.into_iter().collect(); + groups.sort(); + + for group in groups { + let source_deps = source_optional.get(&group); + let target_deps = target_optional.get(&group); + + match (source_deps, target_deps) { + (Some(src), Some(tgt)) if src == tgt => { + println!(" [{group}]: No changes"); + } + (Some(_), Some(tgt)) => { + println!(" [{group}]: Modified ({} packages)", tgt.len()); + } + (Some(_), None) => { + println!(" [{group}]: Removed (group is empty after filtering)"); + } + (None, Some(tgt)) => { + println!(" [{group}]: Added ({} packages)", tgt.len()); + } + (None, None) => unreachable!(), + } + } + println!(); +} + +/// Extract main dependencies from pyproject.toml +fn extract_dependencies(doc: &DocumentMut) -> Vec { + doc.get("project") + .and_then(|p| p.get("dependencies")) + .and_then(|d| d.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|v| v.as_str()) + .map(String::from) + .collect() + }) + .unwrap_or_default() +} + +/// Extract optional-dependencies from pyproject.toml +fn extract_optional_dependencies( + doc: &DocumentMut, +) -> std::collections::HashMap> { + let mut result = std::collections::HashMap::new(); + + if let Some(project) = doc.get("project") { + if let Some(optional) = project.get("optional-dependencies") { + if let Some(table) = optional.as_table() { + for (key, value) in table { + if let Some(arr) = value.as_array() { + let deps: Vec = arr + .iter() + .filter_map(|v| v.as_str()) + .map(String::from) + .collect(); + result.insert(key.to_string(), deps); + } + } + } + } + } + + result +} diff --git a/src/main.rs b/src/main.rs index f5bca3b..be15c5b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,6 +36,7 @@ fn run() -> Result<()> { include, local, r#override, + dry_run, } => commands::copy::run( source, name, @@ -44,6 +45,7 @@ fn run() -> Result<()> { include.as_deref(), local, r#override, + dry_run, )?, } From 8281d01cea1865ccd4e17bdc7b3ea80c14242694 Mon Sep 17 00:00:00 2001 From: KercyDing Date: Mon, 17 Nov 2025 07:37:21 +0800 Subject: [PATCH 7/9] refactor(commands)!: split copy into clone and new commands The copy command had multiple responsibilities and complex flags. Split into two focused commands: - clone: Creates exact 1:1 environment copy (source -> target) - new: Creates project from template with customization options (python version override, package filtering, custom path) --- src/cli.rs | 48 ++-- src/commands/clone.rs | 94 ++++++ src/commands/copy.rs | 645 ------------------------------------------ src/commands/mod.rs | 3 +- src/commands/new.rs | 465 ++++++++++++++++++++++++++++++ src/error.rs | 4 - src/main.rs | 19 +- 7 files changed, 593 insertions(+), 685 deletions(-) create mode 100644 src/commands/clone.rs delete mode 100644 src/commands/copy.rs create mode 100644 src/commands/new.rs diff --git a/src/cli.rs b/src/cli.rs index 32b1357..fbe7be0 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -32,29 +32,24 @@ pub(crate) enum Commands { name: String, }, - #[command(about = "Update uvup to the latest version")] - Update { - #[arg(short, long, help = "Only check for updates without installing")] - check: bool, - }, - - #[command( - about = "Copy an environment to a new environment", - override_usage = "uvup copy [OPTIONS] --name " - )] - Copy { + #[command(about = "Clone an environment (exact 1:1 copy)")] + Clone { #[arg(help = "Source environment name")] source: String, - #[arg( - short, - long, - help = "Target environment name (or use --local)", - conflicts_with = "local" - )] - name: Option, + #[arg(help = "Target environment name")] + target: String, + }, - #[arg(short, long, help = "Python version for target environment (optional)")] + #[command(about = "Create a new project from a template")] + New { + #[arg(help = "Project name")] + name: String, + + #[arg(long, help = "Template environment name")] + template: String, + + #[arg(short, long, help = "Python version (override template version)")] python: Option, #[arg( @@ -71,13 +66,16 @@ pub(crate) enum Commands { )] include: Option>, - #[arg(short, long, help = "Copy to .venv in current directory")] - local: bool, + #[arg(long, help = "Directory to create project in (default: current dir)")] + path: Option, - #[arg(long, help = "Override existing pyproject.toml (creates backup)")] - r#override: bool, - - #[arg(long, help = "Preview changes without applying them")] + #[arg(long, help = "Preview changes without creating")] dry_run: bool, }, + + #[command(about = "Update uvup to the latest version")] + Update { + #[arg(short, long, help = "Only check for updates without installing")] + check: bool, + }, } diff --git a/src/commands/clone.rs b/src/commands/clone.rs new file mode 100644 index 0000000..727756c --- /dev/null +++ b/src/commands/clone.rs @@ -0,0 +1,94 @@ +// Allow println! in this module as it's used for user-facing output +#![allow(clippy::print_stdout)] + +use crate::env::paths::{get_env_path, validate_env_name}; +use crate::error::{Result, UvupError}; +use crate::utils::print_success; +use std::fs; +use std::process::Command; + +/// Clone an environment (exact 1:1 copy without modifications) +pub(crate) fn run(source: String, target: String) -> Result<()> { + // Validate names + validate_env_name(&source)?; + validate_env_name(&target)?; + + // Check source exists + let source_path = get_env_path(&source)?; + if !source_path.exists() { + return Err(UvupError::EnvNotFound(source)); + } + + // Check target doesn't exist + let target_path = get_env_path(&target)?; + if target_path.exists() { + return Err(UvupError::EnvAlreadyExists(target)); + } + + println!("Cloning environment '{source}' to '{target}'..."); + + // Create target directory + fs::create_dir_all(&target_path)?; + + // Copy pyproject.toml + let source_toml = source_path.join("pyproject.toml"); + let target_toml = target_path.join("pyproject.toml"); + fs::copy(&source_toml, &target_toml) + .map_err(|e| UvupError::PathError(format!("Failed to copy pyproject.toml: {e}")))?; + + // Copy hello.py if exists + let source_hello = source_path.join("hello.py"); + if source_hello.exists() { + let target_hello = target_path.join("hello.py"); + fs::copy(&source_hello, &target_hello) + .map_err(|e| UvupError::PathError(format!("Failed to copy hello.py: {e}")))?; + } + + // Copy uv.lock if exists + let source_lock = source_path.join("uv.lock"); + if source_lock.exists() { + let target_lock = target_path.join("uv.lock"); + fs::copy(&source_lock, &target_lock) + .map_err(|e| UvupError::PathError(format!("Failed to copy uv.lock: {e}")))?; + } + + // Create venv + println!("Creating virtual environment..."); + let venv_status = Command::new("uv") + .arg("venv") + .current_dir(&target_path) + .status() + .map_err(|e| { + UvupError::CommandExecutionFailed(format!("Failed to execute uv venv: {e}")) + })?; + + if !venv_status.success() { + let _ = fs::remove_dir_all(&target_path); + return Err(UvupError::CommandExecutionFailed( + "Failed to create virtual environment".to_string(), + )); + } + + // Sync packages (use existing lock file if available) + println!("Installing packages..."); + let sync_status = Command::new("uv") + .arg("sync") + .current_dir(&target_path) + .status() + .map_err(|e| { + UvupError::CommandExecutionFailed(format!("Failed to execute uv sync: {e}")) + })?; + + if !sync_status.success() { + let _ = fs::remove_dir_all(&target_path); + return Err(UvupError::CommandExecutionFailed( + "Failed to install packages".to_string(), + )); + } + + print_success(&format!( + "Successfully cloned environment '{source}' to '{target}'" + )); + + Ok(()) +} diff --git a/src/commands/copy.rs b/src/commands/copy.rs deleted file mode 100644 index bff4762..0000000 --- a/src/commands/copy.rs +++ /dev/null @@ -1,645 +0,0 @@ -// Allow println! in this module as it's used for user-facing output -#![allow(clippy::print_stdout)] - -use crate::env::paths::{get_env_path, validate_env_name}; -use crate::error::{Result, UvupError}; -use crate::utils::print_success; -use std::env; -use std::fs; -use std::io::{self, Write}; -use std::process::Command; -use toml_edit::{DocumentMut, Item, Value}; - -#[allow(clippy::too_many_arguments)] -pub(crate) fn run( - source: String, - name: Option, - python: Option<&str>, - exclude: Option<&[String]>, - include: Option<&[String]>, - local: bool, - override_existing: bool, - dry_run: bool, -) -> Result<()> { - validate_env_name(&source)?; - - let source_path = get_env_path(&source)?; - if !source_path.exists() { - return Err(UvupError::EnvNotFound(source)); - } - - // Determine target configuration - let target_config = determine_target_config(name, local, override_existing)?; - check_target_exists(&target_config)?; - - // Read and process pyproject.toml - let source_doc = read_and_parse_toml(&source_path)?; - let mut target_doc = source_doc.clone(); - - // Apply filters - let has_filters = exclude.is_some() || include.is_some(); - if has_filters { - filter_dependencies(&mut target_doc, exclude, include)?; - } - - // Get Python versions - let source_python = get_python_version_from_toml(&source_doc)?; - let target_python = if let Some(version) = python { - update_python_version(&mut target_doc, version)?; - version.to_string() - } else { - source_python.clone() - }; - - // Dry-run mode: preview changes and exit - if dry_run { - print_dry_run_preview( - &source, - &target_config, - &source_doc, - &target_doc, - &source_python, - &target_python, - exclude, - include, - ); - return Ok(()); - } - - // Normal mode: execute copy - println!( - "Copying environment '{source}' to '{}'...", - target_config.name - ); - - if python.is_some() { - println!("Note: Switching Python version may cause package compatibility issues."); - } - - // Create environment - println!( - "Creating environment '{}' with Python {target_python}...", - target_config.name - ); - create_environment(&target_config, &source_path, &target_doc)?; - - // Sync packages - println!("Installing packages..."); - sync_environment(&target_config)?; - - print_success(&format!( - "Successfully copied environment '{source}' to '{}'", - target_config.name - )); - - Ok(()) -} - -struct TargetConfig { - name: String, - path: std::path::PathBuf, - is_local: bool, -} - -/// Determine target configuration based on user input -fn determine_target_config( - name: Option, - local: bool, - override_existing: bool, -) -> Result { - if local { - let current_dir = env::current_dir() - .map_err(|e| UvupError::PathError(format!("Failed to get current directory: {e}")))?; - - let pyproject_path = current_dir.join("pyproject.toml"); - - // Check if pyproject.toml exists - if pyproject_path.exists() { - if !override_existing { - // No --override flag: tell user to add it - return Err(UvupError::InvalidInput( - "Current directory already has pyproject.toml.\nUse --override to replace it (will create backup as pyproject.toml.backup).".to_string() - )); - } - - // With --override flag: ask for confirmation - print!( - "Replace existing pyproject.toml with environment configuration? [y/N] " - ); - io::stdout().flush().unwrap(); - - let mut input = String::new(); - io::stdin() - .read_line(&mut input) - .map_err(UvupError::IoError)?; - - let input = input.trim().to_lowercase(); - if input != "y" && input != "yes" { - println!("Cancelled."); - return Err(UvupError::InvalidInput("Cancelled by user".to_string())); - } - - // Backup existing pyproject.toml - let backup_path = current_dir.join("pyproject.toml.backup"); - fs::copy(&pyproject_path, &backup_path).map_err(|e| { - UvupError::PathError(format!("Failed to create backup: {e}")) - })?; - println!("Backed up existing pyproject.toml to pyproject.toml.backup"); - } else { - // No pyproject.toml: simple confirmation - print!("Create .venv in '{}'? [Y/n] ", current_dir.display()); - io::stdout().flush().unwrap(); - - let mut input = String::new(); - io::stdin() - .read_line(&mut input) - .map_err(UvupError::IoError)?; - - let input = input.trim().to_lowercase(); - if !input.is_empty() && input != "y" && input != "yes" { - println!("Cancelled."); - return Err(UvupError::InvalidInput("Cancelled by user".to_string())); - } - } - - Ok(TargetConfig { - name: ".venv".to_string(), - path: current_dir, - is_local: true, - }) - } else { - let target = name - .ok_or_else(|| UvupError::InvalidInput("Must provide --name or --local".to_string()))?; - validate_env_name(&target)?; - let path = get_env_path(&target)?; - Ok(TargetConfig { - name: target, - path, - is_local: false, - }) - } -} - -/// Check if target already exists -fn check_target_exists(config: &TargetConfig) -> Result<()> { - if !config.is_local && config.path.exists() { - return Err(UvupError::EnvAlreadyExists(config.name.clone())); - } - - if config.is_local && config.path.join(".venv").exists() { - return Err(UvupError::EnvAlreadyExists(".venv".to_string())); - } - - Ok(()) -} - -/// Read and parse source pyproject.toml -fn read_and_parse_toml(source_path: &std::path::Path) -> Result { - let source_toml_path = source_path.join("pyproject.toml"); - let toml_content = fs::read_to_string(&source_toml_path).map_err(|e| { - UvupError::PathError(format!("Failed to read source pyproject.toml: {e}")) - })?; - - toml_content.parse::().map_err(|e| { - UvupError::CommandExecutionFailed(format!("Failed to parse pyproject.toml: {e}")) - }) -} - -/// Create environment (local or project) -fn create_environment( - config: &TargetConfig, - source_path: &std::path::Path, - doc: &DocumentMut, -) -> Result<()> { - if config.is_local { - create_local_environment(config, doc) - } else { - create_project_environment(config, source_path, doc) - } -} - -/// Create local .venv environment -fn create_local_environment(config: &TargetConfig, doc: &DocumentMut) -> Result<()> { - fs::write(config.path.join("pyproject.toml"), doc.to_string()).map_err(|e| { - UvupError::PathError(format!("Failed to write pyproject.toml: {e}")) - })?; - - let venv_status = Command::new("uv") - .arg("venv") - .current_dir(&config.path) - .status() - .map_err(|e| UvupError::CommandExecutionFailed(format!("Failed to execute uv venv: {e}")))?; - - if !venv_status.success() { - let _ = fs::remove_file(config.path.join("pyproject.toml")); - return Err(UvupError::CommandExecutionFailed( - "Failed to create virtual environment".to_string(), - )); - } - - Ok(()) -} - -/// Create project environment -fn create_project_environment( - config: &TargetConfig, - source_path: &std::path::Path, - doc: &DocumentMut, -) -> Result<()> { - fs::create_dir_all(&config.path)?; - fs::write(config.path.join("pyproject.toml"), doc.to_string()).map_err(|e| { - UvupError::PathError(format!("Failed to write pyproject.toml: {e}")) - })?; - - // Copy hello.py if exists - let source_hello = source_path.join("hello.py"); - if source_hello.exists() { - fs::copy(&source_hello, config.path.join("hello.py")) - .map_err(|e| UvupError::PathError(format!("Failed to copy hello.py: {e}")))?; - } - - let venv_status = Command::new("uv") - .arg("venv") - .current_dir(&config.path) - .status() - .map_err(|e| UvupError::CommandExecutionFailed(format!("Failed to execute uv venv: {e}")))?; - - if !venv_status.success() { - let _ = fs::remove_dir_all(&config.path); - return Err(UvupError::CommandExecutionFailed( - "Failed to create virtual environment".to_string(), - )); - } - - Ok(()) -} - -/// Lock and sync packages using explicit uv lock + uv sync -fn sync_environment(config: &TargetConfig) -> Result<()> { - // Step 1: Explicitly lock dependencies - println!(" Resolving and locking dependencies..."); - let lock_status = Command::new("uv") - .arg("lock") - .current_dir(&config.path) - .status() - .map_err(|e| UvupError::CommandExecutionFailed(format!("Failed to execute uv lock: {e}")))?; - - if !lock_status.success() { - cleanup_failed_environment(config); - return Err(UvupError::CommandExecutionFailed( - "Failed to resolve and lock dependencies (possible version conflicts)".to_string(), - )); - } - - // Step 2: Explicitly sync environment with locked dependencies - println!(" Installing locked packages..."); - let sync_status = Command::new("uv") - .arg("sync") - .current_dir(&config.path) - .status() - .map_err(|e| UvupError::CommandExecutionFailed(format!("Failed to execute uv sync: {e}")))?; - - if !sync_status.success() { - cleanup_failed_environment(config); - return Err(UvupError::CommandExecutionFailed( - "Failed to install locked packages (possible network or permission issues)".to_string(), - )); - } - - Ok(()) -} - -/// Cleanup failed environment files/directories -fn cleanup_failed_environment(config: &TargetConfig) { - if config.is_local { - let _ = fs::remove_file(config.path.join("pyproject.toml")); - let _ = fs::remove_file(config.path.join("uv.lock")); - let _ = fs::remove_dir_all(config.path.join(".venv")); - } else { - let _ = fs::remove_dir_all(&config.path); - } -} - - - -/// Filter dependencies in pyproject.toml based on exclude/include patterns -fn filter_dependencies( - doc: &mut DocumentMut, - exclude: Option<&[String]>, - include: Option<&[String]>, -) -> Result<()> { - // Filter main dependencies - if let Some(dependencies) = doc - .get_mut("project") - .and_then(|p| p.get_mut("dependencies")) - { - let deps_array = dependencies.as_array_mut().ok_or_else(|| { - UvupError::CommandExecutionFailed("Invalid dependencies format".to_string()) - })?; - - let filtered = filter_dependency_array(deps_array, exclude, include); - *deps_array = toml_edit::Array::from_iter(filtered); - } - - // Filter optional-dependencies (if exists) - if let Some(project) = doc.get_mut("project") { - if let Some(optional_deps) = project.get_mut("optional-dependencies") { - if let Some(optional_table) = optional_deps.as_table_mut() { - let mut empty_groups = Vec::new(); - - for (group_name, group_deps) in optional_table.iter_mut() { - if let Some(deps_array) = group_deps.as_array_mut() { - let filtered = filter_dependency_array(deps_array, exclude, include); - - if filtered.is_empty() { - println!(" Note: Optional group '{group_name}' is now empty after filtering"); - empty_groups.push(group_name.to_string()); - } else { - *deps_array = toml_edit::Array::from_iter(filtered); - } - } - } - - // Remove empty groups - for group in empty_groups { - optional_table.remove(&group); - } - } - } - } - - Ok(()) -} - -/// Filter a single dependency array based on exclude/include patterns -fn filter_dependency_array( - deps_array: &toml_edit::Array, - exclude: Option<&[String]>, - include: Option<&[String]>, -) -> Vec { - let mut filtered_deps = Vec::new(); - - for dep in deps_array { - if let Some(dep_str) = dep.as_str() { - let package_name = extract_package_name(dep_str); - - // Apply include filter first - if let Some(include_list) = include { - let included = include_list - .iter() - .any(|inc| package_name == inc.to_lowercase()); - if !included { - continue; - } - } - - // Apply exclude filter - if let Some(exclude_list) = exclude { - let excluded = exclude_list - .iter() - .any(|exc| package_name == exc.to_lowercase()); - if excluded { - println!(" Excluding: {package_name}"); - continue; - } - } - - filtered_deps.push(dep.clone()); - } - } - - filtered_deps -} - -/// Extract core package name from dependency string -/// -/// Handles various formats: -/// - "requests>=2.31.0" -> "requests" -/// - "requests[http3]>=2.0" -> "requests" -/// - "requests~=2.31.0" -> "requests" -/// - "my-package>=1.0" -> "my-package" -fn extract_package_name(dep_str: &str) -> String { - // Find the first occurrence of version specifier or bracket - let end_pos = dep_str - .find(['=', '>', '<', '~', '!', '[']) - .unwrap_or(dep_str.len()); - - dep_str[..end_pos].trim().to_lowercase() -} - - -/// Get Python version from pyproject.toml -fn get_python_version_from_toml(doc: &DocumentMut) -> Result { - let version_str = doc - .get("project") - .and_then(|p| p.get("requires-python")) - .and_then(|v| v.as_str()) - .ok_or_else(|| { - UvupError::CommandExecutionFailed( - "No requires-python found in pyproject.toml".to_string(), - ) - })?; - - // Parse version string like ">=3.12" to "3.12" - let version = version_str - .trim_start_matches(|c: char| !c.is_ascii_digit()) - .split('.') - .take(2) - .collect::>() - .join("."); - - Ok(version) -} - -/// Update Python version in pyproject.toml -fn update_python_version(doc: &mut DocumentMut, version: &str) -> Result<()> { - let requires_python = doc - .get_mut("project") - .and_then(|p| p.get_mut("requires-python")) - .ok_or_else(|| { - UvupError::CommandExecutionFailed( - "No requires-python found in pyproject.toml".to_string(), - ) - })?; - - *requires_python = Item::Value(Value::from(format!(">={version}"))); - - Ok(()) -} - -/// Print dry-run preview of changes -#[allow(clippy::too_many_arguments)] -fn print_dry_run_preview( - source: &str, - target_config: &TargetConfig, - source_doc: &DocumentMut, - target_doc: &DocumentMut, - source_python: &str, - target_python: &str, - exclude: Option<&[String]>, - include: Option<&[String]>, -) { - println!("-- Dry Run Mode --"); - println!(); - println!("Source: '{source}' (Python {source_python})"); - println!("Target: '{}' (Python {target_python})", target_config.name); - if target_config.is_local { - println!("Mode: Local (.venv in current directory)"); - } else { - println!("Mode: Global environment"); - } - println!(); - - // Show Python version change - if source_python != target_python { - println!("Python version change:"); - println!(" {source_python} → {target_python}"); - println!(); - } - - // Show filter configuration - if exclude.is_some() || include.is_some() { - println!("Filters applied:"); - if let Some(exc) = exclude { - println!(" Exclude: {}", exc.join(", ")); - } - if let Some(inc) = include { - println!(" Include: {}", inc.join(", ")); - } - println!(); - } - - // Compare dependencies - println!("Dependency changes:"); - compare_dependencies(source_doc, target_doc); - println!(); - - // Compare optional-dependencies - compare_optional_dependencies(source_doc, target_doc); - - println!("To apply these changes, run the same command without --dry-run"); -} - -/// Compare and show dependency changes -fn compare_dependencies(source_doc: &DocumentMut, target_doc: &DocumentMut) { - let source_deps = extract_dependencies(source_doc); - let target_deps = extract_dependencies(target_doc); - - let mut added = Vec::new(); - let mut removed = Vec::new(); - let mut kept = Vec::new(); - - for dep in &source_deps { - if target_deps.contains(dep) { - kept.push(dep); - } else { - removed.push(dep); - } - } - - for dep in &target_deps { - if !source_deps.contains(dep) { - added.push(dep); - } - } - - if removed.is_empty() && added.is_empty() { - println!(" No changes to main dependencies"); - } else { - if !removed.is_empty() { - println!(" Removed ({}):", removed.len()); - for dep in &removed { - println!(" - {dep}"); - } - } - if !added.is_empty() { - println!(" Added ({}):", added.len()); - for dep in &added { - println!(" + {dep}"); - } - } - if !kept.is_empty() && (!removed.is_empty() || !added.is_empty()) { - println!(" Kept ({}):", kept.len()); - } - } -} - -/// Compare and show optional-dependencies changes -fn compare_optional_dependencies(source_doc: &DocumentMut, target_doc: &DocumentMut) { - let source_optional = extract_optional_dependencies(source_doc); - let target_optional = extract_optional_dependencies(target_doc); - - if source_optional.is_empty() && target_optional.is_empty() { - return; - } - - println!("Optional dependencies:"); - - let mut all_groups: std::collections::HashSet = source_optional.keys().cloned().collect(); - all_groups.extend(target_optional.keys().cloned()); - - let mut groups: Vec<_> = all_groups.into_iter().collect(); - groups.sort(); - - for group in groups { - let source_deps = source_optional.get(&group); - let target_deps = target_optional.get(&group); - - match (source_deps, target_deps) { - (Some(src), Some(tgt)) if src == tgt => { - println!(" [{group}]: No changes"); - } - (Some(_), Some(tgt)) => { - println!(" [{group}]: Modified ({} packages)", tgt.len()); - } - (Some(_), None) => { - println!(" [{group}]: Removed (group is empty after filtering)"); - } - (None, Some(tgt)) => { - println!(" [{group}]: Added ({} packages)", tgt.len()); - } - (None, None) => unreachable!(), - } - } - println!(); -} - -/// Extract main dependencies from pyproject.toml -fn extract_dependencies(doc: &DocumentMut) -> Vec { - doc.get("project") - .and_then(|p| p.get("dependencies")) - .and_then(|d| d.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_str()) - .map(String::from) - .collect() - }) - .unwrap_or_default() -} - -/// Extract optional-dependencies from pyproject.toml -fn extract_optional_dependencies( - doc: &DocumentMut, -) -> std::collections::HashMap> { - let mut result = std::collections::HashMap::new(); - - if let Some(project) = doc.get("project") { - if let Some(optional) = project.get("optional-dependencies") { - if let Some(table) = optional.as_table() { - for (key, value) in table { - if let Some(arr) = value.as_array() { - let deps: Vec = arr - .iter() - .filter_map(|v| v.as_str()) - .map(String::from) - .collect(); - result.insert(key.to_string(), deps); - } - } - } - } - } - - result -} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 03132d7..a0f46a4 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,6 +1,7 @@ -pub(crate) mod copy; +pub(crate) mod clone; pub(crate) mod create; pub(crate) mod init; pub(crate) mod list; +pub(crate) mod new; pub(crate) mod remove; pub(crate) mod update; diff --git a/src/commands/new.rs b/src/commands/new.rs new file mode 100644 index 0000000..726a8d6 --- /dev/null +++ b/src/commands/new.rs @@ -0,0 +1,465 @@ +// Allow println! in this module as it's used for user-facing output +#![allow(clippy::print_stdout)] + +use crate::env::paths::{get_env_path, validate_env_name}; +use crate::error::{Result, UvupError}; +use crate::utils::print_success; +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use toml_edit::{DocumentMut, Item, Value}; + +#[allow(clippy::too_many_arguments)] +pub(crate) fn run( + name: &str, + template: String, + python: Option<&str>, + exclude: Option<&[String]>, + include: Option<&[String]>, + path: Option<&str>, + dry_run: bool, +) -> Result<()> { + // Validate template name + validate_env_name(&template)?; + + // Check template exists + let template_path = get_env_path(&template)?; + if !template_path.exists() { + return Err(UvupError::EnvNotFound(template)); + } + + // Determine project path + let project_path = if let Some(p) = path { + PathBuf::from(p).join(name) + } else { + env::current_dir() + .map_err(|e| UvupError::PathError(format!("Failed to get current directory: {e}")))? + .join(name) + }; + + // Check project doesn't exist + if project_path.exists() { + return Err(UvupError::PathError(format!( + "Directory '{}' already exists", + project_path.display() + ))); + } + + // Read and process template pyproject.toml + let template_doc = read_and_parse_toml(&template_path)?; + let mut project_doc = template_doc.clone(); + + // Apply filters + if exclude.is_some() || include.is_some() { + filter_dependencies(&mut project_doc, exclude, include)?; + } + + // Get Python versions + let template_python = get_python_version_from_toml(&template_doc)?; + let project_python = if let Some(version) = python { + update_python_version(&mut project_doc, version)?; + version.to_string() + } else { + template_python.clone() + }; + + // Update project name in pyproject.toml + if let Some(project_table) = project_doc.get_mut("project") { + if let Some(name_item) = project_table.get_mut("name") { + *name_item = Item::Value(Value::from(name)); + } + } + + // Dry-run mode + if dry_run { + print_dry_run_preview( + &template, + name, + &project_path, + &template_doc, + &project_doc, + &template_python, + &project_python, + exclude, + include, + ); + return Ok(()); + } + + // Create project + println!("Creating project '{name}' from template '{template}'..."); + + fs::create_dir_all(&project_path)?; + + // Write pyproject.toml + fs::write(project_path.join("pyproject.toml"), project_doc.to_string()) + .map_err(|e| UvupError::PathError(format!("Failed to write pyproject.toml: {e}")))?; + + // Create venv + println!("Creating virtual environment with Python {project_python}..."); + let venv_status = Command::new("uv") + .arg("venv") + .current_dir(&project_path) + .status() + .map_err(|e| { + UvupError::CommandExecutionFailed(format!("Failed to execute uv venv: {e}")) + })?; + + if !venv_status.success() { + let _ = fs::remove_dir_all(&project_path); + return Err(UvupError::CommandExecutionFailed( + "Failed to create virtual environment".to_string(), + )); + } + + // Lock and sync + println!("Installing packages..."); + sync_environment(&project_path)?; + + print_success(&format!( + "Successfully created project '{name}' from template '{template}'" + )); + println!("Project location: {}", project_path.display()); + + Ok(()) +} + +/// Read and parse pyproject.toml +fn read_and_parse_toml(path: &Path) -> Result { + let toml_path = path.join("pyproject.toml"); + let toml_content = fs::read_to_string(&toml_path) + .map_err(|e| UvupError::PathError(format!("Failed to read pyproject.toml: {e}")))?; + + toml_content.parse::().map_err(|e| { + UvupError::CommandExecutionFailed(format!("Failed to parse pyproject.toml: {e}")) + }) +} + +/// Lock and sync packages +fn sync_environment(project_path: &Path) -> Result<()> { + println!(" Resolving and locking dependencies..."); + let lock_status = Command::new("uv") + .arg("lock") + .current_dir(project_path) + .status() + .map_err(|e| { + UvupError::CommandExecutionFailed(format!("Failed to execute uv lock: {e}")) + })?; + + if !lock_status.success() { + let _ = fs::remove_dir_all(project_path); + return Err(UvupError::CommandExecutionFailed( + "Failed to resolve and lock dependencies (possible version conflicts)".to_string(), + )); + } + + println!(" Installing locked packages..."); + let sync_status = Command::new("uv") + .arg("sync") + .current_dir(project_path) + .status() + .map_err(|e| { + UvupError::CommandExecutionFailed(format!("Failed to execute uv sync: {e}")) + })?; + + if !sync_status.success() { + let _ = fs::remove_dir_all(project_path); + return Err(UvupError::CommandExecutionFailed( + "Failed to install locked packages (possible network or permission issues)".to_string(), + )); + } + + Ok(()) +} + +/// Filter dependencies +fn filter_dependencies( + doc: &mut DocumentMut, + exclude: Option<&[String]>, + include: Option<&[String]>, +) -> Result<()> { + // Filter main dependencies + if let Some(dependencies) = doc + .get_mut("project") + .and_then(|p| p.get_mut("dependencies")) + { + let deps_array = dependencies.as_array_mut().ok_or_else(|| { + UvupError::CommandExecutionFailed("Invalid dependencies format".to_string()) + })?; + + let filtered = filter_dependency_array(deps_array, exclude, include); + *deps_array = toml_edit::Array::from_iter(filtered); + } + + // Filter optional-dependencies + if let Some(project) = doc.get_mut("project") { + if let Some(optional_deps) = project.get_mut("optional-dependencies") { + if let Some(optional_table) = optional_deps.as_table_mut() { + let mut empty_groups = Vec::new(); + + for (group_name, group_deps) in optional_table.iter_mut() { + if let Some(deps_array) = group_deps.as_array_mut() { + let filtered = filter_dependency_array(deps_array, exclude, include); + + if filtered.is_empty() { + println!( + " Note: Optional group '{group_name}' is now empty after filtering" + ); + empty_groups.push(group_name.to_string()); + } else { + *deps_array = toml_edit::Array::from_iter(filtered); + } + } + } + + for group in empty_groups { + optional_table.remove(&group); + } + } + } + } + + Ok(()) +} + +/// Filter a single dependency array +fn filter_dependency_array( + deps_array: &toml_edit::Array, + exclude: Option<&[String]>, + include: Option<&[String]>, +) -> Vec { + let mut filtered_deps = Vec::new(); + + for dep in deps_array { + if let Some(dep_str) = dep.as_str() { + let package_name = extract_package_name(dep_str); + + if let Some(include_list) = include { + let included = include_list + .iter() + .any(|inc| package_name == inc.to_lowercase()); + if !included { + continue; + } + } + + if let Some(exclude_list) = exclude { + let excluded = exclude_list + .iter() + .any(|exc| package_name == exc.to_lowercase()); + if excluded { + println!(" Excluding: {package_name}"); + continue; + } + } + + filtered_deps.push(dep.clone()); + } + } + + filtered_deps +} + +/// Extract package name from dependency string +fn extract_package_name(dep_str: &str) -> String { + let end_pos = dep_str + .find(['=', '>', '<', '~', '!', '[']) + .unwrap_or(dep_str.len()); + dep_str[..end_pos].trim().to_lowercase() +} + +/// Get Python version from pyproject.toml +fn get_python_version_from_toml(doc: &DocumentMut) -> Result { + let version_str = doc + .get("project") + .and_then(|p| p.get("requires-python")) + .and_then(|v| v.as_str()) + .ok_or_else(|| { + UvupError::CommandExecutionFailed( + "No requires-python found in pyproject.toml".to_string(), + ) + })?; + + let version = version_str + .trim_start_matches(|c: char| !c.is_ascii_digit()) + .split('.') + .take(2) + .collect::>() + .join("."); + + Ok(version) +} + +/// Update Python version in pyproject.toml +fn update_python_version(doc: &mut DocumentMut, version: &str) -> Result<()> { + let requires_python = doc + .get_mut("project") + .and_then(|p| p.get_mut("requires-python")) + .ok_or_else(|| { + UvupError::CommandExecutionFailed( + "No requires-python found in pyproject.toml".to_string(), + ) + })?; + + *requires_python = Item::Value(Value::from(format!(">={version}"))); + Ok(()) +} + +/// Print dry-run preview +#[allow(clippy::too_many_arguments)] +fn print_dry_run_preview( + template: &str, + name: &str, + project_path: &Path, + template_doc: &DocumentMut, + project_doc: &DocumentMut, + template_python: &str, + project_python: &str, + exclude: Option<&[String]>, + include: Option<&[String]>, +) { + println!("-- Dry Run Mode --"); + println!(); + println!("Template: '{template}' (Python {template_python})"); + println!("Project: '{name}' (Python {project_python})"); + println!("Location: {}", project_path.display()); + println!(); + + if template_python != project_python { + println!("Python version change:"); + println!(" {template_python} → {project_python}"); + println!(); + } + + if exclude.is_some() || include.is_some() { + println!("Filters applied:"); + if let Some(exc) = exclude { + println!(" Exclude: {}", exc.join(", ")); + } + if let Some(inc) = include { + println!(" Include: {}", inc.join(", ")); + } + println!(); + } + + println!("Dependency changes:"); + compare_dependencies(template_doc, project_doc); + println!(); + + compare_optional_dependencies(template_doc, project_doc); + + println!("To create this project, run the same command without --dry-run"); +} + +/// Compare dependencies +fn compare_dependencies(template_doc: &DocumentMut, project_doc: &DocumentMut) { + let template_deps = extract_dependencies(template_doc); + let project_deps = extract_dependencies(project_doc); + + let mut removed = Vec::new(); + let mut kept = Vec::new(); + + for dep in &template_deps { + if project_deps.contains(dep) { + kept.push(dep); + } else { + removed.push(dep); + } + } + + if removed.is_empty() { + println!(" No changes to main dependencies"); + } else { + if !removed.is_empty() { + println!(" Removed ({}):", removed.len()); + for dep in &removed { + println!(" - {dep}"); + } + } + if !kept.is_empty() { + println!(" Kept ({}):", kept.len()); + } + } +} + +/// Compare optional-dependencies +fn compare_optional_dependencies(template_doc: &DocumentMut, project_doc: &DocumentMut) { + let template_optional = extract_optional_dependencies(template_doc); + let project_optional = extract_optional_dependencies(project_doc); + + if template_optional.is_empty() && project_optional.is_empty() { + return; + } + + println!("Optional dependencies:"); + + let mut all_groups: std::collections::HashSet = + template_optional.keys().cloned().collect(); + all_groups.extend(project_optional.keys().cloned()); + + let mut groups: Vec<_> = all_groups.into_iter().collect(); + groups.sort(); + + for group in groups { + let template_deps = template_optional.get(&group); + let project_deps = project_optional.get(&group); + + match (template_deps, project_deps) { + (Some(src), Some(tgt)) if src == tgt => { + println!(" [{group}]: No changes"); + } + (Some(_), Some(tgt)) => { + println!(" [{group}]: Modified ({} packages)", tgt.len()); + } + (Some(_), None) => { + println!(" [{group}]: Removed (group is empty after filtering)"); + } + (None, Some(tgt)) => { + println!(" [{group}]: Added ({} packages)", tgt.len()); + } + (None, None) => unreachable!(), + } + } + println!(); +} + +/// Extract dependencies +fn extract_dependencies(doc: &DocumentMut) -> Vec { + doc.get("project") + .and_then(|p| p.get("dependencies")) + .and_then(|d| d.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|v| v.as_str()) + .map(String::from) + .collect() + }) + .unwrap_or_default() +} + +/// Extract optional-dependencies +fn extract_optional_dependencies( + doc: &DocumentMut, +) -> std::collections::HashMap> { + let mut result = std::collections::HashMap::new(); + + if let Some(project) = doc.get("project") { + if let Some(optional) = project.get("optional-dependencies") { + if let Some(table) = optional.as_table() { + for (key, value) in table { + if let Some(arr) = value.as_array() { + let deps: Vec = arr + .iter() + .filter_map(|v| v.as_str()) + .map(String::from) + .collect(); + result.insert(key.to_string(), deps); + } + } + } + } + } + + result +} diff --git a/src/error.rs b/src/error.rs index 7442e0d..984c5ae 100644 --- a/src/error.rs +++ b/src/error.rs @@ -7,7 +7,6 @@ pub(crate) enum UvupError { EnvAlreadyExists(String), EnvNotFound(String), InvalidEnvName(String), - InvalidInput(String), ShellDetectionFailed, IoError(io::Error), PathError(String), @@ -39,9 +38,6 @@ impl fmt::Display for UvupError { "Environment names must contain only alphanumeric characters, hyphens, and underscores" ) } - UvupError::InvalidInput(msg) => { - write!(f, "Error: {msg}") - } UvupError::ShellDetectionFailed => { writeln!(f, "Error: Could not detect your shell")?; write!(f, "Supported shells: bash, zsh, fish, powershell") diff --git a/src/main.rs b/src/main.rs index be15c5b..c2d4de7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,26 +27,25 @@ fn run() -> Result<()> { } Commands::List => commands::list::run()?, Commands::Remove { name } => commands::remove::run(name)?, - Commands::Update { check } => commands::update::run(check)?, - Commands::Copy { - source, + Commands::Clone { source, target } => commands::clone::run(source, target)?, + Commands::New { name, + template, python, exclude, include, - local, - r#override, + path, dry_run, - } => commands::copy::run( - source, - name, + } => commands::new::run( + &name, + template, python.as_deref(), exclude.as_deref(), include.as_deref(), - local, - r#override, + path.as_deref(), dry_run, )?, + Commands::Update { check } => commands::update::run(check)?, } Ok(()) From 37d4a3bb1d82fe00340f7ae1c3583ade6d691426 Mon Sep 17 00:00:00 2001 From: KercyDing Date: Mon, 17 Nov 2025 07:44:41 +0800 Subject: [PATCH 8/9] feat(commands): add sync command to sync project with template Introduces a new 'sync' command that allows syncing the current project with a template environment. The command supports: - Template environment selection - Python version override - Package exclusion/inclusion filters (comma-separated) - Dry-run mode to preview changes --- src/cli.rs | 26 ++ src/commands/mod.rs | 1 + src/commands/sync.rs | 468 +++++++++++++++++++++++ src/main.rs | 13 + testproject/pyproject.toml | 17 + testproject/uv.lock | 755 +++++++++++++++++++++++++++++++++++++ 6 files changed, 1280 insertions(+) create mode 100644 src/commands/sync.rs create mode 100644 testproject/pyproject.toml create mode 100644 testproject/uv.lock diff --git a/src/cli.rs b/src/cli.rs index fbe7be0..25a7762 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -73,6 +73,32 @@ pub(crate) enum Commands { dry_run: bool, }, + #[command(about = "Sync current project with a template")] + Sync { + #[arg(long, help = "Template environment name")] + template: String, + + #[arg(short, long, help = "Python version (override current version)")] + python: Option, + + #[arg( + long, + value_delimiter = ',', + help = "Exclude packages (comma-separated)" + )] + exclude: Option>, + + #[arg( + long, + value_delimiter = ',', + help = "Include only these packages (comma-separated)" + )] + include: Option>, + + #[arg(long, help = "Preview changes without syncing")] + dry_run: bool, + }, + #[command(about = "Update uvup to the latest version")] Update { #[arg(short, long, help = "Only check for updates without installing")] diff --git a/src/commands/mod.rs b/src/commands/mod.rs index a0f46a4..ba7b32a 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -4,4 +4,5 @@ pub(crate) mod init; pub(crate) mod list; pub(crate) mod new; pub(crate) mod remove; +pub(crate) mod sync; pub(crate) mod update; diff --git a/src/commands/sync.rs b/src/commands/sync.rs new file mode 100644 index 0000000..b7cbbc5 --- /dev/null +++ b/src/commands/sync.rs @@ -0,0 +1,468 @@ +// Allow println! in this module as it's used for user-facing output +#![allow(clippy::print_stdout)] + +use crate::env::paths::get_env_path; +use crate::error::{Result, UvupError}; +use crate::utils::print_success; +use std::env; +use std::fs; +use std::path::Path; +use std::process::Command; +use toml_edit::{DocumentMut, Item, Value}; + +#[allow(clippy::too_many_arguments)] +pub(crate) fn run( + template: String, + python: Option<&str>, + exclude: Option<&[String]>, + include: Option<&[String]>, + dry_run: bool, +) -> Result<()> { + // Get current directory + let current_dir = env::current_dir() + .map_err(|e| UvupError::PathError(format!("Failed to get current directory: {e}")))?; + + // Check pyproject.toml exists in current directory + let current_toml_path = current_dir.join("pyproject.toml"); + if !current_toml_path.exists() { + return Err(UvupError::PathError( + "No pyproject.toml found in current directory".to_string(), + )); + } + + // Check template exists + let template_path = get_env_path(&template)?; + if !template_path.exists() { + return Err(UvupError::EnvNotFound(template)); + } + + // Read current and template pyproject.toml + let current_doc = read_and_parse_toml(¤t_toml_path)?; + let template_doc = read_and_parse_toml(&template_path.join("pyproject.toml"))?; + + // Process template + let mut synced_doc = current_doc.clone(); + + // Sync dependencies from template + sync_dependencies(&mut synced_doc, &template_doc, exclude, include); + + // Get Python versions + let current_python = get_python_version_from_toml(¤t_doc)?; + let template_python = get_python_version_from_toml(&template_doc)?; + let synced_python = if let Some(version) = python { + update_python_version(&mut synced_doc, version)?; + version.to_string() + } else { + current_python.clone() + }; + + // Dry-run mode + if dry_run { + print_dry_run_preview( + &template, + ¤t_dir, + ¤t_doc, + &synced_doc, + ¤t_python, + &template_python, + &synced_python, + exclude, + include, + ); + return Ok(()); + } + + // Sync project + println!("Syncing project with template '{template}'..."); + + // Backup current pyproject.toml + let backup_path = current_dir.join("pyproject.toml.backup"); + fs::copy(¤t_toml_path, &backup_path) + .map_err(|e| UvupError::PathError(format!("Failed to backup pyproject.toml: {e}")))?; + + // Write updated pyproject.toml + fs::write(¤t_toml_path, synced_doc.to_string()) + .map_err(|e| UvupError::PathError(format!("Failed to write pyproject.toml: {e}")))?; + + // Lock and sync + println!("Installing packages..."); + if let Err(e) = sync_environment(¤t_dir) { + // Restore backup on error + let _ = fs::copy(&backup_path, ¤t_toml_path); + let _ = fs::remove_file(&backup_path); + return Err(e); + } + + // Remove backup on success + let _ = fs::remove_file(&backup_path); + + print_success(&format!( + "Successfully synced project with template '{template}'" + )); + + Ok(()) +} + +/// Read and parse pyproject.toml +fn read_and_parse_toml(path: &Path) -> Result { + let toml_content = fs::read_to_string(path) + .map_err(|e| UvupError::PathError(format!("Failed to read pyproject.toml: {e}")))?; + + toml_content.parse::().map_err(|e| { + UvupError::CommandExecutionFailed(format!("Failed to parse pyproject.toml: {e}")) + }) +} + +/// Lock and sync packages +fn sync_environment(project_path: &Path) -> Result<()> { + println!(" Resolving and locking dependencies..."); + let lock_status = Command::new("uv") + .arg("lock") + .current_dir(project_path) + .status() + .map_err(|e| { + UvupError::CommandExecutionFailed(format!("Failed to execute uv lock: {e}")) + })?; + + if !lock_status.success() { + return Err(UvupError::CommandExecutionFailed( + "Failed to resolve and lock dependencies (possible version conflicts)".to_string(), + )); + } + + println!(" Installing locked packages..."); + let sync_status = Command::new("uv") + .arg("sync") + .current_dir(project_path) + .status() + .map_err(|e| { + UvupError::CommandExecutionFailed(format!("Failed to execute uv sync: {e}")) + })?; + + if !sync_status.success() { + return Err(UvupError::CommandExecutionFailed( + "Failed to install locked packages (possible network or permission issues)".to_string(), + )); + } + + Ok(()) +} + +/// Sync dependencies from template to current project +fn sync_dependencies( + target_doc: &mut DocumentMut, + template_doc: &DocumentMut, + exclude: Option<&[String]>, + include: Option<&[String]>, +) { + // Get template dependencies + if let Some(template_deps) = template_doc + .get("project") + .and_then(|p| p.get("dependencies")) + .and_then(|d| d.as_array()) + { + let filtered = filter_dependency_array(template_deps, exclude, include); + + // Update target dependencies + if let Some(target_project) = target_doc.get_mut("project") { + target_project["dependencies"] = + Item::Value(Value::Array(toml_edit::Array::from_iter(filtered))); + } + } + + // Sync optional-dependencies + if let Some(template_optional) = template_doc + .get("project") + .and_then(|p| p.get("optional-dependencies")) + .and_then(|o| o.as_table()) + { + let mut synced_optional = toml_edit::Table::new(); + + for (group_name, group_deps) in template_optional { + if let Some(deps_array) = group_deps.as_array() { + let filtered = filter_dependency_array(deps_array, exclude, include); + + if filtered.is_empty() { + println!( + " Note: Optional group '{group_name}' is empty after filtering, skipping" + ); + } else { + synced_optional.insert( + group_name, + Item::Value(Value::Array(toml_edit::Array::from_iter(filtered))), + ); + } + } + } + + // Update target optional-dependencies + if let Some(target_project) = target_doc.get_mut("project") { + if synced_optional.is_empty() { + target_project + .as_table_mut() + .map(|t| t.remove("optional-dependencies")); + } else { + target_project["optional-dependencies"] = Item::Table(synced_optional); + } + } + } +} + +/// Filter a single dependency array +fn filter_dependency_array( + deps_array: &toml_edit::Array, + exclude: Option<&[String]>, + include: Option<&[String]>, +) -> Vec { + let mut filtered_deps = Vec::new(); + + for dep in deps_array { + if let Some(dep_str) = dep.as_str() { + let package_name = extract_package_name(dep_str); + + if let Some(include_list) = include { + let included = include_list + .iter() + .any(|inc| package_name == inc.to_lowercase()); + if !included { + continue; + } + } + + if let Some(exclude_list) = exclude { + let excluded = exclude_list + .iter() + .any(|exc| package_name == exc.to_lowercase()); + if excluded { + println!(" Excluding: {package_name}"); + continue; + } + } + + filtered_deps.push(dep.clone()); + } + } + + filtered_deps +} + +/// Extract package name from dependency string +fn extract_package_name(dep_str: &str) -> String { + let end_pos = dep_str + .find(['=', '>', '<', '~', '!', '[']) + .unwrap_or(dep_str.len()); + dep_str[..end_pos].trim().to_lowercase() +} + +/// Get Python version from pyproject.toml +fn get_python_version_from_toml(doc: &DocumentMut) -> Result { + let version_str = doc + .get("project") + .and_then(|p| p.get("requires-python")) + .and_then(|v| v.as_str()) + .ok_or_else(|| { + UvupError::CommandExecutionFailed( + "No requires-python found in pyproject.toml".to_string(), + ) + })?; + + let version = version_str + .trim_start_matches(|c: char| !c.is_ascii_digit()) + .split('.') + .take(2) + .collect::>() + .join("."); + + Ok(version) +} + +/// Update Python version in pyproject.toml +fn update_python_version(doc: &mut DocumentMut, version: &str) -> Result<()> { + let requires_python = doc + .get_mut("project") + .and_then(|p| p.get_mut("requires-python")) + .ok_or_else(|| { + UvupError::CommandExecutionFailed( + "No requires-python found in pyproject.toml".to_string(), + ) + })?; + + *requires_python = Item::Value(Value::from(format!(">={version}"))); + Ok(()) +} + +/// Print dry-run preview +#[allow(clippy::too_many_arguments)] +fn print_dry_run_preview( + template: &str, + current_dir: &Path, + current_doc: &DocumentMut, + synced_doc: &DocumentMut, + current_python: &str, + template_python: &str, + synced_python: &str, + exclude: Option<&[String]>, + include: Option<&[String]>, +) { + println!("-- Dry Run Mode --"); + println!(); + println!("Template: '{template}' (Python {template_python})"); + println!( + "Current: {} (Python {current_python})", + current_dir.display() + ); + if current_python != synced_python { + println!("Synced: Python {synced_python}"); + } + println!(); + + if current_python != synced_python { + println!("Python version change:"); + println!(" {current_python} → {synced_python}"); + println!(); + } + + if exclude.is_some() || include.is_some() { + println!("Filters applied:"); + if let Some(exc) = exclude { + println!(" Exclude: {}", exc.join(", ")); + } + if let Some(inc) = include { + println!(" Include: {}", inc.join(", ")); + } + println!(); + } + + println!("Dependency changes:"); + compare_dependencies(current_doc, synced_doc); + println!(); + + compare_optional_dependencies(current_doc, synced_doc); + + println!("To sync this project, run the same command without --dry-run"); +} + +/// Compare dependencies +fn compare_dependencies(current_doc: &DocumentMut, synced_doc: &DocumentMut) { + let current_deps = extract_dependencies(current_doc); + let synced_deps = extract_dependencies(synced_doc); + + let mut added = Vec::new(); + let mut removed = Vec::new(); + let mut kept = Vec::new(); + + for dep in &synced_deps { + if current_deps.contains(dep) { + kept.push(dep); + } else { + added.push(dep); + } + } + + for dep in ¤t_deps { + if !synced_deps.contains(dep) { + removed.push(dep); + } + } + + if added.is_empty() && removed.is_empty() { + println!(" No changes to main dependencies"); + } else { + if !added.is_empty() { + println!(" Added ({}):", added.len()); + for dep in &added { + println!(" + {dep}"); + } + } + if !removed.is_empty() { + println!(" Removed ({}):", removed.len()); + for dep in &removed { + println!(" - {dep}"); + } + } + if !kept.is_empty() { + println!(" Kept ({}):", kept.len()); + } + } +} + +/// Compare optional-dependencies +fn compare_optional_dependencies(current_doc: &DocumentMut, synced_doc: &DocumentMut) { + let current_optional = extract_optional_dependencies(current_doc); + let synced_optional = extract_optional_dependencies(synced_doc); + + if current_optional.is_empty() && synced_optional.is_empty() { + return; + } + + println!("Optional dependencies:"); + + let mut all_groups: std::collections::HashSet = + current_optional.keys().cloned().collect(); + all_groups.extend(synced_optional.keys().cloned()); + + let mut groups: Vec<_> = all_groups.into_iter().collect(); + groups.sort(); + + for group in groups { + let current_deps = current_optional.get(&group); + let synced_deps = synced_optional.get(&group); + + match (current_deps, synced_deps) { + (Some(cur), Some(syn)) if cur == syn => { + println!(" [{group}]: No changes"); + } + (Some(_), Some(syn)) => { + println!(" [{group}]: Modified ({} packages)", syn.len()); + } + (Some(_), None) => { + println!(" [{group}]: Removed"); + } + (None, Some(syn)) => { + println!(" [{group}]: Added ({} packages)", syn.len()); + } + (None, None) => unreachable!(), + } + } + println!(); +} + +/// Extract dependencies +fn extract_dependencies(doc: &DocumentMut) -> Vec { + doc.get("project") + .and_then(|p| p.get("dependencies")) + .and_then(|d| d.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|v| v.as_str()) + .map(String::from) + .collect() + }) + .unwrap_or_default() +} + +/// Extract optional-dependencies +fn extract_optional_dependencies( + doc: &DocumentMut, +) -> std::collections::HashMap> { + let mut result = std::collections::HashMap::new(); + + if let Some(project) = doc.get("project") { + if let Some(optional) = project.get("optional-dependencies") { + if let Some(table) = optional.as_table() { + for (key, value) in table { + if let Some(arr) = value.as_array() { + let deps: Vec = arr + .iter() + .filter_map(|v| v.as_str()) + .map(String::from) + .collect(); + result.insert(key.to_string(), deps); + } + } + } + } + } + + result +} diff --git a/src/main.rs b/src/main.rs index c2d4de7..862fc79 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,6 +45,19 @@ fn run() -> Result<()> { path.as_deref(), dry_run, )?, + Commands::Sync { + template, + python, + exclude, + include, + dry_run, + } => commands::sync::run( + template, + python.as_deref(), + exclude.as_deref(), + include.as_deref(), + dry_run, + )?, Commands::Update { check } => commands::update::run(check)?, } diff --git a/testproject/pyproject.toml b/testproject/pyproject.toml new file mode 100644 index 0000000..ba622f3 --- /dev/null +++ b/testproject/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "testproject" +version = "0.1.0" +description = "Add your description here" +requires-python = ">=3.12" +dependencies = [ + "numpy>=2.3.5", + "pandas>=2.3.3", + "requests>=2.32.5"] + +[project.optional-dependencies] +dev = [ + "pytest>=7.0.0", + "black>=23.0.0"] +viz = [ + "matplotlib>=3.5.0", + "seaborn>=0.12.0"] diff --git a/testproject/uv.lock b/testproject/uv.lock new file mode 100644 index 0000000..bc047f5 --- /dev/null +++ b/testproject/uv.lock @@ -0,0 +1,755 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "black" +version = "25.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/ad/33adf4708633d047950ff2dfdea2e215d84ac50ef95aff14a614e4b6e9b2/black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08", size = 655669, upload-time = "2025-11-10T01:53:50.558Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/12/5c35e600b515f35ffd737da7febdb2ab66bb8c24d88560d5e3ef3d28c3fd/black-25.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:80e7486ad3535636657aa180ad32a7d67d7c273a80e12f1b4bfa0823d54e8fac", size = 1772831, upload-time = "2025-11-10T02:03:47Z" }, + { url = "https://files.pythonhosted.org/packages/1a/75/b3896bec5a2bb9ed2f989a970ea40e7062f8936f95425879bbe162746fe5/black-25.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cced12b747c4c76bc09b4db057c319d8545307266f41aaee665540bc0e04e96", size = 1608520, upload-time = "2025-11-10T01:58:46.895Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b5/2bfc18330eddbcfb5aab8d2d720663cd410f51b2ed01375f5be3751595b0/black-25.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb2d54a39e0ef021d6c5eef442e10fd71fcb491be6413d083a320ee768329dd", size = 1682719, upload-time = "2025-11-10T01:56:55.24Z" }, + { url = "https://files.pythonhosted.org/packages/96/fb/f7dc2793a22cdf74a72114b5ed77fe3349a2e09ef34565857a2f917abdf2/black-25.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae263af2f496940438e5be1a0c1020e13b09154f3af4df0835ea7f9fe7bfa409", size = 1362684, upload-time = "2025-11-10T01:57:07.639Z" }, + { url = "https://files.pythonhosted.org/packages/ad/47/3378d6a2ddefe18553d1115e36aea98f4a90de53b6a3017ed861ba1bd3bc/black-25.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a1d40348b6621cc20d3d7530a5b8d67e9714906dfd7346338249ad9c6cedf2b", size = 1772446, upload-time = "2025-11-10T02:02:16.181Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4b/0f00bfb3d1f7e05e25bfc7c363f54dc523bb6ba502f98f4ad3acf01ab2e4/black-25.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51c65d7d60bb25429ea2bf0731c32b2a2442eb4bd3b2afcb47830f0b13e58bfd", size = 1607983, upload-time = "2025-11-10T02:02:52.502Z" }, + { url = "https://files.pythonhosted.org/packages/99/fe/49b0768f8c9ae57eb74cc10a1f87b4c70453551d8ad498959721cc345cb7/black-25.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:936c4dd07669269f40b497440159a221ee435e3fddcf668e0c05244a9be71993", size = 1682481, upload-time = "2025-11-10T01:57:12.35Z" }, + { url = "https://files.pythonhosted.org/packages/55/17/7e10ff1267bfa950cc16f0a411d457cdff79678fbb77a6c73b73a5317904/black-25.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:f42c0ea7f59994490f4dccd64e6b2dd49ac57c7c84f38b8faab50f8759db245c", size = 1363869, upload-time = "2025-11-10T01:58:24.608Z" }, + { url = "https://files.pythonhosted.org/packages/67/c0/cc865ce594d09e4cd4dfca5e11994ebb51604328489f3ca3ae7bb38a7db5/black-25.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:35690a383f22dd3e468c85dc4b915217f87667ad9cce781d7b42678ce63c4170", size = 1771358, upload-time = "2025-11-10T02:03:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/37/77/4297114d9e2fd2fc8ab0ab87192643cd49409eb059e2940391e7d2340e57/black-25.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dae49ef7369c6caa1a1833fd5efb7c3024bb7e4499bf64833f65ad27791b1545", size = 1612902, upload-time = "2025-11-10T01:59:33.382Z" }, + { url = "https://files.pythonhosted.org/packages/de/63/d45ef97ada84111e330b2b2d45e1dd163e90bd116f00ac55927fb6bf8adb/black-25.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bd4a22a0b37401c8e492e994bce79e614f91b14d9ea911f44f36e262195fdda", size = 1680571, upload-time = "2025-11-10T01:57:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4b/5604710d61cdff613584028b4cb4607e56e148801ed9b38ee7970799dab6/black-25.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:aa211411e94fdf86519996b7f5f05e71ba34835d8f0c0f03c00a26271da02664", size = 1382599, upload-time = "2025-11-10T01:57:57.427Z" }, + { url = "https://files.pythonhosted.org/packages/00/5d/aed32636ed30a6e7f9efd6ad14e2a0b0d687ae7c8c7ec4e4a557174b895c/black-25.11.0-py3-none-any.whl", hash = "sha256:e3f562da087791e96cefcd9dda058380a442ab322a02e222add53736451f604b", size = 204918, upload-time = "2025-11-10T01:53:48.917Z" }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "fonttools" +version = "4.60.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/42/97a13e47a1e51a5a7142475bbcf5107fe3a68fc34aef331c897d5fb98ad0/fonttools-4.60.1.tar.gz", hash = "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9", size = 3559823, upload-time = "2025-09-29T21:13:27.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/f7/a10b101b7a6f8836a5adb47f2791f2075d044a6ca123f35985c42edc82d8/fonttools-4.60.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc", size = 2832953, upload-time = "2025-09-29T21:11:39.616Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/7bd094b59c926acf2304d2151354ddbeb74b94812f3dc943c231db09cb41/fonttools-4.60.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877", size = 2352706, upload-time = "2025-09-29T21:11:41.826Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ca/4bb48a26ed95a1e7eba175535fe5805887682140ee0a0d10a88e1de84208/fonttools-4.60.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c", size = 4923716, upload-time = "2025-09-29T21:11:43.893Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9f/2cb82999f686c1d1ddf06f6ae1a9117a880adbec113611cc9d22b2fdd465/fonttools-4.60.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401", size = 4968175, upload-time = "2025-09-29T21:11:46.439Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/be569699e37d166b78e6218f2cde8c550204f2505038cdd83b42edc469b9/fonttools-4.60.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903", size = 4911031, upload-time = "2025-09-29T21:11:48.977Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9f/89411cc116effaec5260ad519162f64f9c150e5522a27cbb05eb62d0c05b/fonttools-4.60.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed", size = 5062966, upload-time = "2025-09-29T21:11:54.344Z" }, + { url = "https://files.pythonhosted.org/packages/62/a1/f888221934b5731d46cb9991c7a71f30cb1f97c0ef5fcf37f8da8fce6c8e/fonttools-4.60.1-cp312-cp312-win32.whl", hash = "sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6", size = 2218750, upload-time = "2025-09-29T21:11:56.601Z" }, + { url = "https://files.pythonhosted.org/packages/88/8f/a55b5550cd33cd1028601df41acd057d4be20efa5c958f417b0c0613924d/fonttools-4.60.1-cp312-cp312-win_amd64.whl", hash = "sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383", size = 2267026, upload-time = "2025-09-29T21:11:58.852Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5b/cdd2c612277b7ac7ec8c0c9bc41812c43dc7b2d5f2b0897e15fdf5a1f915/fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb", size = 2825777, upload-time = "2025-09-29T21:12:01.22Z" }, + { url = "https://files.pythonhosted.org/packages/d6/8a/de9cc0540f542963ba5e8f3a1f6ad48fa211badc3177783b9d5cadf79b5d/fonttools-4.60.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4", size = 2348080, upload-time = "2025-09-29T21:12:03.785Z" }, + { url = "https://files.pythonhosted.org/packages/2d/8b/371ab3cec97ee3fe1126b3406b7abd60c8fec8975fd79a3c75cdea0c3d83/fonttools-4.60.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c", size = 4903082, upload-time = "2025-09-29T21:12:06.382Z" }, + { url = "https://files.pythonhosted.org/packages/04/05/06b1455e4bc653fcb2117ac3ef5fa3a8a14919b93c60742d04440605d058/fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77", size = 4960125, upload-time = "2025-09-29T21:12:09.314Z" }, + { url = "https://files.pythonhosted.org/packages/8e/37/f3b840fcb2666f6cb97038793606bdd83488dca2d0b0fc542ccc20afa668/fonttools-4.60.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199", size = 4901454, upload-time = "2025-09-29T21:12:11.931Z" }, + { url = "https://files.pythonhosted.org/packages/fd/9e/eb76f77e82f8d4a46420aadff12cec6237751b0fb9ef1de373186dcffb5f/fonttools-4.60.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c", size = 5044495, upload-time = "2025-09-29T21:12:15.241Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b3/cede8f8235d42ff7ae891bae8d619d02c8ac9fd0cfc450c5927a6200c70d/fonttools-4.60.1-cp313-cp313-win32.whl", hash = "sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272", size = 2217028, upload-time = "2025-09-29T21:12:17.96Z" }, + { url = "https://files.pythonhosted.org/packages/75/4d/b022c1577807ce8b31ffe055306ec13a866f2337ecee96e75b24b9b753ea/fonttools-4.60.1-cp313-cp313-win_amd64.whl", hash = "sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac", size = 2266200, upload-time = "2025-09-29T21:12:20.14Z" }, + { url = "https://files.pythonhosted.org/packages/9a/83/752ca11c1aa9a899b793a130f2e466b79ea0cf7279c8d79c178fc954a07b/fonttools-4.60.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3", size = 2822830, upload-time = "2025-09-29T21:12:24.406Z" }, + { url = "https://files.pythonhosted.org/packages/57/17/bbeab391100331950a96ce55cfbbff27d781c1b85ebafb4167eae50d9fe3/fonttools-4.60.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85", size = 2345524, upload-time = "2025-09-29T21:12:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2e/d4831caa96d85a84dd0da1d9f90d81cec081f551e0ea216df684092c6c97/fonttools-4.60.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537", size = 4843490, upload-time = "2025-09-29T21:12:29.123Z" }, + { url = "https://files.pythonhosted.org/packages/49/13/5e2ea7c7a101b6fc3941be65307ef8df92cbbfa6ec4804032baf1893b434/fonttools-4.60.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003", size = 4944184, upload-time = "2025-09-29T21:12:31.414Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2b/cf9603551c525b73fc47c52ee0b82a891579a93d9651ed694e4e2cd08bb8/fonttools-4.60.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08", size = 4890218, upload-time = "2025-09-29T21:12:33.936Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2f/933d2352422e25f2376aae74f79eaa882a50fb3bfef3c0d4f50501267101/fonttools-4.60.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99", size = 4999324, upload-time = "2025-09-29T21:12:36.637Z" }, + { url = "https://files.pythonhosted.org/packages/38/99/234594c0391221f66216bc2c886923513b3399a148defaccf81dc3be6560/fonttools-4.60.1-cp314-cp314-win32.whl", hash = "sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6", size = 2220861, upload-time = "2025-09-29T21:12:39.108Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1d/edb5b23726dde50fc4068e1493e4fc7658eeefcaf75d4c5ffce067d07ae5/fonttools-4.60.1-cp314-cp314-win_amd64.whl", hash = "sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987", size = 2270934, upload-time = "2025-09-29T21:12:41.339Z" }, + { url = "https://files.pythonhosted.org/packages/fb/da/1392aaa2170adc7071fe7f9cfd181a5684a7afcde605aebddf1fb4d76df5/fonttools-4.60.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299", size = 2894340, upload-time = "2025-09-29T21:12:43.774Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a7/3b9f16e010d536ce567058b931a20b590d8f3177b2eda09edd92e392375d/fonttools-4.60.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01", size = 2375073, upload-time = "2025-09-29T21:12:46.437Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/e9bcf51980f98e59bb5bb7c382a63c6f6cac0eec5f67de6d8f2322382065/fonttools-4.60.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801", size = 4849758, upload-time = "2025-09-29T21:12:48.694Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dc/1d2cf7d1cba82264b2f8385db3f5960e3d8ce756b4dc65b700d2c496f7e9/fonttools-4.60.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc", size = 5085598, upload-time = "2025-09-29T21:12:51.081Z" }, + { url = "https://files.pythonhosted.org/packages/5d/4d/279e28ba87fb20e0c69baf72b60bbf1c4d873af1476806a7b5f2b7fac1ff/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc", size = 4957603, upload-time = "2025-09-29T21:12:53.423Z" }, + { url = "https://files.pythonhosted.org/packages/78/d4/ff19976305e0c05aa3340c805475abb00224c954d3c65e82c0a69633d55d/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed", size = 4974184, upload-time = "2025-09-29T21:12:55.962Z" }, + { url = "https://files.pythonhosted.org/packages/63/22/8553ff6166f5cd21cfaa115aaacaa0dc73b91c079a8cfd54a482cbc0f4f5/fonttools-4.60.1-cp314-cp314t-win32.whl", hash = "sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259", size = 2282241, upload-time = "2025-09-29T21:12:58.179Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cb/fa7b4d148e11d5a72761a22e595344133e83a9507a4c231df972e657579b/fonttools-4.60.1-cp314-cp314t-win_amd64.whl", hash = "sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c", size = 2345760, upload-time = "2025-09-29T21:13:00.375Z" }, + { url = "https://files.pythonhosted.org/packages/c7/93/0dd45cd283c32dea1545151d8c3637b4b8c53cdb3a625aeb2885b184d74d/fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb", size = 1143175, upload-time = "2025-09-29T21:13:24.134Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, + { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, + { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, + { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, + { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, + { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" }, + { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" }, + { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" }, + { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" }, + { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" }, + { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" }, + { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" }, + { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" }, + { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" }, + { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" }, + { url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" }, + { url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" }, + { url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" }, + { url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" }, + { url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" }, + { url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" }, + { url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" }, + { url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" }, + { url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" }, + { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" }, + { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" }, + { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/e2/d2d5295be2f44c678ebaf3544ba32d20c1f9ef08c49fe47f496180e1db15/matplotlib-3.10.7.tar.gz", hash = "sha256:a06ba7e2a2ef9131c79c49e63dad355d2d878413a0376c1727c8b9335ff731c7", size = 34804865, upload-time = "2025-10-09T00:28:00.669Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/b3/09eb0f7796932826ec20c25b517d568627754f6c6462fca19e12c02f2e12/matplotlib-3.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a0edb7209e21840e8361e91ea84ea676658aa93edd5f8762793dec77a4a6748", size = 8272389, upload-time = "2025-10-09T00:26:42.474Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/1ae80ddafb8652fd8046cb5c8460ecc8d4afccb89e2c6d6bec61e04e1eaf/matplotlib-3.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c380371d3c23e0eadf8ebff114445b9f970aff2010198d498d4ab4c3b41eea4f", size = 8128247, upload-time = "2025-10-09T00:26:44.77Z" }, + { url = "https://files.pythonhosted.org/packages/7d/18/95ae2e242d4a5c98bd6e90e36e128d71cf1c7e39b0874feaed3ef782e789/matplotlib-3.10.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d5f256d49fea31f40f166a5e3131235a5d2f4b7f44520b1cf0baf1ce568ccff0", size = 8696996, upload-time = "2025-10-09T00:26:46.792Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3d/5b559efc800bd05cb2033aa85f7e13af51958136a48327f7c261801ff90a/matplotlib-3.10.7-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11ae579ac83cdf3fb72573bb89f70e0534de05266728740d478f0f818983c695", size = 9530153, upload-time = "2025-10-09T00:26:49.07Z" }, + { url = "https://files.pythonhosted.org/packages/88/57/eab4a719fd110312d3c220595d63a3c85ec2a39723f0f4e7fa7e6e3f74ba/matplotlib-3.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4c14b6acd16cddc3569a2d515cfdd81c7a68ac5639b76548cfc1a9e48b20eb65", size = 9593093, upload-time = "2025-10-09T00:26:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/31/3c/80816f027b3a4a28cd2a0a6ef7f89a2db22310e945cd886ec25bfb399221/matplotlib-3.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:0d8c32b7ea6fb80b1aeff5a2ceb3fb9778e2759e899d9beff75584714afcc5ee", size = 8122771, upload-time = "2025-10-09T00:26:53.296Z" }, + { url = "https://files.pythonhosted.org/packages/de/77/ef1fc78bfe99999b2675435cc52120887191c566b25017d78beaabef7f2d/matplotlib-3.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:5f3f6d315dcc176ba7ca6e74c7768fb7e4cf566c49cb143f6bc257b62e634ed8", size = 7992812, upload-time = "2025-10-09T00:26:54.882Z" }, + { url = "https://files.pythonhosted.org/packages/02/9c/207547916a02c78f6bdd83448d9b21afbc42f6379ed887ecf610984f3b4e/matplotlib-3.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1d9d3713a237970569156cfb4de7533b7c4eacdd61789726f444f96a0d28f57f", size = 8273212, upload-time = "2025-10-09T00:26:56.752Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d0/b3d3338d467d3fc937f0bb7f256711395cae6f78e22cef0656159950adf0/matplotlib-3.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37a1fea41153dd6ee061d21ab69c9cf2cf543160b1b85d89cd3d2e2a7902ca4c", size = 8128713, upload-time = "2025-10-09T00:26:59.001Z" }, + { url = "https://files.pythonhosted.org/packages/22/ff/6425bf5c20d79aa5b959d1ce9e65f599632345391381c9a104133fe0b171/matplotlib-3.10.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b3c4ea4948d93c9c29dc01c0c23eef66f2101bf75158c291b88de6525c55c3d1", size = 8698527, upload-time = "2025-10-09T00:27:00.69Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7f/ccdca06f4c2e6c7989270ed7829b8679466682f4cfc0f8c9986241c023b6/matplotlib-3.10.7-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22df30ffaa89f6643206cf13877191c63a50e8f800b038bc39bee9d2d4957632", size = 9529690, upload-time = "2025-10-09T00:27:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/b8/95/b80fc2c1f269f21ff3d193ca697358e24408c33ce2b106a7438a45407b63/matplotlib-3.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b69676845a0a66f9da30e87f48be36734d6748024b525ec4710be40194282c84", size = 9593732, upload-time = "2025-10-09T00:27:04.653Z" }, + { url = "https://files.pythonhosted.org/packages/e1/b6/23064a96308b9aeceeffa65e96bcde459a2ea4934d311dee20afde7407a0/matplotlib-3.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:744991e0cc863dd669c8dc9136ca4e6e0082be2070b9d793cbd64bec872a6815", size = 8122727, upload-time = "2025-10-09T00:27:06.814Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a6/2faaf48133b82cf3607759027f82b5c702aa99cdfcefb7f93d6ccf26a424/matplotlib-3.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:fba2974df0bf8ce3c995fa84b79cde38326e0f7b5409e7a3a481c1141340bcf7", size = 7992958, upload-time = "2025-10-09T00:27:08.567Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f0/b018fed0b599bd48d84c08794cb242227fe3341952da102ee9d9682db574/matplotlib-3.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:932c55d1fa7af4423422cb6a492a31cbcbdbe68fd1a9a3f545aa5e7a143b5355", size = 8316849, upload-time = "2025-10-09T00:27:10.254Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b7/bb4f23856197659f275e11a2a164e36e65e9b48ea3e93c4ec25b4f163198/matplotlib-3.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e38c2d581d62ee729a6e144c47a71b3f42fb4187508dbbf4fe71d5612c3433b", size = 8178225, upload-time = "2025-10-09T00:27:12.241Z" }, + { url = "https://files.pythonhosted.org/packages/62/56/0600609893ff277e6f3ab3c0cef4eafa6e61006c058e84286c467223d4d5/matplotlib-3.10.7-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:786656bb13c237bbcebcd402f65f44dd61ead60ee3deb045af429d889c8dbc67", size = 8711708, upload-time = "2025-10-09T00:27:13.879Z" }, + { url = "https://files.pythonhosted.org/packages/d8/1a/6bfecb0cafe94d6658f2f1af22c43b76cf7a1c2f0dc34ef84cbb6809617e/matplotlib-3.10.7-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09d7945a70ea43bf9248f4b6582734c2fe726723204a76eca233f24cffc7ef67", size = 9541409, upload-time = "2025-10-09T00:27:15.684Z" }, + { url = "https://files.pythonhosted.org/packages/08/50/95122a407d7f2e446fd865e2388a232a23f2b81934960ea802f3171518e4/matplotlib-3.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d0b181e9fa8daf1d9f2d4c547527b167cb8838fc587deabca7b5c01f97199e84", size = 9594054, upload-time = "2025-10-09T00:27:17.547Z" }, + { url = "https://files.pythonhosted.org/packages/13/76/75b194a43b81583478a81e78a07da8d9ca6ddf50dd0a2ccabf258059481d/matplotlib-3.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:31963603041634ce1a96053047b40961f7a29eb8f9a62e80cc2c0427aa1d22a2", size = 8200100, upload-time = "2025-10-09T00:27:20.039Z" }, + { url = "https://files.pythonhosted.org/packages/f5/9e/6aefebdc9f8235c12bdeeda44cc0383d89c1e41da2c400caf3ee2073a3ce/matplotlib-3.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:aebed7b50aa6ac698c90f60f854b47e48cd2252b30510e7a1feddaf5a3f72cbf", size = 8042131, upload-time = "2025-10-09T00:27:21.608Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4b/e5bc2c321b6a7e3a75638d937d19ea267c34bd5a90e12bee76c4d7c7a0d9/matplotlib-3.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d883460c43e8c6b173fef244a2341f7f7c0e9725c7fe68306e8e44ed9c8fb100", size = 8273787, upload-time = "2025-10-09T00:27:23.27Z" }, + { url = "https://files.pythonhosted.org/packages/86/ad/6efae459c56c2fbc404da154e13e3a6039129f3c942b0152624f1c621f05/matplotlib-3.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07124afcf7a6504eafcb8ce94091c5898bbdd351519a1beb5c45f7a38c67e77f", size = 8131348, upload-time = "2025-10-09T00:27:24.926Z" }, + { url = "https://files.pythonhosted.org/packages/a6/5a/a4284d2958dee4116359cc05d7e19c057e64ece1b4ac986ab0f2f4d52d5a/matplotlib-3.10.7-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c17398b709a6cce3d9fdb1595c33e356d91c098cd9486cb2cc21ea2ea418e715", size = 9533949, upload-time = "2025-10-09T00:27:26.704Z" }, + { url = "https://files.pythonhosted.org/packages/de/ff/f3781b5057fa3786623ad8976fc9f7b0d02b2f28534751fd5a44240de4cf/matplotlib-3.10.7-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7146d64f561498764561e9cd0ed64fcf582e570fc519e6f521e2d0cfd43365e1", size = 9804247, upload-time = "2025-10-09T00:27:28.514Z" }, + { url = "https://files.pythonhosted.org/packages/47/5a/993a59facb8444efb0e197bf55f545ee449902dcee86a4dfc580c3b61314/matplotlib-3.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90ad854c0a435da3104c01e2c6f0028d7e719b690998a2333d7218db80950722", size = 9595497, upload-time = "2025-10-09T00:27:30.418Z" }, + { url = "https://files.pythonhosted.org/packages/0d/a5/77c95aaa9bb32c345cbb49626ad8eb15550cba2e6d4c88081a6c2ac7b08d/matplotlib-3.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:4645fc5d9d20ffa3a39361fcdbcec731382763b623b72627806bf251b6388866", size = 8252732, upload-time = "2025-10-09T00:27:32.332Z" }, + { url = "https://files.pythonhosted.org/packages/74/04/45d269b4268d222390d7817dae77b159651909669a34ee9fdee336db5883/matplotlib-3.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:9257be2f2a03415f9105c486d304a321168e61ad450f6153d77c69504ad764bb", size = 8124240, upload-time = "2025-10-09T00:27:33.94Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c7/ca01c607bb827158b439208c153d6f14ddb9fb640768f06f7ca3488ae67b/matplotlib-3.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1e4bbad66c177a8fdfa53972e5ef8be72a5f27e6a607cec0d8579abd0f3102b1", size = 8316938, upload-time = "2025-10-09T00:27:35.534Z" }, + { url = "https://files.pythonhosted.org/packages/84/d2/5539e66e9f56d2fdec94bb8436f5e449683b4e199bcc897c44fbe3c99e28/matplotlib-3.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d8eb7194b084b12feb19142262165832fc6ee879b945491d1c3d4660748020c4", size = 8178245, upload-time = "2025-10-09T00:27:37.334Z" }, + { url = "https://files.pythonhosted.org/packages/77/b5/e6ca22901fd3e4fe433a82e583436dd872f6c966fca7e63cf806b40356f8/matplotlib-3.10.7-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d41379b05528091f00e1728004f9a8d7191260f3862178b88e8fd770206318", size = 9541411, upload-time = "2025-10-09T00:27:39.387Z" }, + { url = "https://files.pythonhosted.org/packages/9e/99/a4524db57cad8fee54b7237239a8f8360bfcfa3170d37c9e71c090c0f409/matplotlib-3.10.7-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a74f79fafb2e177f240579bc83f0b60f82cc47d2f1d260f422a0627207008ca", size = 9803664, upload-time = "2025-10-09T00:27:41.492Z" }, + { url = "https://files.pythonhosted.org/packages/e6/a5/85e2edf76ea0ad4288d174926d9454ea85f3ce5390cc4e6fab196cbf250b/matplotlib-3.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:702590829c30aada1e8cef0568ddbffa77ca747b4d6e36c6d173f66e301f89cc", size = 9594066, upload-time = "2025-10-09T00:27:43.694Z" }, + { url = "https://files.pythonhosted.org/packages/39/69/9684368a314f6d83fe5c5ad2a4121a3a8e03723d2e5c8ea17b66c1bad0e7/matplotlib-3.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:f79d5de970fc90cd5591f60053aecfce1fcd736e0303d9f0bf86be649fa68fb8", size = 8342832, upload-time = "2025-10-09T00:27:45.543Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/e22e08da14bc1a0894184640d47819d2338b792732e20d292bf86e5ab785/matplotlib-3.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:cb783436e47fcf82064baca52ce748af71725d0352e1d31564cbe9c95df92b9c", size = 8172585, upload-time = "2025-10-09T00:27:47.185Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "numpy" +version = "2.3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" }, + { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" }, + { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" }, + { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" }, + { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" }, + { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" }, + { url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" }, + { url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" }, + { url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" }, + { url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" }, + { url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" }, + { url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" }, + { url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" }, + { url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" }, + { url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" }, + { url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" }, + { url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" }, + { url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" }, + { url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" }, + { url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" }, + { url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" }, + { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" }, + { url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706, upload-time = "2025-11-16T22:51:19.558Z" }, + { url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507, upload-time = "2025-11-16T22:51:22.492Z" }, + { url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049, upload-time = "2025-11-16T22:51:25.171Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603, upload-time = "2025-11-16T22:51:27Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696, upload-time = "2025-11-16T22:51:29.402Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350, upload-time = "2025-11-16T22:51:32.167Z" }, + { url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190, upload-time = "2025-11-16T22:51:35.403Z" }, + { url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749, upload-time = "2025-11-16T22:51:39.698Z" }, + { url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432, upload-time = "2025-11-16T22:51:42.476Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388, upload-time = "2025-11-16T22:51:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651, upload-time = "2025-11-16T22:51:47.749Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503, upload-time = "2025-11-16T22:51:50.443Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612, upload-time = "2025-11-16T22:51:53.609Z" }, + { url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042, upload-time = "2025-11-16T22:51:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502, upload-time = "2025-11-16T22:51:58.584Z" }, + { url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962, upload-time = "2025-11-16T22:52:01.698Z" }, + { url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054, upload-time = "2025-11-16T22:52:04.267Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613, upload-time = "2025-11-16T22:52:08.651Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147, upload-time = "2025-11-16T22:52:11.453Z" }, + { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" }, + { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "pillow" +version = "12.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377, upload-time = "2025-10-15T18:22:05.993Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343, upload-time = "2025-10-15T18:22:07.718Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981, upload-time = "2025-10-15T18:22:09.287Z" }, + { url = "https://files.pythonhosted.org/packages/88/e1/9098d3ce341a8750b55b0e00c03f1630d6178f38ac191c81c97a3b047b44/pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d", size = 8041399, upload-time = "2025-10-15T18:22:10.872Z" }, + { url = "https://files.pythonhosted.org/packages/a7/62/a22e8d3b602ae8cc01446d0c57a54e982737f44b6f2e1e019a925143771d/pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953", size = 6347740, upload-time = "2025-10-15T18:22:12.769Z" }, + { url = "https://files.pythonhosted.org/packages/4f/87/424511bdcd02c8d7acf9f65caa09f291a519b16bd83c3fb3374b3d4ae951/pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8", size = 7040201, upload-time = "2025-10-15T18:22:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4d/435c8ac688c54d11755aedfdd9f29c9eeddf68d150fe42d1d3dbd2365149/pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79", size = 6462334, upload-time = "2025-10-15T18:22:16.375Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f2/ad34167a8059a59b8ad10bc5c72d4d9b35acc6b7c0877af8ac885b5f2044/pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba", size = 7134162, upload-time = "2025-10-15T18:22:17.996Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/a7391df6adacf0a5c2cf6ac1cf1fcc1369e7d439d28f637a847f8803beb3/pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0", size = 6298769, upload-time = "2025-10-15T18:22:19.923Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0b/d87733741526541c909bbf159e338dcace4f982daac6e5a8d6be225ca32d/pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a", size = 7001107, upload-time = "2025-10-15T18:22:21.644Z" }, + { url = "https://files.pythonhosted.org/packages/bc/96/aaa61ce33cc98421fb6088af2a03be4157b1e7e0e87087c888e2370a7f45/pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", size = 2436012, upload-time = "2025-10-15T18:22:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" }, + { url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" }, + { url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" }, + { url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" }, + { url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" }, + { url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" }, + { url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" }, + { url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" }, + { url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" }, + { url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" }, + { url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" }, + { url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" }, + { url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" }, + { url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" }, + { url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" }, + { url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" }, + { url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" }, + { url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" }, + { url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" }, + { url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" }, + { url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" }, + { url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" }, + { url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" }, + { url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.2.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274, upload-time = "2025-09-21T04:11:06.277Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytokens" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/8d/a762be14dae1c3bf280202ba3172020b2b0b4c537f94427435f19c413b72/pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a", size = 17644, upload-time = "2025-11-05T13:36:35.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/25/d9db8be44e205a124f6c98bc0324b2bb149b7431c53877fc6d1038dddaf5/pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3", size = 12195, upload-time = "2025-11-05T13:36:33.183Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "seaborn" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy" }, + { name = "pandas" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696, upload-time = "2024-01-25T13:21:52.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914, upload-time = "2024-01-25T13:21:49.598Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "testproject" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "numpy" }, + { name = "pandas" }, + { name = "requests" }, +] + +[package.optional-dependencies] +dev = [ + { name = "black" }, + { name = "pytest" }, +] +viz = [ + { name = "matplotlib" }, + { name = "seaborn" }, +] + +[package.metadata] +requires-dist = [ + { name = "black", marker = "extra == 'dev'", specifier = ">=23.0.0" }, + { name = "matplotlib", marker = "extra == 'viz'", specifier = ">=3.5.0" }, + { name = "numpy", specifier = ">=2.3.5" }, + { name = "pandas", specifier = ">=2.3.3" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, + { name = "requests", specifier = ">=2.32.5" }, + { name = "seaborn", marker = "extra == 'viz'", specifier = ">=0.12.0" }, +] +provides-extras = ["dev", "viz"] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] From d80a9500655055b0035a756ee8c8090cbfb6a0b2 Mon Sep 17 00:00:00 2001 From: KercyDing Date: Mon, 17 Nov 2025 08:29:16 +0800 Subject: [PATCH 9/9] docs(release): add v0.2.0 documentation - Add v0.2.0 release notes to CHANGELOG with complete feature list - Document four-method command architecture (create/clone/new/sync) - Add 570-line COMMANDS.md with complete command reference - Add 489-line USE_CASES.md with real-world usage scenarios - Update README to reflect template-driven workflow - Simplify Core Philosophy to concise bullet points - Add Documentation section with quick help and docs links - Update code examples to use modern `uv add` syntax - Clean up test project files --- CHANGELOG.md | 102 +++++ README.md | 146 ++++++- docs/COMMANDS.md | 434 +++++++++++++++++++++ docs/USE_CASES.md | 474 +++++++++++++++++++++++ testproject/pyproject.toml | 17 - testproject/uv.lock | 755 ------------------------------------- 6 files changed, 1137 insertions(+), 791 deletions(-) create mode 100644 docs/COMMANDS.md create mode 100644 docs/USE_CASES.md delete mode 100644 testproject/pyproject.toml delete mode 100644 testproject/uv.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index db8b04e..6d2209b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,104 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.2.0] - 2025-11-17 + +### Added + +- **Four-Method Command Architecture**: Clear separation of responsibilities + - `uvup clone ` - Create exact 1:1 copy of an environment + - `uvup new --template