From dcd49ab4975792af507900275c76aa2da6ce4ed0 Mon Sep 17 00:00:00 2001 From: Reiase Date: Sat, 27 Jun 2026 21:00:13 +0800 Subject: [PATCH 1/2] Harden cluster mutex handling and align docs with refactor reality. Use poison-safe lock_mutex in torchrun cluster paths, rewrite federation catalog prefixes via AST (with legacy fallback), update modularity docs for known boundary debt and torchrun vs Pulsing membership, soften README overhead claims, and track uv.lock while gitignoring generated bundled_web. Co-authored-by: Cursor --- AGENTS.md | 78 + README.md | 2 +- docs/src/design/modularity.md | 22 +- docs/src/design/modularity.zh.md | 23 +- probing/core/src/core/federation/rewrite.rs | 124 +- probing/server/src/cluster_report_backoff.rs | 3 +- probing/server/src/torchrun_cluster.rs | 39 +- .../tests/hierarchical_cluster_report.rs | 7 +- uv.lock | 1307 +++++++++++++++++ 9 files changed, 1573 insertions(+), 32 deletions(-) create mode 100644 AGENTS.md create mode 100644 uv.lock diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..0fab432d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,78 @@ +# Agent instructions — probing + +This repository uses the [Agent Skills](https://agentskills.io) layout for training diagnostics. + +## Module boundaries + +Before adding code, read **`docs/src/design/modularity.md`** (中文: `modularity.zh.md`). + +| Layer | Where | Your change belongs if… | +|-------|--------|-------------------------| +| L1 Platform | `probing/core`, `memtable`, `proto` | SQL engine, federation, storage format | +| L2 Collectors | `probing/extensions/*`, `python/probing/profiling` | New metrics / tables | +| L3 Control | `probing/server`, `probing/cli` | HTTP, inject, fan-out | +| L4 Experience | `skills/`, `web/`, Python hooks | Diagnostics UX, skills, UI | + +**Contracts:** `ProbeDataSource` (tables), `ProbeExtension` (config/HTTP), `@table` (Python data), `skills/*/steps.yaml` (workflows). Do not add cross-collector calls — use SQL JOINs. + +## Skills + +All diagnostic skills live under **`skills/`**. Each subdirectory contains: + +- **`SKILL.md`** — when to use the skill and how to interpret results (read this for routing) +- **`steps.yaml`** — executable probe steps (used by `probing skill run` and the Web Investigate agent) + +Browse the catalog: `skills/catalog.yaml` + +## Install skills into your agent + +So Cursor / Claude Code / Codex can discover and invoke skills: + +```bash +./skills/install.sh +``` + +This copies `skills//` into: + +- `.cursor/skills/` (Cursor) +- `.claude/skills/` (Claude Code) +- `.agents/skills/` (Codex) + +Use `probing skill install --user` for global install under `~/`. + +## Run diagnostics + +Requires a probed training process (`PROBING=1` or `probing -t inject`): + +```bash +probing skill list +probing -t skill run health_overview +probing -t skill run slow_rank --global +probing -t skill run nccl_culprit_victim +``` + +From Python (e.g. in agent-generated scripts): + +```python +from probing.skills.tools import list_skills, run_skill +run_skill("health_overview", target="") +``` + +## Built-in skills (summary) + +| id | use when | +|----|----------| +| `health_overview` | first look / triage | +| `training_hang` | stall or hang | +| `slow_rank` | straggler rank | +| `comm_bottleneck` | NCCL / collective slow | +| `nccl_culprit_victim` | NCCL culprit/victim analysis | +| `memory_leak` | GPU memory growth | +| `module_bottleneck` | slow PyTorch modules | +| `gpu_pressure` | GPU util / headroom | + +Details in each `skills//SKILL.md`. + +## Extending + +Add table plugins under `python/probing/ext/` (data). Add diagnostic skills under `skills/` (how to investigate). NCCL profiler plugin: `docs/src/design/nccl-profiler.md`. See `docs/src/design/extensibility.md`. diff --git a/README.md b/README.md index 5c8d8194..23aa65ab 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ > Uncover the Hidden Truth of AI Performance -Probing is a production-grade performance profiler designed specifically for distributed AI workloads. Built on dynamic probe injection technology, it delivers zero-overhead runtime introspection with SQL-queryable performance metrics and cross-node correlation analysis. +Probing is a production-grade performance profiler designed specifically for distributed AI workloads. Built on dynamic probe injection technology, it delivers **minimal-overhead** runtime introspection—lock-free mmap writes on the hot path, background sampling—with SQL-queryable performance metrics and cross-node correlation analysis. ### What probing delivers... diff --git a/docs/src/design/modularity.md b/docs/src/design/modularity.md index 66ea6c69..e48764a9 100644 --- a/docs/src/design/modularity.md +++ b/docs/src/design/modularity.md @@ -113,7 +113,7 @@ Python-side collectors (same layer, different language): | Unit | Path | Responsibility | |------|------|----------------| -| **probing-server** | `probing/server/` | Axum routes, auth, `initialize_engine()`, cluster fan-out | +| **probing-server** | `probing/server/` | Axum routes, auth, `initialize_engine()`, cluster fan-out, **torchrun heartbeat** (`torchrun_cluster.rs`) | | **probing-cli** | `probing/cli/` | HTTP client to probe; inject/list/query/repl/**skill** | Stable HTTP surface: `probing/server/API.md`, enforced by `tests/regression/spec/api_spec.json`. @@ -325,7 +325,9 @@ Use this table to decide **where a change belongs**: | Concern | Owner module | Interface | |---------|--------------|-----------| | SQL parsing, federation rewrite | probing-core | `Engine::async_query` | +| Table/column semantic docs | probing-core | `semantic_catalog` → `probe.probing.table_docs` / `column_docs` | | mmap format, compaction | probing-memtable | `RowWriter`, `ColdStore` | +| Torchrun cluster heartbeat | probing-server | `torchrun_cluster.rs`, `cluster_report_backoff.rs`, PUT `/apis/nodes` | | Mixed Python/C stack | probing-python/features | `python.backtrace`, pprof | | Torch module sampling | python/probing/profiling | `python.torch_trace` | | Collective wall time | python/probing/profiling/collective | `python.comm_collective` | @@ -372,6 +374,9 @@ Track and fix incrementally: | Issue | Current | Target | |-------|---------|--------| | Python ext → CLI | `probing-python` → `probing-cli` | **Accepted** for maturin wheel (`cli_main` only); keep import surface minimal | +| Python ext → CC | `probing-python` → `probing-cc` (stack tracer SIGUSR2) | Move signal helper to L1 shared util or thin `probing-signals` crate | +| Core → NCCL/HCCL | `probing-core` → `probing-nccl-profiler` / `probing-hccl-shim` (`builtin-schema-docs` feature) for `semantic_catalog` | Register docs via collector hooks or manifest; drop L1→L2 compile deps | +| Core → skills YAML | `semantic_catalog.rs` `include_str!(skills/semantic/tables.yaml)` | Accept as L4 overlay SSOT, or move YAML under `probing/core/resources/` | | Server → python `features/*` | ~~`server/profiling.rs`~~ removed | Flamegraphs via `torchextension` / `pprofextension` `ProbeExtensionCall` | | Server → python REPL internals | ~~`PythonRepl` in server~~ | `/ws` uses `ReplSession` facade only | | Composition sprawl | All wiring in `server/engine.rs` | Optional: manifest TOML listing enabled extensions | @@ -379,6 +384,19 @@ Track and fix incrementally: | kmsg collector | Registered (Linux/kmsg feature gate) | Done | | Architecture doc | 2-layer diagram | Superseded by this doc + [Data Layer](data-layer.md) | +### Cluster membership: Torchrun heartbeat vs Pulsing + +Two **complementary** paths populate `cluster.nodes` and power `global.*` federation. +They do not replace each other. + +| Path | Layer | When | Mechanism | +|------|-------|------|-----------| +| **Torchrun cluster heartbeat** | L3 `probing-server` | Default for `torchrun` / elastic jobs (`WORLD_SIZE > 1`, `PROBING=1/2`) | Hierarchical HTTP PUT + TCPStore side channel (`probing/torchrun//…`). Does **not** touch torch rendezvous keys. See [Torchrun cluster heartbeat](torchrun-cluster.md). | +| **Pulsing integration** | L4 passive + external runtime | Another process already runs [Pulsing](cluster-pulsing.md) and writes `pulsing.*` memtables | Probing discovers `pulsing.*` mmap tables; no probing-owned heartbeat thread. Optional bootstrap via Pulsing APIs. | + +**Default for torchrun users:** heartbeat auto-starts from the Rust ctor (`maybe_start_torchrun_cluster()`). +**Pulsing:** use when the job already centers on Pulsing for membership/failure detection, or you need Pulsing actors alongside probing tables. + --- ## 9. Adding a new feature (decision tree) @@ -414,6 +432,8 @@ Need new raw signals? | [Data Layer](data-layer.md) | MEMT/MEMC internals | | [Extensibility](extensibility.md) | Public extension paths (table + skill) | | [Distributed](distributed.md) | Federation & cluster | +| [Torchrun cluster heartbeat](torchrun-cluster.md) | Hierarchical torchrun membership | +| [Cluster with Pulsing](cluster-pulsing.md) | Optional Pulsing-based membership | | [NCCL Profiler](nccl-profiler.md) | NCCL plugin boundary | | [web/DESIGN.md](https://github.com/DeepLink-org/probing/blob/main/web/DESIGN.md) | UI module layout | | [AGENTS.md](https://github.com/DeepLink-org/probing/blob/main/AGENTS.md) | Agent skill usage | diff --git a/docs/src/design/modularity.zh.md b/docs/src/design/modularity.zh.md index 9aea1678..096845b4 100644 --- a/docs/src/design/modularity.zh.md +++ b/docs/src/design/modularity.zh.md @@ -108,7 +108,7 @@ Python 侧采集(同层,不同语言): | 单元 | 路径 | 职责 | |------|------|------| -| **probing-server** | `probing/server/` | 路由、认证、`initialize_engine()`、fan-out | +| **probing-server** | `probing/server/` | 路由、认证、`initialize_engine()`、fan-out、**torchrun 心跳**(`torchrun_cluster.rs`) | | **probing-cli** | `probing/cli/` | HTTP 客户端;inject/query/repl/**skill** | HTTP 契约:`probing/server/API.md` + `tests/regression/spec/api_spec.json`。 @@ -269,7 +269,9 @@ sequenceDiagram | 关注点 | 归属 | 接口 | |--------|------|------| | SQL 解析、federation 重写 | probing-core | `Engine::async_query` | +| 表/列语义文档 | probing-core | `semantic_catalog` → `probe.probing.table_docs` / `column_docs` | | mmap 格式、冷压缩 | probing-memtable | `RowWriter`, `ColdStore` | +| Torchrun 集群心跳 | probing-server | `torchrun_cluster.rs`、`cluster_report_backoff.rs`、`PUT /apis/nodes` | | 混合 Python/C 栈 | probing-python/features | `python.backtrace`、pprof | | Torch 模块采样 | python/profiling | `python.torch_trace` | | Collective 墙钟 | python/profiling/collective | `python.comm_collective` | @@ -313,13 +315,28 @@ sequenceDiagram | 问题 | 现状 | 目标 | |------|------|------| | python → cli | `probing-python` → `probing-cli` | **可接受**(maturin wheel,仅 `cli_main`);禁止扩散 import | +| python → cc | `probing-python` → `probing-cc`(stack tracer 发 SIGUSR2) | 信号 helper 下沉 L1 或独立 `probing-signals` | +| core → NCCL/HCCL | `probing-core` → nccl/hccl shim(`builtin-schema-docs`)供 `semantic_catalog` | 改由采集器注册 docs,去掉 L1→L2 编译依赖 | +| core → skills YAML | `semantic_catalog` `include_str!(skills/semantic/tables.yaml)` | 接受为 L4 overlay SSOT,或迁到 `probing/core/resources/` | | server → python features | ~~`server/profiling.rs`~~ 已删 | 火焰图走 Extension | | server → REPL 内部 | ~~`PythonRepl`~~ | `/ws` 仅用 `ReplSession` 门面 | | 组装集中 | 全在 server/engine.rs | 可选 extension manifest | | Skill 三份 loader | Rust/Python/Web | `skills/` 为 SSOT,CI 同步校验 | -| kmsg 采集器 | 已实现未注册 | 在 engine 注册或删除 | +| kmsg 采集器 | 已注册(Linux/kmsg feature gate) | Done | | Architecture 文档 | 二层旧图 | 以本文 + [数据层](data-layer.zh.md) 为准 | +### 集群成员:Torchrun 心跳 vs Pulsing + +两条**互补**路径填充 `cluster.nodes` 并支撑 `global.*` 联邦,互不取代。 + +| 路径 | 层 | 适用场景 | 机制 | +|------|-----|----------|------| +| **Torchrun 集群心跳** | L3 `probing-server` | 默认:`torchrun`/elastic(`WORLD_SIZE > 1`,`PROBING=1/2`) | 分层 HTTP PUT + TCPStore 旁路键(`probing/torchrun//…`),**不**写 rendezvous 键。见 [Torchrun 集群心跳](torchrun-cluster.zh.md)。 | +| **Pulsing 集成** | L4 被动 + 外部运行时 | 作业已跑 [Pulsing](cluster-pulsing.zh.md) 并写 `pulsing.*` memtable | Probing 发现 mmap 表;无 probing 自有心跳线程。 | + +**torchrun 用户默认**:Rust ctor 自动 `maybe_start_torchrun_cluster()`。 +**Pulsing**:作业以 Pulsing 为中心做成员/故障检测,或需要 Pulsing actor 与 probing 表并存时使用。 + --- ## 9. 新功能决策树 @@ -347,6 +364,8 @@ sequenceDiagram | [数据层](data-layer.zh.md) | MEMT/MEMC 内部实现 | | [扩展机制](extensibility.zh.md) | 对外扩展路径(表 + skill + NCCL) | | [分布式](distributed.zh.md) | 联邦与集群 | +| [Torchrun 集群心跳](torchrun-cluster.zh.md) | 分层 torchrun 成员注册 | +| [基于 Pulsing 的集群](cluster-pulsing.zh.md) | 可选 Pulsing 成员发现 | | [NCCL Profiler](nccl-profiler.zh.md) | NCCL 插件边界 | | [web/DESIGN.md](https://github.com/DeepLink-org/probing/blob/main/web/DESIGN.md) | 前端模块布局 | | [AGENTS.md](https://github.com/DeepLink-org/probing/blob/main/AGENTS.md) | Agent 使用 skill | diff --git a/probing/core/src/core/federation/rewrite.rs b/probing/core/src/core/federation/rewrite.rs index f6823a86..07033ce1 100644 --- a/probing/core/src/core/federation/rewrite.rs +++ b/probing/core/src/core/federation/rewrite.rs @@ -1,6 +1,10 @@ //! SQL rewrite helpers for the `probe` / `global` catalog split. -use datafusion::sql::sqlparser::ast::{Query, SelectItem, SetExpr, Statement}; +use std::ops::ControlFlow; + +use datafusion::sql::sqlparser::ast::{ + visit_relations_mut, Ident, ObjectName, ObjectNamePart, Query, SelectItem, SetExpr, Statement, +}; use datafusion::sql::sqlparser::dialect::GenericDialect; use datafusion::sql::sqlparser::parser::Parser; @@ -10,12 +14,98 @@ const KNOWN_SCHEMAS: &[&str] = &[ /// Rewrite federated SQL so remote probing nodes execute against the local `probe` catalog. pub fn rewrite_global_catalog_to_probe(sql: &str) -> String { - sql.replace("global.", "probe.") - .replace("GLOBAL.", "probe.") + rewrite_catalog_relations(sql, rewrite_relation_global_to_probe) + .unwrap_or_else(|| rewrite_global_catalog_to_probe_legacy(sql)) } /// Rewrite a user/cluster SQL string to reference the `global` catalog. pub fn rewrite_sql_for_global_fanout(sql: &str) -> String { + rewrite_catalog_relations(sql, rewrite_relation_to_global) + .unwrap_or_else(|| rewrite_sql_for_global_fanout_legacy(sql)) +} + +fn rewrite_catalog_relations(sql: &str, mut rewrite_fn: F) -> Option +where + F: FnMut(&mut ObjectName), +{ + let dialect = GenericDialect {}; + let mut stmts = Parser::parse_sql(&dialect, sql).ok()?; + if stmts.is_empty() { + return None; + } + for stmt in &mut stmts { + let _ = visit_relations_mut(stmt, |name| { + rewrite_fn(name); + ControlFlow::<(), ()>::Continue(()) + }); + } + Some( + stmts + .iter() + .map(|stmt| stmt.to_string()) + .collect::>() + .join("; "), + ) +} + +fn relation_ident_parts(name: &ObjectName) -> Vec { + name.0 + .iter() + .filter_map(|part| part.as_ident().map(|ident| ident.value.clone())) + .collect() +} + +fn set_relation_parts(name: &mut ObjectName, parts: &[&str]) { + name.0 = parts + .iter() + .map(|part| ObjectNamePart::Identifier(Ident::new(*part))) + .collect(); +} + +fn first_ident_eq(parts: &[String], expected: &str) -> bool { + parts + .first() + .is_some_and(|part| part.eq_ignore_ascii_case(expected)) +} + +fn rewrite_relation_global_to_probe(name: &mut ObjectName) { + let parts = relation_ident_parts(name); + if !first_ident_eq(&parts, "global") { + return; + } + let mut rewritten = vec!["probe"]; + rewritten.extend(parts.iter().skip(1).map(String::as_str)); + set_relation_parts(name, &rewritten); +} + +fn rewrite_relation_to_global(name: &mut ObjectName) { + let parts = relation_ident_parts(name); + if parts.is_empty() || first_ident_eq(&parts, "global") { + return; + } + if first_ident_eq(&parts, "probe") { + let mut rewritten = vec!["global"]; + rewritten.extend(parts.iter().skip(1).map(String::as_str)); + set_relation_parts(name, &rewritten); + return; + } + if parts.first().is_some_and(|part| { + KNOWN_SCHEMAS + .iter() + .any(|schema| part.eq_ignore_ascii_case(schema)) + }) { + let mut rewritten = vec!["global"]; + rewritten.extend(parts.iter().map(String::as_str)); + set_relation_parts(name, &rewritten); + } +} + +fn rewrite_global_catalog_to_probe_legacy(sql: &str) -> String { + sql.replace("global.", "probe.") + .replace("GLOBAL.", "probe.") +} + +fn rewrite_sql_for_global_fanout_legacy(sql: &str) -> String { if sql.to_lowercase().contains("global.") { return sql.to_string(); } @@ -291,4 +381,32 @@ mod tests { "SELECT * FROM global.python.comm_collective WHERE rank > 0 LIMIT 10" ); } + + #[test] + fn global_fanout_does_not_rewrite_catalog_prefix_in_string_literals() { + let sql = "SELECT 'probe.python.secret' AS note FROM python.metrics LIMIT 1"; + let out = rewrite_sql_for_global_fanout(sql); + assert!(out.contains("'probe.python.secret'")); + assert!(out.contains("global.python.metrics")); + assert!(!out.contains("'global.python.secret'")); + } + + #[test] + fn global_to_probe_does_not_rewrite_catalog_prefix_in_string_literals() { + let sql = "SELECT 'global.python.metrics' AS note FROM global.python.metrics LIMIT 1"; + let out = rewrite_global_catalog_to_probe(sql); + assert!(out.contains("'global.python.metrics'")); + assert!(out.contains("probe.python.metrics")); + assert!(!out.contains("'probe.python.metrics'")); + } + + #[test] + fn global_fanout_still_rewrites_unqualified_table_when_literal_mentions_global() { + let sql = "SELECT name FROM python.metrics WHERE name = 'see global.python.b'"; + let out = rewrite_sql_for_global_fanout(sql); + assert_eq!( + out, + "SELECT name FROM global.python.metrics WHERE name = 'see global.python.b'" + ); + } } diff --git a/probing/server/src/cluster_report_backoff.rs b/probing/server/src/cluster_report_backoff.rs index ecf138c4..0468afcd 100644 --- a/probing/server/src/cluster_report_backoff.rs +++ b/probing/server/src/cluster_report_backoff.rs @@ -193,6 +193,7 @@ pub fn classify_report_outcome( #[cfg(test)] mod tests { use super::*; + use probing_core::sync::lock_mutex; use std::sync::{LazyLock, Mutex}; static ENV_LOCK: LazyLock> = LazyLock::new(|| Mutex::new(())); @@ -210,7 +211,7 @@ mod tests { } fn with_env(vars: &[(&str, &str)], f: F) { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = lock_mutex(&ENV_LOCK, "cluster_report_backoff test ENV_LOCK"); clear_backoff_env(); for (k, v) in vars { std::env::set_var(k, v); diff --git a/probing/server/src/torchrun_cluster.rs b/probing/server/src/torchrun_cluster.rs index d16f0c65..3d8a1129 100644 --- a/probing/server/src/torchrun_cluster.rs +++ b/probing/server/src/torchrun_cluster.rs @@ -15,6 +15,7 @@ use std::sync::{LazyLock, Mutex}; use std::time::Duration; use anyhow::{Context, Result}; +use probing_core::sync::lock_mutex; use probing_proto::prelude::Node; use probing_store::store::TCPStore; use serde::{Deserialize, Serialize}; @@ -242,7 +243,7 @@ async fn publish_master() -> Result<()> { &serde_json::to_string(&info).context("serialize master info")?, ) .await?; - *MASTER_INFO.lock().unwrap() = Some(info); + *lock_mutex(&MASTER_INFO, "torchrun MASTER_INFO") = Some(info); log::info!( "probing torchrun: published master at {} (key={})", reachable, @@ -366,25 +367,23 @@ fn nodes_for_upstream_report() -> Vec { fn report_parent_http() -> Option { if local_rank() != 0 { - return LOCAL0_PARENT.lock().unwrap().clone(); + return lock_mutex(&LOCAL0_PARENT, "torchrun LOCAL0_PARENT").clone(); } if is_global_rank0() { return Some(local_http_base()); } - MASTER_INFO - .lock() - .unwrap() + lock_mutex(&MASTER_INFO, "torchrun MASTER_INFO") .as_ref() .map(|m| m.http_base.clone()) } fn ensure_master_info() -> bool { - if MASTER_INFO.lock().unwrap().is_some() { + if lock_mutex(&MASTER_INFO, "torchrun MASTER_INFO").is_some() { return true; } let timeout = Duration::from_secs_f64(discover_timeout_secs()); if let Some(info) = SERVER_RUNTIME.block_on(poll_master_info(timeout)) { - *MASTER_INFO.lock().unwrap() = Some(info); + *lock_mutex(&MASTER_INFO, "torchrun MASTER_INFO") = Some(info); return true; } false @@ -394,12 +393,12 @@ fn ensure_local0_parent() -> bool { if local_rank() == 0 { return true; } - if LOCAL0_PARENT.lock().unwrap().is_some() { + if lock_mutex(&LOCAL0_PARENT, "torchrun LOCAL0_PARENT").is_some() { return true; } let timeout = Duration::from_secs_f64(discover_timeout_secs()); if let Some(base) = SERVER_RUNTIME.block_on(poll_local0_parent(timeout)) { - *LOCAL0_PARENT.lock().unwrap() = Some(base); + *lock_mutex(&LOCAL0_PARENT, "torchrun LOCAL0_PARENT") = Some(base); return true; } false @@ -451,7 +450,7 @@ async fn hierarchical_report_worker() { .await .unwrap_or(ReportOutcome::Failed); let sleep_for = { - let mut backoff = REPORT_BACKOFF.lock().unwrap(); + let mut backoff = lock_mutex(&REPORT_BACKOFF, "torchrun REPORT_BACKOFF"); backoff.record(outcome); backoff.sleep_duration() }; @@ -512,12 +511,12 @@ pub fn refresh_torchrun_role() -> bool { if !STARTED.load(Ordering::SeqCst) { return false; } - REPORT_BACKOFF.lock().unwrap().reset(); + lock_mutex(&REPORT_BACKOFF, "torchrun REPORT_BACKOFF").reset(); SERVER_RUNTIME.spawn(async { let outcome = tokio::task::spawn_blocking(report_once) .await .unwrap_or(ReportOutcome::Failed); - let mut backoff = REPORT_BACKOFF.lock().unwrap(); + let mut backoff = lock_mutex(&REPORT_BACKOFF, "torchrun REPORT_BACKOFF"); backoff.record(outcome); }); true @@ -525,9 +524,7 @@ pub fn refresh_torchrun_role() -> bool { /// Master HTTP base URL when this process is global rank 0. pub fn master_http_base() -> Option { - MASTER_INFO - .lock() - .unwrap() + lock_mutex(&MASTER_INFO, "torchrun MASTER_INFO") .as_ref() .map(|m| m.http_base.clone()) } @@ -557,7 +554,7 @@ mod tests { #[test] fn run_prefix_uses_rdzv_id() { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = lock_mutex(&ENV_LOCK, "torchrun test ENV_LOCK"); clear_torchrun_env(); std::env::set_var("RDZV_ID", "probing-29680"); assert_eq!(run_prefix(), "probing/torchrun/probing-29680"); @@ -565,7 +562,7 @@ mod tests { #[test] fn local0_store_key_includes_node_rank() { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = lock_mutex(&ENV_LOCK, "torchrun test ENV_LOCK"); clear_torchrun_env(); std::env::set_var("TORCHELASTIC_RUN_ID", "job-x"); std::env::set_var("NODE_RANK", "3"); @@ -574,7 +571,7 @@ mod tests { #[test] fn merge_self_is_idempotent() { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = lock_mutex(&ENV_LOCK, "torchrun test ENV_LOCK"); clear_torchrun_env(); std::env::set_var("RANK", "0"); std::env::set_var("LOCAL_RANK", "0"); @@ -596,7 +593,7 @@ mod tests { #[test] fn local_group_and_leaf_filters() { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = lock_mutex(&ENV_LOCK, "torchrun test ENV_LOCK"); clear_torchrun_env(); std::env::set_var("GROUP_RANK", "3"); std::env::set_var("RANK", "24"); @@ -621,7 +618,7 @@ mod tests { #[test] fn reachable_addr_maps_unspecified_bind() { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = lock_mutex(&ENV_LOCK, "torchrun test ENV_LOCK"); clear_torchrun_env(); std::env::set_var("MASTER_ADDR", "10.0.0.1"); assert_eq!(reachable_addr("0.0.0.0:9922"), "10.0.0.1:9922"); @@ -629,7 +626,7 @@ mod tests { #[test] fn bind_spec_global_rank0_uses_probing_port() { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = lock_mutex(&ENV_LOCK, "torchrun test ENV_LOCK"); clear_torchrun_env(); std::env::set_var("RANK", "0"); std::env::set_var("PROBING_PORT", "18080"); diff --git a/probing/server/tests/hierarchical_cluster_report.rs b/probing/server/tests/hierarchical_cluster_report.rs index 0a15d9a0..780cd792 100644 --- a/probing/server/tests/hierarchical_cluster_report.rs +++ b/probing/server/tests/hierarchical_cluster_report.rs @@ -5,6 +5,7 @@ use std::sync::{Arc, LazyLock, Mutex}; use std::time::{SystemTime, UNIX_EPOCH}; use axum::{extract::State, routing::get, Json, Router}; +use probing_core::sync::lock_mutex; use probing_proto::prelude::{Cluster, Node, NodeReportRequest, NodeReportResponse}; use probing_server::cluster_http::{fetch_nodes_blocking, put_nodes_blocking}; use probing_server::server::SERVER_RUNTIME; @@ -44,7 +45,7 @@ async fn put_nodes_handler( State(state): State, Json(body): Json, ) -> Json { - let mut cluster = state.cluster.lock().unwrap(); + let mut cluster = lock_mutex(&state.cluster, "hierarchical_cluster_report cluster"); for mut node in body.nodes { if node.timestamp == 0 { node.timestamp = now_micros(); @@ -66,7 +67,7 @@ async fn put_nodes_handler( } async fn get_nodes_handler(State(state): State) -> Json> { - let cluster = state.cluster.lock().unwrap(); + let cluster = lock_mutex(&state.cluster, "hierarchical_cluster_report cluster"); let mut nodes = cluster.list(); nodes.sort_by_key(|n| n.rank.unwrap_or(i32::MAX)); Json(nodes) @@ -122,7 +123,7 @@ fn local_group_ranks(store: &[Node], group_rank: i32) -> Vec { #[test] fn hierarchical_two_nodes_times_two_gpus_converges_on_master() { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = lock_mutex(&ENV_LOCK, "hierarchical_cluster_report ENV_LOCK"); for key in ["RANK", "GROUP_RANK", "LOCAL_RANK"] { std::env::remove_var(key); } diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..25308c84 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1307 @@ +version = 1 +revision = 3 +requires-python = ">=3.7" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", + "python_full_version < '3.8'", +] + +[[package]] +name = "black" +version = "23.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "mypy-extensions", version = "1.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "packaging", version = "24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pathspec", version = "0.11.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "platformdirs", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "tomli", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "typed-ast", marker = "python_full_version < '3.8' and implementation_name == 'cpython'" }, + { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/36/66370f5017b100225ec4950a60caeef60201a10080da57ddb24124453fba/black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940", size = 582156, upload-time = "2023-03-29T01:00:54.457Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/f4/7908f71cc71da08df1317a3619f002cbf91927fb5d3ffc7723905a2113f7/black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915", size = 1342273, upload-time = "2023-03-29T01:19:03.787Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/07aab2623cfd3789786f17e051487a41d5657258c7b1ef8f780512ffea9c/black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9", size = 2676721, upload-time = "2023-03-29T01:25:23.459Z" }, + { url = "https://files.pythonhosted.org/packages/29/b1/b584fc863c155653963039664a592b3327b002405043b7e761b9b0212337/black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2", size = 1520336, upload-time = "2023-03-29T01:28:32.973Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b4/0f13ab7f5e364795ff82b76b0f9a4c9c50afda6f1e2feeb8b03fdd7ec57d/black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c", size = 1654611, upload-time = "2023-03-29T01:11:12.718Z" }, + { url = "https://files.pythonhosted.org/packages/de/b4/76f152c5eb0be5471c22cd18380d31d188930377a1a57969073b89d6615d/black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c", size = 1286657, upload-time = "2023-03-29T01:15:20.317Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6f/d3832960a3b646b333b7f0d80d336a3c123012e9d9d5dba4a622b2b6181d/black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6", size = 1326112, upload-time = "2023-03-29T01:19:05.794Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a5/17b40bfd9b607b69fa726b0b3a473d14b093dcd5191ea1a1dd664eccfee3/black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b", size = 2643808, upload-time = "2023-03-29T01:25:27.825Z" }, + { url = "https://files.pythonhosted.org/packages/69/49/7e1f0cf585b0d607aad3f971f95982cc4208fc77f92363d632d23021ee57/black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d", size = 1503287, upload-time = "2023-03-29T01:28:35.228Z" }, + { url = "https://files.pythonhosted.org/packages/c0/53/42e312c17cfda5c8fc4b6b396a508218807a3fcbb963b318e49d3ddd11d5/black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70", size = 1638625, upload-time = "2023-03-29T01:11:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0d/81dd4194ce7057c199d4f28e4c2a885082d9d929e7a55c514b23784f7787/black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326", size = 1293585, upload-time = "2023-03-29T01:15:22.935Z" }, + { url = "https://files.pythonhosted.org/packages/24/eb/2d2d2c27cb64cfd073896f62a952a802cd83cf943a692a2f278525b57ca9/black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b", size = 1447428, upload-time = "2023-03-29T01:28:37.49Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/15d2122f90ff1cd70f06892ebda777b650218cf84b56b5916a993dc1359a/black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2", size = 1576467, upload-time = "2023-03-29T01:11:18.293Z" }, + { url = "https://files.pythonhosted.org/packages/ca/44/eb41edd3f558a6139f09eee052dead4a7a464e563b822ddf236f5a8ee286/black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925", size = 1226437, upload-time = "2023-03-29T01:15:24.92Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f4/2b0c6ac9e1f8584296747f66dd511898b4ebd51d6510dba118279bff53b6/black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27", size = 1331955, upload-time = "2023-03-29T01:19:07.695Z" }, + { url = "https://files.pythonhosted.org/packages/21/14/d5a2bec5fb15f9118baab7123d344646fac0b1c6939d51c2b05259cd2d9c/black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331", size = 2658520, upload-time = "2023-03-29T01:25:30.535Z" }, + { url = "https://files.pythonhosted.org/packages/13/0a/ed8b66c299e896780e4528eed4018f5b084da3b9ba4ee48328550567d866/black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5", size = 1509852, upload-time = "2023-03-29T01:28:39.106Z" }, + { url = "https://files.pythonhosted.org/packages/12/4b/99c71d1cf1353edd5aff2700b8960f92e9b805c9dab72639b67dbb449d3a/black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961", size = 1641852, upload-time = "2023-03-29T01:11:20.065Z" }, + { url = "https://files.pythonhosted.org/packages/d1/6e/5810b6992ed70403124c67e8b3f62858a32b35405177553f1a78ed6b6e31/black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8", size = 1297694, upload-time = "2023-03-29T01:15:26.919Z" }, + { url = "https://files.pythonhosted.org/packages/13/25/cfa06788d0a936f2445af88f13604b5bcd5c9d050db618c718e6ebe66f74/black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30", size = 1341089, upload-time = "2023-03-29T01:19:09.503Z" }, + { url = "https://files.pythonhosted.org/packages/fd/5b/fc2d7922c1a6bb49458d424b5be71d251f2d0dc97be9534e35d171bdc653/black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3", size = 2674699, upload-time = "2023-03-29T01:25:32.853Z" }, + { url = "https://files.pythonhosted.org/packages/49/d7/f3b7da6c772800f5375aeb050a3dcf682f0bbeb41d313c9c2820d0156e4e/black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266", size = 1519946, upload-time = "2023-03-29T01:28:40.746Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d7/85f3d79f9e543402de2244c4d117793f262149e404ea0168841613c33e07/black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab", size = 1654176, upload-time = "2023-03-29T01:11:21.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/273d610249f0335afb1ddb03664a03223f4826e3d1a95170a0142cb19fb4/black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb", size = 1286299, upload-time = "2023-03-29T01:15:28.898Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e7/4642b7f462381799393fbad894ba4b32db00870a797f0616c197b07129a9/black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4", size = 180965, upload-time = "2023-03-29T01:00:52.253Z" }, +] + +[[package]] +name = "black" +version = "24.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "mypy-extensions", version = "1.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "packaging", version = "26.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pathspec", version = "0.12.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "platformdirs", version = "4.3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/b0/46fb0d4e00372f4a86a6f8efa3cb193c9f64863615e39010b1477e010578/black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f", size = 644810, upload-time = "2024-08-02T17:43:18.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/6e/74e29edf1fba3887ed7066930a87f698ffdcd52c5dbc263eabb06061672d/black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6", size = 1632092, upload-time = "2024-08-02T17:47:26.911Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/575cb6c3faee690b05c9d11ee2e8dba8fbd6d6c134496e644c1feb1b47da/black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb", size = 1457529, upload-time = "2024-08-02T17:47:29.109Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/d34099e95c437b53d01c4aa37cf93944b233066eb034ccf7897fa4e5f286/black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42", size = 1757443, upload-time = "2024-08-02T17:46:20.306Z" }, + { url = "https://files.pythonhosted.org/packages/87/a0/6d2e4175ef364b8c4b64f8441ba041ed65c63ea1db2720d61494ac711c15/black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a", size = 1418012, upload-time = "2024-08-02T17:47:20.33Z" }, + { url = "https://files.pythonhosted.org/packages/08/a6/0a3aa89de9c283556146dc6dbda20cd63a9c94160a6fbdebaf0918e4a3e1/black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1", size = 1615080, upload-time = "2024-08-02T17:48:05.467Z" }, + { url = "https://files.pythonhosted.org/packages/db/94/b803d810e14588bb297e565821a947c108390a079e21dbdcb9ab6956cd7a/black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af", size = 1438143, upload-time = "2024-08-02T17:47:30.247Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b5/f485e1bbe31f768e2e5210f52ea3f432256201289fd1a3c0afda693776b0/black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4", size = 1738774, upload-time = "2024-08-02T17:46:17.837Z" }, + { url = "https://files.pythonhosted.org/packages/a8/69/a000fc3736f89d1bdc7f4a879f8aaf516fb03613bb51a0154070383d95d9/black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af", size = 1427503, upload-time = "2024-08-02T17:46:22.654Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a8/05fb14195cfef32b7c8d4585a44b7499c2a4b205e1662c427b941ed87054/black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368", size = 1646132, upload-time = "2024-08-02T17:49:52.843Z" }, + { url = "https://files.pythonhosted.org/packages/41/77/8d9ce42673e5cb9988f6df73c1c5c1d4e9e788053cccd7f5fb14ef100982/black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed", size = 1448665, upload-time = "2024-08-02T17:47:54.479Z" }, + { url = "https://files.pythonhosted.org/packages/cc/94/eff1ddad2ce1d3cc26c162b3693043c6b6b575f538f602f26fe846dfdc75/black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018", size = 1762458, upload-time = "2024-08-02T17:46:19.384Z" }, + { url = "https://files.pythonhosted.org/packages/28/ea/18b8d86a9ca19a6942e4e16759b2fa5fc02bbc0eb33c1b866fcd387640ab/black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2", size = 1436109, upload-time = "2024-08-02T17:46:52.97Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d4/ae03761ddecc1a37d7e743b89cccbcf3317479ff4b88cfd8818079f890d0/black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd", size = 1617322, upload-time = "2024-08-02T17:51:20.203Z" }, + { url = "https://files.pythonhosted.org/packages/14/4b/4dfe67eed7f9b1ddca2ec8e4418ea74f0d1dc84d36ea874d618ffa1af7d4/black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2", size = 1442108, upload-time = "2024-08-02T17:50:40.824Z" }, + { url = "https://files.pythonhosted.org/packages/97/14/95b3f91f857034686cae0e73006b8391d76a8142d339b42970eaaf0416ea/black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e", size = 1745786, upload-time = "2024-08-02T17:46:02.939Z" }, + { url = "https://files.pythonhosted.org/packages/95/54/68b8883c8aa258a6dde958cd5bdfada8382bec47c5162f4a01e66d839af1/black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920", size = 1426754, upload-time = "2024-08-02T17:46:38.603Z" }, + { url = "https://files.pythonhosted.org/packages/13/b2/b3f24fdbb46f0e7ef6238e131f13572ee8279b70f237f221dd168a9dba1a/black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c", size = 1631706, upload-time = "2024-08-02T17:49:57.606Z" }, + { url = "https://files.pythonhosted.org/packages/d9/35/31010981e4a05202a84a3116423970fd1a59d2eda4ac0b3570fbb7029ddc/black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e", size = 1457429, upload-time = "2024-08-02T17:49:12.764Z" }, + { url = "https://files.pythonhosted.org/packages/27/25/3f706b4f044dd569a20a4835c3b733dedea38d83d2ee0beb8178a6d44945/black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47", size = 1756488, upload-time = "2024-08-02T17:46:08.067Z" }, + { url = "https://files.pythonhosted.org/packages/63/72/79375cd8277cbf1c5670914e6bd4c1b15dea2c8f8e906dc21c448d0535f0/black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb", size = 1417721, upload-time = "2024-08-02T17:46:42.637Z" }, + { url = "https://files.pythonhosted.org/packages/27/1e/83fa8a787180e1632c3d831f7e58994d7aaf23a0961320d21e84f922f919/black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed", size = 206504, upload-time = "2024-08-02T17:43:15.747Z" }, +] + +[[package]] +name = "black" +version = "25.11.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "mypy-extensions", version = "1.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "packaging", version = "26.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pathspec", version = "1.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pytokens", marker = "python_full_version == '3.9.*'" }, + { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] +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/b3/d2/6caccbc96f9311e8ec3378c296d4f4809429c43a6cd2394e3c390e86816d/black-25.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec311e22458eec32a807f029b2646f661e6859c3f61bc6d9ffb67958779f392e", size = 1743501, upload-time = "2025-11-10T01:59:06.202Z" }, + { url = "https://files.pythonhosted.org/packages/69/35/b986d57828b3f3dccbf922e2864223197ba32e74c5004264b1c62bc9f04d/black-25.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1032639c90208c15711334d681de2e24821af0575573db2810b0763bcd62e0f0", size = 1597308, upload-time = "2025-11-10T01:57:58.633Z" }, + { url = "https://files.pythonhosted.org/packages/39/8e/8b58ef4b37073f52b64a7b2dd8c9a96c84f45d6f47d878d0aa557e9a2d35/black-25.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0f7c461df55cf32929b002335883946a4893d759f2df343389c4396f3b6b37", size = 1656194, upload-time = "2025-11-10T01:57:10.909Z" }, + { url = "https://files.pythonhosted.org/packages/8d/30/9c2267a7955ecc545306534ab88923769a979ac20a27cf618d370091e5dd/black-25.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:f9786c24d8e9bd5f20dc7a7f0cdd742644656987f6ea6947629306f937726c03", size = 1347996, upload-time = "2025-11-10T01:57:22.391Z" }, + { url = "https://files.pythonhosted.org/packages/c4/62/d304786b75ab0c530b833a89ce7d997924579fb7484ecd9266394903e394/black-25.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:895571922a35434a9d8ca67ef926da6bc9ad464522a5fe0db99b394ef1c0675a", size = 1727891, upload-time = "2025-11-10T02:01:40.507Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/ffe8a006aa522c9e3f430e7b93568a7b2163f4b3f16e8feb6d8c3552761a/black-25.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb4f4b65d717062191bdec8e4a442539a8ea065e6af1c4f4d36f0cdb5f71e170", size = 1581875, upload-time = "2025-11-10T01:57:51.192Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7c8bda3108d0bb57387ac41b4abb5c08782b26da9f9c4421ef6694dac01a/black-25.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d81a44cbc7e4f73a9d6ae449ec2317ad81512d1e7dce7d57f6333fd6259737bc", size = 1642716, upload-time = "2025-11-10T01:56:51.589Z" }, + { url = "https://files.pythonhosted.org/packages/34/b9/f17dea34eecb7cc2609a89627d480fb6caea7b86190708eaa7eb15ed25e7/black-25.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:7eebd4744dfe92ef1ee349dc532defbf012a88b087bb7ddd688ff59a447b080e", size = 1352904, upload-time = "2025-11-10T01:59:26.252Z" }, + { 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/d5/9a/5b2c0e3215fe748fcf515c2dd34658973a1210bf610e24de5ba887e4f1c8/black-25.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3bb5ce32daa9ff0605d73b6f19da0b0e6c1f8f2d75594db539fdfed722f2b06", size = 1743063, upload-time = "2025-11-10T02:02:43.175Z" }, + { url = "https://files.pythonhosted.org/packages/a1/20/245164c6efc27333409c62ba54dcbfbe866c6d1957c9a6c0647786e950da/black-25.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9815ccee1e55717fe9a4b924cae1646ef7f54e0f990da39a34fc7b264fcf80a2", size = 1596867, upload-time = "2025-11-10T02:00:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6f/1a3859a7da205f3d50cf3a8bec6bdc551a91c33ae77a045bb24c1f46ab54/black-25.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92285c37b93a1698dcbc34581867b480f1ba3a7b92acf1fe0467b04d7a4da0dc", size = 1655678, upload-time = "2025-11-10T01:57:09.028Z" }, + { url = "https://files.pythonhosted.org/packages/56/1a/6dec1aeb7be90753d4fcc273e69bc18bfd34b353223ed191da33f7519410/black-25.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:43945853a31099c7c0ff8dface53b4de56c41294fa6783c0441a8b1d9bf668bc", size = 1347452, upload-time = "2025-11-10T01:57:01.871Z" }, + { 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 = "black" +version = "26.5.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "click", version = "8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mypy-extensions", version = "1.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", version = "26.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pathspec", version = "1.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "platformdirs", version = "4.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytokens", marker = "python_full_version >= '3.10'" }, + { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/37/5628dd55bf2b34257fc7603f0fe97c40e3aaf24265f416a9c85c95ca1436/black-26.5.1.tar.gz", hash = "sha256:dd321f668053961824bcc1be1cc1df748b2d7e4fa28086b08331e577b0100a73", size = 679439, upload-time = "2026-05-18T16:53:36.107Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/84/b3f55026206a9e8820a91503308075ca48eadc515e436731ca01dbe043b3/black-26.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9942db8888e06943c5dde66ca0037dcff82a2a4ec1ad0ada9e0d2ee9d9823893", size = 1987719, upload-time = "2026-05-18T17:05:02.757Z" }, + { url = "https://files.pythonhosted.org/packages/c6/34/7db312c5e5783d6e76cffd9d5ac8972a32badae4c6e3288dac0eed8d3bed/black-26.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:89c93167a74d3a75dfaa38a5c7cca015537d5820dd7f17d63267d674a61cae90", size = 1810083, upload-time = "2026-05-18T17:05:04.302Z" }, + { url = "https://files.pythonhosted.org/packages/33/e2/e0101e73c2c8727634e2efcb35e2b34bd23ad70dfa673789f5773a591b21/black-26.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22f2cd76d069cc54c71f10360744ba8983fbb616903b4304a85b734915c8e1b4", size = 1860633, upload-time = "2026-05-18T17:05:06.391Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4c/e15c0c5b23cf3651035fe5addcce90e283af3548a3f91bb03d81b83106ab/black-26.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:87ed5c6f450580a2f6790bc7cbfb016dfc73bc750249762268a3695361315eef", size = 1477886, upload-time = "2026-05-18T17:05:07.96Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3f/59d43ade98d2ce5c8dc34a4e46cbecd177e6d55d7d4092969c6003ccc655/black-26.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:58b4bd92cf88aacf83d88479c8f9caee044b1ec55f2451a337354a7ea2590a22", size = 1277111, upload-time = "2026-05-18T17:05:09.473Z" }, + { url = "https://files.pythonhosted.org/packages/4b/96/3c3e09f09f44a37aac36b178a279cd19aa7001bd796187a7b162a294c81f/black-26.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96ae2c733b2aabdd9986e2c5df628ff3473676cd1c5faded1ff496cf6d74083c", size = 1970639, upload-time = "2026-05-18T17:05:11.461Z" }, + { url = "https://files.pythonhosted.org/packages/83/ea/5ad117b9ee3ecd933c712bcbae610006e5b7cc9f41c526cd7ed3b6c4124c/black-26.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e48b87e03bf109288e55cfceadcfa15ff5470aca2851a851950ed2926f450d7", size = 1792130, upload-time = "2026-05-18T17:05:12.983Z" }, + { url = "https://files.pythonhosted.org/packages/06/3a/7c448bc623fcdfa96672531beb5a616ea5e64f6975955254d7731ffb0ad9/black-26.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5119fa92ae61f786e8c3662fd60aece1d0a2dd5cca5d0c79417a95e7a4272a59", size = 1846134, upload-time = "2026-05-18T17:05:14.506Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5b/0b39b3a5917f0657ac014ad2edb58c139553a478adfe7f817abf1622ff6e/black-26.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:30d3c14661f2792e9142cce3eeeb1cbc175b3eb5f733be0c8eeb99651e52b0c3", size = 1478883, upload-time = "2026-05-18T17:05:16.542Z" }, + { url = "https://files.pythonhosted.org/packages/4c/48/dc222692e0f95030db1bbfb6c857e76858bad09058221ea7aae815255327/black-26.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:1ef92b76f7733f282fd096ea406200b5a286c42947412b0eaff3a74e3616cefe", size = 1277776, upload-time = "2026-05-18T17:05:18.029Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/7744b906703228264ef73bdd534df88ec1ef3de45c4e78f6d31b9e32d0c9/black-26.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4ad6fa01f941920f54f2bbb35f3df7673428a0ef98a0b0840c2eaef3b110efa8", size = 2012518, upload-time = "2026-05-18T17:05:20.108Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c0/c5a3b1636dfd09c42534f2b3cf33506814f6d3e066fb0879ffa16c1ae860/black-26.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3915f256e75a2d7cf88d8953d37f780455dc586cc72dee059c528fe77f581217", size = 1816016, upload-time = "2026-05-18T17:05:21.84Z" }, + { url = "https://files.pythonhosted.org/packages/1f/0e/36044316b65ca471d3bb6d3703fd06fb50c6b727c3562f6a5a3153634f88/black-26.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d98d4137277c75dfb898ec8d846c4fd68ba1e9cf77f95e2865c203dc18f4c3d", size = 1884150, upload-time = "2026-05-18T17:05:23.546Z" }, + { url = "https://files.pythonhosted.org/packages/b3/33/dafc5808c2af43672912111d7c3354af1615f7e2be3bed7a878461abbe4d/black-26.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:a1dca32d9f1784af512a13410ec204c6f7f0aa9797a111c42e1c03449821c264", size = 1486825, upload-time = "2026-05-18T17:05:25.004Z" }, + { url = "https://files.pythonhosted.org/packages/82/14/b965ee6ad2a311f28bdbf692def3ee9848d2ae289dab28b27657fcee3e78/black-26.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1037d5ac7b7b310b2632ad867ec8d0e4c4819dcdb0b820f63135da746a24e418", size = 1288646, upload-time = "2026-05-18T17:05:26.477Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5c/c384363980e11e25ca6b93205949bb331fbf35f4e0dbec376dfa6326cec8/black-26.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b36cf2ddf5566e205f6535f782a62194a184d33e175b64ae8c40b1737522be3", size = 2009020, upload-time = "2026-05-18T17:05:28.132Z" }, + { url = "https://files.pythonhosted.org/packages/0b/df/9f31c5e0babbfed77d505fc5d120beb98b21b33feaeded3924ea941fe360/black-26.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f7ea64ebfa01b50f693508fc39f875e264446d3b097088f84f203b9d09618a0", size = 1813335, upload-time = "2026-05-18T17:05:31.266Z" }, + { url = "https://files.pythonhosted.org/packages/fb/24/8e7b9a2fa61b0afd82209efe937557d180a1fa055bd7f6161eb9defc3719/black-26.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecb3e624844c798144e9bd986954e0adc81d8911a1f30f375e1252fe26e8c294", size = 1881614, upload-time = "2026-05-18T17:05:32.718Z" }, + { url = "https://files.pythonhosted.org/packages/49/ad/b4e0d9365ba8ac34f6bbab62a4b1b2dd5d618fac3fa1b8db968c844201b5/black-26.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:e1a26503279b6b310669fb0b219c39e4820b77e8189fe80f522bb511f247db0a", size = 1488925, upload-time = "2026-05-18T17:05:34.259Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4b/652b859bf5df88a751c30451b09338f7fd26a77d1271c666992f836b7711/black-26.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c34b25da232ead53a6f335b76dbea124f4d152ad568b9080d6f944bc2b34b52", size = 1289883, upload-time = "2026-05-18T17:05:36.019Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a8da8eb208c51c7f4ce74609a45d0dcc6d8a2141e45e81ee5289d1bb0d59/black-26.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e88976690a64b0af98312ca958415849cb42423423c5f2ee74af4b49a97a2168", size = 2004800, upload-time = "2026-05-18T17:05:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/11/8a/a479296a19e383b70a725882a6cf3d786540601ff03cabbaaf1cce864c5a/black-26.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32d5ea7f6c8bdfa6e648326ebca1f02b0764e2a029edc6f8dce2627e19d468c3", size = 1815576, upload-time = "2026-05-18T17:05:40.309Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/cfaf3d39f25132c156a068f6b805576c9103a84086019507c70e1911ee7d/black-26.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea8d16dc41655aa113cd64665e7219446cd7e4ff2248d7178eaa905190c86b18", size = 1877927, upload-time = "2026-05-18T17:05:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/66/76/302e313964bcff7e28df329d39f84f5270095730d85ff0acc260610a0d82/black-26.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:577f21094ea469ef92ec1adaf2c9441a226d2144d01a5be2fa823cecf6543e50", size = 1511860, upload-time = "2026-05-18T17:05:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/27/4e/a3827e35e0e567f9f9ee59e2a0ab979267dca98718f25547ca8c6733afd4/black-26.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:ed1a20af114c301a0269bf01163d51dbef72737fd65f850001e7cbe7f3c7abae", size = 1316632, upload-time = "2026-05-18T17:05:45.521Z" }, + { url = "https://files.pythonhosted.org/packages/94/51/f975cae76d44274cc2868dc9040ac5d58d464784610234455b4e7b19c6ef/black-26.5.1-py3-none-any.whl", hash = "sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2", size = 213693, upload-time = "2026-05-18T16:53:33.964Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", + "python_full_version < '3.8'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "click" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, +] + +[[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 = "coverage" +version = "7.2.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/45/8b/421f30467e69ac0e414214856798d4bc32da1336df745e49e49ae5c1e2a8/coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59", size = 762575, upload-time = "2023-05-29T20:08:50.273Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/24/be01e62a7bce89bcffe04729c540382caa5a06bee45ae42136c93e2499f5/coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8", size = 200724, upload-time = "2023-05-29T20:07:03.422Z" }, + { url = "https://files.pythonhosted.org/packages/3d/80/7060a445e1d2c9744b683dc935248613355657809d6c6b2716cdf4ca4766/coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb", size = 201024, upload-time = "2023-05-29T20:07:05.694Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9d/926fce7e03dbfc653104c2d981c0fa71f0572a9ebd344d24c573bd6f7c4f/coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6", size = 229528, upload-time = "2023-05-29T20:07:07.307Z" }, + { url = "https://files.pythonhosted.org/packages/d1/3a/67f5d18f911abf96857f6f7e4df37ca840e38179e2cc9ab6c0b9c3380f19/coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2", size = 227842, upload-time = "2023-05-29T20:07:09.331Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bd/1b2331e3a04f4cc9b7b332b1dd0f3a1261dfc4114f8479bebfcc2afee9e8/coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063", size = 228717, upload-time = "2023-05-29T20:07:11.38Z" }, + { url = "https://files.pythonhosted.org/packages/2b/86/3dbf9be43f8bf6a5ca28790a713e18902b2d884bc5fa9512823a81dff601/coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1", size = 234632, upload-time = "2023-05-29T20:07:13.376Z" }, + { url = "https://files.pythonhosted.org/packages/91/e8/469ed808a782b9e8305a08bad8c6fa5f8e73e093bda6546c5aec68275bff/coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353", size = 232875, upload-time = "2023-05-29T20:07:15.093Z" }, + { url = "https://files.pythonhosted.org/packages/29/8f/4fad1c2ba98104425009efd7eaa19af9a7c797e92d40cd2ec026fa1f58cb/coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495", size = 234094, upload-time = "2023-05-29T20:07:17.013Z" }, + { url = "https://files.pythonhosted.org/packages/94/4e/d4e46a214ae857be3d7dc5de248ba43765f60daeb1ab077cb6c1536c7fba/coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818", size = 203184, upload-time = "2023-05-29T20:07:18.69Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e9/d6730247d8dec2a3dddc520ebe11e2e860f0f98cee3639e23de6cf920255/coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850", size = 204096, upload-time = "2023-05-29T20:07:20.153Z" }, + { url = "https://files.pythonhosted.org/packages/c6/fa/529f55c9a1029c840bcc9109d5a15ff00478b7ff550a1ae361f8745f8ad5/coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f", size = 200895, upload-time = "2023-05-29T20:07:21.963Z" }, + { url = "https://files.pythonhosted.org/packages/67/d7/cd8fe689b5743fffac516597a1222834c42b80686b99f5b44ef43ccc2a43/coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe", size = 201120, upload-time = "2023-05-29T20:07:23.765Z" }, + { url = "https://files.pythonhosted.org/packages/8c/95/16eed713202406ca0a37f8ac259bbf144c9d24f9b8097a8e6ead61da2dbb/coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3", size = 233178, upload-time = "2023-05-29T20:07:25.281Z" }, + { url = "https://files.pythonhosted.org/packages/c1/49/4d487e2ad5d54ed82ac1101e467e8994c09d6123c91b2a962145f3d262c2/coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f", size = 230754, upload-time = "2023-05-29T20:07:27.044Z" }, + { url = "https://files.pythonhosted.org/packages/a7/cd/3ce94ad9d407a052dc2a74fbeb1c7947f442155b28264eb467ee78dea812/coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb", size = 232558, upload-time = "2023-05-29T20:07:28.743Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a8/12cc7b261f3082cc299ab61f677f7e48d93e35ca5c3c2f7241ed5525ccea/coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833", size = 241509, upload-time = "2023-05-29T20:07:30.434Z" }, + { url = "https://files.pythonhosted.org/packages/04/fa/43b55101f75a5e9115259e8be70ff9279921cb6b17f04c34a5702ff9b1f7/coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97", size = 239924, upload-time = "2023-05-29T20:07:32.065Z" }, + { url = "https://files.pythonhosted.org/packages/68/5f/d2bd0f02aa3c3e0311986e625ccf97fdc511b52f4f1a063e4f37b624772f/coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a", size = 240977, upload-time = "2023-05-29T20:07:34.184Z" }, + { url = "https://files.pythonhosted.org/packages/ba/92/69c0722882643df4257ecc5437b83f4c17ba9e67f15dc6b77bad89b6982e/coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a", size = 203168, upload-time = "2023-05-29T20:07:35.869Z" }, + { url = "https://files.pythonhosted.org/packages/b1/96/c12ed0dfd4ec587f3739f53eb677b9007853fd486ccb0e7d5512a27bab2e/coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562", size = 204185, upload-time = "2023-05-29T20:07:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d5/52fa1891d1802ab2e1b346d37d349cb41cdd4fd03f724ebbf94e80577687/coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4", size = 201020, upload-time = "2023-05-29T20:07:38.724Z" }, + { url = "https://files.pythonhosted.org/packages/24/df/6765898d54ea20e3197a26d26bb65b084deefadd77ce7de946b9c96dfdc5/coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4", size = 233994, upload-time = "2023-05-29T20:07:40.274Z" }, + { url = "https://files.pythonhosted.org/packages/15/81/b108a60bc758b448c151e5abceed027ed77a9523ecbc6b8a390938301841/coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01", size = 231358, upload-time = "2023-05-29T20:07:41.998Z" }, + { url = "https://files.pythonhosted.org/packages/61/90/c76b9462f39897ebd8714faf21bc985b65c4e1ea6dff428ea9dc711ed0dd/coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6", size = 233316, upload-time = "2023-05-29T20:07:43.539Z" }, + { url = "https://files.pythonhosted.org/packages/04/d6/8cba3bf346e8b1a4fb3f084df7d8cea25a6b6c56aaca1f2e53829be17e9e/coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d", size = 240159, upload-time = "2023-05-29T20:07:44.982Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ea/4a252dc77ca0605b23d477729d139915e753ee89e4c9507630e12ad64a80/coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de", size = 238127, upload-time = "2023-05-29T20:07:46.522Z" }, + { url = "https://files.pythonhosted.org/packages/9f/5c/d9760ac497c41f9c4841f5972d0edf05d50cad7814e86ee7d133ec4a0ac8/coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d", size = 239833, upload-time = "2023-05-29T20:07:47.992Z" }, + { url = "https://files.pythonhosted.org/packages/69/8c/26a95b08059db1cbb01e4b0e6d40f2e9debb628c6ca86b78f625ceaf9bab/coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511", size = 203463, upload-time = "2023-05-29T20:07:49.939Z" }, + { url = "https://files.pythonhosted.org/packages/b7/00/14b00a0748e9eda26e97be07a63cc911108844004687321ddcc213be956c/coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3", size = 204347, upload-time = "2023-05-29T20:07:51.909Z" }, + { url = "https://files.pythonhosted.org/packages/80/d7/67937c80b8fd4c909fdac29292bc8b35d9505312cff6bcab41c53c5b1df6/coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f", size = 200580, upload-time = "2023-05-29T20:07:54.076Z" }, + { url = "https://files.pythonhosted.org/packages/7a/05/084864fa4bbf8106f44fb72a56e67e0cd372d3bf9d893be818338c81af5d/coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb", size = 226237, upload-time = "2023-05-29T20:07:56.28Z" }, + { url = "https://files.pythonhosted.org/packages/67/a2/6fa66a50e6e894286d79a3564f42bd54a9bd27049dc0a63b26d9924f0aa3/coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9", size = 224256, upload-time = "2023-05-29T20:07:58.189Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c0/73f139794c742840b9ab88e2e17fe14a3d4668a166ff95d812ac66c0829d/coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd", size = 225550, upload-time = "2023-05-29T20:08:00.383Z" }, + { url = "https://files.pythonhosted.org/packages/03/ec/6f30b4e0c96ce03b0e64aec46b4af2a8c49b70d1b5d0d69577add757b946/coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a", size = 232440, upload-time = "2023-05-29T20:08:02.495Z" }, + { url = "https://files.pythonhosted.org/packages/22/c1/2f6c1b6f01a0996c9e067a9c780e1824351dbe17faae54388a4477e6d86f/coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959", size = 230897, upload-time = "2023-05-29T20:08:04.382Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d6/53e999ec1bf7498ca4bc5f3b8227eb61db39068d2de5dcc359dec5601b5a/coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02", size = 232024, upload-time = "2023-05-29T20:08:06.031Z" }, + { url = "https://files.pythonhosted.org/packages/e9/40/383305500d24122dbed73e505a4d6828f8f3356d1f68ab6d32c781754b81/coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f", size = 203293, upload-time = "2023-05-29T20:08:07.598Z" }, + { url = "https://files.pythonhosted.org/packages/0e/bc/7e3a31534fabb043269f14fb64e2bb2733f85d4cf39e5bbc71357c57553a/coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0", size = 204040, upload-time = "2023-05-29T20:08:09.919Z" }, + { url = "https://files.pythonhosted.org/packages/c6/fc/be19131010930a6cf271da48202c8cc1d3f971f68c02fb2d3a78247f43dc/coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5", size = 200689, upload-time = "2023-05-29T20:08:11.594Z" }, + { url = "https://files.pythonhosted.org/packages/28/d7/9a8de57d87f4bbc6f9a6a5ded1eaac88a89bf71369bb935dac3c0cf2893e/coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5", size = 200986, upload-time = "2023-05-29T20:08:13.228Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e4/e6182e4697665fb594a7f4e4f27cb3a4dd00c2e3d35c5c706765de8c7866/coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9", size = 230648, upload-time = "2023-05-29T20:08:15.11Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e3/f552d5871943f747165b92a924055c5d6daa164ae659a13f9018e22f3990/coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6", size = 228511, upload-time = "2023-05-29T20:08:16.877Z" }, + { url = "https://files.pythonhosted.org/packages/44/55/49f65ccdd4dfd6d5528e966b28c37caec64170c725af32ab312889d2f857/coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e", size = 229852, upload-time = "2023-05-29T20:08:18.47Z" }, + { url = "https://files.pythonhosted.org/packages/0d/31/340428c238eb506feb96d4fb5c9ea614db1149517f22cc7ab8c6035ef6d9/coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050", size = 235578, upload-time = "2023-05-29T20:08:20.298Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ce/97c1dd6592c908425622fe7f31c017d11cf0421729b09101d4de75bcadc8/coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5", size = 234079, upload-time = "2023-05-29T20:08:22.365Z" }, + { url = "https://files.pythonhosted.org/packages/de/a3/5a98dc9e239d0dc5f243ef5053d5b1bdcaa1dee27a691dfc12befeccf878/coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f", size = 234991, upload-time = "2023-05-29T20:08:24.974Z" }, + { url = "https://files.pythonhosted.org/packages/4a/fb/78986d3022e5ccf2d4370bc43a5fef8374f092b3c21d32499dee8e30b7b6/coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e", size = 203160, upload-time = "2023-05-29T20:08:26.701Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1c/6b3c9c363fb1433c79128e0d692863deb761b1b78162494abb9e5c328bc0/coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c", size = 204085, upload-time = "2023-05-29T20:08:28.146Z" }, + { url = "https://files.pythonhosted.org/packages/88/da/495944ebf0ad246235a6bd523810d9f81981f9b81c6059ba1f56e943abe0/coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9", size = 200725, upload-time = "2023-05-29T20:08:29.851Z" }, + { url = "https://files.pythonhosted.org/packages/ca/0c/3dfeeb1006c44b911ee0ed915350db30325d01808525ae7cc8d57643a2ce/coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2", size = 201022, upload-time = "2023-05-29T20:08:31.429Z" }, + { url = "https://files.pythonhosted.org/packages/61/af/5964b8d7d9a5c767785644d9a5a63cacba9a9c45cc42ba06d25895ec87be/coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7", size = 229102, upload-time = "2023-05-29T20:08:32.982Z" }, + { url = "https://files.pythonhosted.org/packages/d9/1d/cd467fceb62c371f9adb1d739c92a05d4e550246daa90412e711226bd320/coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e", size = 227441, upload-time = "2023-05-29T20:08:35.044Z" }, + { url = "https://files.pythonhosted.org/packages/fe/57/e4f8ad64d84ca9e759d783a052795f62a9f9111585e46068845b1cb52c2b/coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1", size = 228265, upload-time = "2023-05-29T20:08:36.861Z" }, + { url = "https://files.pythonhosted.org/packages/88/8b/b0d9fe727acae907fa7f1c8194ccb6fe9d02e1c3e9001ecf74c741f86110/coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9", size = 234217, upload-time = "2023-05-29T20:08:38.837Z" }, + { url = "https://files.pythonhosted.org/packages/66/2e/c99fe1f6396d93551aa352c75410686e726cd4ea104479b9af1af22367ce/coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250", size = 232466, upload-time = "2023-05-29T20:08:40.768Z" }, + { url = "https://files.pythonhosted.org/packages/bb/e9/88747b40c8fb4a783b40222510ce6d66170217eb05d7f46462c36b4fa8cc/coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2", size = 233669, upload-time = "2023-05-29T20:08:42.944Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d5/a8e276bc005e42114468d4fe03e0a9555786bc51cbfe0d20827a46c1565a/coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb", size = 203199, upload-time = "2023-05-29T20:08:44.734Z" }, + { url = "https://files.pythonhosted.org/packages/a9/0c/4a848ae663b47f1195abcb09a951751dd61f80b503303b9b9d768e0fd321/coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27", size = 204109, upload-time = "2023-05-29T20:08:46.417Z" }, + { url = "https://files.pythonhosted.org/packages/67/fb/b3b1d7887e1ea25a9608b0776e480e4bbc303ca95a31fd585555ec4fff5a/coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d", size = 193207, upload-time = "2023-05-29T20:08:48.153Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, +] + +[[package]] +name = "coverage" +version = "7.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791, upload-time = "2024-08-04T19:45:30.9Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/61/eb7ce5ed62bacf21beca4937a90fe32545c91a3c8a42a30c6616d48fc70d/coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", size = 206690, upload-time = "2024-08-04T19:43:07.695Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/041928e434442bd3afde5584bdc3f932fb4562b1597629f537387cec6f3d/coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", size = 207127, upload-time = "2024-08-04T19:43:10.15Z" }, + { url = "https://files.pythonhosted.org/packages/c7/c8/6ca52b5147828e45ad0242388477fdb90df2c6cbb9a441701a12b3c71bc8/coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", size = 235654, upload-time = "2024-08-04T19:43:12.405Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/9ac2b62557f4340270942011d6efeab9833648380109e897d48ab7c1035d/coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc", size = 233598, upload-time = "2024-08-04T19:43:14.078Z" }, + { url = "https://files.pythonhosted.org/packages/53/23/9e2c114d0178abc42b6d8d5281f651a8e6519abfa0ef460a00a91f80879d/coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", size = 234732, upload-time = "2024-08-04T19:43:16.632Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7e/a0230756fb133343a52716e8b855045f13342b70e48e8ad41d8a0d60ab98/coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", size = 233816, upload-time = "2024-08-04T19:43:19.049Z" }, + { url = "https://files.pythonhosted.org/packages/28/7c/3753c8b40d232b1e5eeaed798c875537cf3cb183fb5041017c1fdb7ec14e/coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", size = 232325, upload-time = "2024-08-04T19:43:21.246Z" }, + { url = "https://files.pythonhosted.org/packages/57/e3/818a2b2af5b7573b4b82cf3e9f137ab158c90ea750a8f053716a32f20f06/coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", size = 233418, upload-time = "2024-08-04T19:43:22.945Z" }, + { url = "https://files.pythonhosted.org/packages/c8/fb/4532b0b0cefb3f06d201648715e03b0feb822907edab3935112b61b885e2/coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", size = 209343, upload-time = "2024-08-04T19:43:25.121Z" }, + { url = "https://files.pythonhosted.org/packages/5a/25/af337cc7421eca1c187cc9c315f0a755d48e755d2853715bfe8c418a45fa/coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", size = 210136, upload-time = "2024-08-04T19:43:26.851Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5f/67af7d60d7e8ce61a4e2ddcd1bd5fb787180c8d0ae0fbd073f903b3dd95d/coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", size = 206796, upload-time = "2024-08-04T19:43:29.115Z" }, + { url = "https://files.pythonhosted.org/packages/e1/0e/e52332389e057daa2e03be1fbfef25bb4d626b37d12ed42ae6281d0a274c/coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", size = 207244, upload-time = "2024-08-04T19:43:31.285Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cd/766b45fb6e090f20f8927d9c7cb34237d41c73a939358bc881883fd3a40d/coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", size = 239279, upload-time = "2024-08-04T19:43:33.581Z" }, + { url = "https://files.pythonhosted.org/packages/70/6c/a9ccd6fe50ddaf13442a1e2dd519ca805cbe0f1fcd377fba6d8339b98ccb/coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", size = 236859, upload-time = "2024-08-04T19:43:35.301Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/8351b465febb4dbc1ca9929505202db909c5a635c6fdf33e089bbc3d7d85/coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", size = 238549, upload-time = "2024-08-04T19:43:37.578Z" }, + { url = "https://files.pythonhosted.org/packages/68/3c/289b81fa18ad72138e6d78c4c11a82b5378a312c0e467e2f6b495c260907/coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", size = 237477, upload-time = "2024-08-04T19:43:39.92Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1c/aa1efa6459d822bd72c4abc0b9418cf268de3f60eeccd65dc4988553bd8d/coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", size = 236134, upload-time = "2024-08-04T19:43:41.453Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c8/521c698f2d2796565fe9c789c2ee1ccdae610b3aa20b9b2ef980cc253640/coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", size = 236910, upload-time = "2024-08-04T19:43:43.037Z" }, + { url = "https://files.pythonhosted.org/packages/7d/30/033e663399ff17dca90d793ee8a2ea2890e7fdf085da58d82468b4220bf7/coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", size = 209348, upload-time = "2024-08-04T19:43:44.787Z" }, + { url = "https://files.pythonhosted.org/packages/20/05/0d1ccbb52727ccdadaa3ff37e4d2dc1cd4d47f0c3df9eb58d9ec8508ca88/coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", size = 210230, upload-time = "2024-08-04T19:43:46.707Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d4/300fc921dff243cd518c7db3a4c614b7e4b2431b0d1145c1e274fd99bd70/coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", size = 206983, upload-time = "2024-08-04T19:43:49.082Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", size = 207221, upload-time = "2024-08-04T19:43:52.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/2ead05e735022d1a7f3a0a683ac7f737de14850395a826192f0288703472/coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", size = 240342, upload-time = "2024-08-04T19:43:53.746Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ef/94043e478201ffa85b8ae2d2c79b4081e5a1b73438aafafccf3e9bafb6b5/coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", size = 237371, upload-time = "2024-08-04T19:43:55.993Z" }, + { url = "https://files.pythonhosted.org/packages/1f/0f/c890339dd605f3ebc269543247bdd43b703cce6825b5ed42ff5f2d6122c7/coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", size = 239455, upload-time = "2024-08-04T19:43:57.618Z" }, + { url = "https://files.pythonhosted.org/packages/d1/04/7fd7b39ec7372a04efb0f70c70e35857a99b6a9188b5205efb4c77d6a57a/coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", size = 238924, upload-time = "2024-08-04T19:44:00.012Z" }, + { url = "https://files.pythonhosted.org/packages/ed/bf/73ce346a9d32a09cf369f14d2a06651329c984e106f5992c89579d25b27e/coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", size = 237252, upload-time = "2024-08-04T19:44:01.713Z" }, + { url = "https://files.pythonhosted.org/packages/86/74/1dc7a20969725e917b1e07fe71a955eb34bc606b938316bcc799f228374b/coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", size = 238897, upload-time = "2024-08-04T19:44:03.898Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e9/d9cc3deceb361c491b81005c668578b0dfa51eed02cd081620e9a62f24ec/coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", size = 209606, upload-time = "2024-08-04T19:44:05.532Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/5a2e41922ea6740f77d555c4d47544acd7dc3f251fe14199c09c0f5958d3/coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", size = 210373, upload-time = "2024-08-04T19:44:07.079Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f9/9aa4dfb751cb01c949c990d136a0f92027fbcc5781c6e921df1cb1563f20/coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", size = 207007, upload-time = "2024-08-04T19:44:09.453Z" }, + { url = "https://files.pythonhosted.org/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", size = 207269, upload-time = "2024-08-04T19:44:11.045Z" }, + { url = "https://files.pythonhosted.org/packages/14/5b/9dec847b305e44a5634d0fb8498d135ab1d88330482b74065fcec0622224/coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", size = 239886, upload-time = "2024-08-04T19:44:12.83Z" }, + { url = "https://files.pythonhosted.org/packages/7b/b7/35760a67c168e29f454928f51f970342d23cf75a2bb0323e0f07334c85f3/coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", size = 237037, upload-time = "2024-08-04T19:44:15.393Z" }, + { url = "https://files.pythonhosted.org/packages/f7/95/d2fd31f1d638df806cae59d7daea5abf2b15b5234016a5ebb502c2f3f7ee/coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", size = 239038, upload-time = "2024-08-04T19:44:17.466Z" }, + { url = "https://files.pythonhosted.org/packages/6e/bd/110689ff5752b67924efd5e2aedf5190cbbe245fc81b8dec1abaffba619d/coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", size = 238690, upload-time = "2024-08-04T19:44:19.336Z" }, + { url = "https://files.pythonhosted.org/packages/d3/a8/08d7b38e6ff8df52331c83130d0ab92d9c9a8b5462f9e99c9f051a4ae206/coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", size = 236765, upload-time = "2024-08-04T19:44:20.994Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6a/9cf96839d3147d55ae713eb2d877f4d777e7dc5ba2bce227167d0118dfe8/coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", size = 238611, upload-time = "2024-08-04T19:44:22.616Z" }, + { url = "https://files.pythonhosted.org/packages/74/e4/7ff20d6a0b59eeaab40b3140a71e38cf52547ba21dbcf1d79c5a32bba61b/coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", size = 209671, upload-time = "2024-08-04T19:44:24.418Z" }, + { url = "https://files.pythonhosted.org/packages/35/59/1812f08a85b57c9fdb6d0b383d779e47b6f643bc278ed682859512517e83/coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", size = 210368, upload-time = "2024-08-04T19:44:26.276Z" }, + { url = "https://files.pythonhosted.org/packages/9c/15/08913be1c59d7562a3e39fce20661a98c0a3f59d5754312899acc6cb8a2d/coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", size = 207758, upload-time = "2024-08-04T19:44:29.028Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ae/b5d58dff26cade02ada6ca612a76447acd69dccdbb3a478e9e088eb3d4b9/coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", size = 208035, upload-time = "2024-08-04T19:44:30.673Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", size = 250839, upload-time = "2024-08-04T19:44:32.412Z" }, + { url = "https://files.pythonhosted.org/packages/7c/1e/c2967cb7991b112ba3766df0d9c21de46b476d103e32bb401b1b2adf3380/coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", size = 246569, upload-time = "2024-08-04T19:44:34.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", size = 248927, upload-time = "2024-08-04T19:44:36.313Z" }, + { url = "https://files.pythonhosted.org/packages/c8/fa/13a6f56d72b429f56ef612eb3bc5ce1b75b7ee12864b3bd12526ab794847/coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", size = 248401, upload-time = "2024-08-04T19:44:38.155Z" }, + { url = "https://files.pythonhosted.org/packages/75/06/0429c652aa0fb761fc60e8c6b291338c9173c6aa0f4e40e1902345b42830/coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", size = 246301, upload-time = "2024-08-04T19:44:39.883Z" }, + { url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598, upload-time = "2024-08-04T19:44:41.59Z" }, + { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307, upload-time = "2024-08-04T19:44:43.301Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453, upload-time = "2024-08-04T19:44:45.677Z" }, + { url = "https://files.pythonhosted.org/packages/81/d0/d9e3d554e38beea5a2e22178ddb16587dbcbe9a1ef3211f55733924bf7fa/coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", size = 206674, upload-time = "2024-08-04T19:44:47.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/ea/cab2dc248d9f45b2b7f9f1f596a4d75a435cb364437c61b51d2eb33ceb0e/coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", size = 207101, upload-time = "2024-08-04T19:44:49.32Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6f/f82f9a500c7c5722368978a5390c418d2a4d083ef955309a8748ecaa8920/coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", size = 236554, upload-time = "2024-08-04T19:44:51.631Z" }, + { url = "https://files.pythonhosted.org/packages/a6/94/d3055aa33d4e7e733d8fa309d9adf147b4b06a82c1346366fc15a2b1d5fa/coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3", size = 234440, upload-time = "2024-08-04T19:44:53.464Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6e/885bcd787d9dd674de4a7d8ec83faf729534c63d05d51d45d4fa168f7102/coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", size = 235889, upload-time = "2024-08-04T19:44:55.165Z" }, + { url = "https://files.pythonhosted.org/packages/f4/63/df50120a7744492710854860783d6819ff23e482dee15462c9a833cc428a/coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6", size = 235142, upload-time = "2024-08-04T19:44:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5d/9d0acfcded2b3e9ce1c7923ca52ccc00c78a74e112fc2aee661125b7843b/coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569", size = 233805, upload-time = "2024-08-04T19:44:59.033Z" }, + { url = "https://files.pythonhosted.org/packages/c4/56/50abf070cb3cd9b1dd32f2c88f083aab561ecbffbcd783275cb51c17f11d/coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989", size = 234655, upload-time = "2024-08-04T19:45:01.398Z" }, + { url = "https://files.pythonhosted.org/packages/25/ee/b4c246048b8485f85a2426ef4abab88e48c6e80c74e964bea5cd4cd4b115/coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7", size = 209296, upload-time = "2024-08-04T19:45:03.819Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1c/96cf86b70b69ea2b12924cdf7cabb8ad10e6130eab8d767a1099fbd2a44f/coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8", size = 210137, upload-time = "2024-08-04T19:45:06.25Z" }, + { url = "https://files.pythonhosted.org/packages/19/d3/d54c5aa83268779d54c86deb39c1c4566e5d45c155369ca152765f8db413/coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", size = 206688, upload-time = "2024-08-04T19:45:08.358Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fe/137d5dca72e4a258b1bc17bb04f2e0196898fe495843402ce826a7419fe3/coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", size = 207120, upload-time = "2024-08-04T19:45:11.526Z" }, + { url = "https://files.pythonhosted.org/packages/78/5b/a0a796983f3201ff5485323b225d7c8b74ce30c11f456017e23d8e8d1945/coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", size = 235249, upload-time = "2024-08-04T19:45:13.202Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e1/76089d6a5ef9d68f018f65411fcdaaeb0141b504587b901d74e8587606ad/coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", size = 233237, upload-time = "2024-08-04T19:45:14.961Z" }, + { url = "https://files.pythonhosted.org/packages/9a/6f/eef79b779a540326fee9520e5542a8b428cc3bfa8b7c8f1022c1ee4fc66c/coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", size = 234311, upload-time = "2024-08-04T19:45:16.924Z" }, + { url = "https://files.pythonhosted.org/packages/75/e1/656d65fb126c29a494ef964005702b012f3498db1a30dd562958e85a4049/coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", size = 233453, upload-time = "2024-08-04T19:45:18.672Z" }, + { url = "https://files.pythonhosted.org/packages/68/6a/45f108f137941a4a1238c85f28fd9d048cc46b5466d6b8dda3aba1bb9d4f/coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", size = 231958, upload-time = "2024-08-04T19:45:20.63Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e7/47b809099168b8b8c72ae311efc3e88c8d8a1162b3ba4b8da3cfcdb85743/coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", size = 232938, upload-time = "2024-08-04T19:45:23.062Z" }, + { url = "https://files.pythonhosted.org/packages/52/80/052222ba7058071f905435bad0ba392cc12006380731c37afaf3fe749b88/coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", size = 209352, upload-time = "2024-08-04T19:45:25.042Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d8/1b92e0b3adcf384e98770a00ca095da1b5f7b483e6563ae4eb5e935d24a1/coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", size = 210153, upload-time = "2024-08-04T19:45:27.079Z" }, + { url = "https://files.pythonhosted.org/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", size = 198926, upload-time = "2024-08-04T19:45:28.875Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, +] + +[[package]] +name = "coverage" +version = "7.10.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, + { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, + { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, + { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, + { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, + { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, + { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, + { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, + { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, + { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, + { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, + { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, + { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, + { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, + { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, + { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, + { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, + { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/d1c25053764b4c42eb294aae92ab617d2e4f803397f9c7c8295caa77a260/coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3", size = 217978, upload-time = "2025-09-21T20:03:30.362Z" }, + { url = "https://files.pythonhosted.org/packages/52/2f/b9f9daa39b80ece0b9548bbb723381e29bc664822d9a12c2135f8922c22b/coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c", size = 218370, upload-time = "2025-09-21T20:03:32.147Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6e/30d006c3b469e58449650642383dddf1c8fb63d44fdf92994bfd46570695/coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396", size = 244802, upload-time = "2025-09-21T20:03:33.919Z" }, + { url = "https://files.pythonhosted.org/packages/b0/49/8a070782ce7e6b94ff6a0b6d7c65ba6bc3091d92a92cef4cd4eb0767965c/coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40", size = 246625, upload-time = "2025-09-21T20:03:36.09Z" }, + { url = "https://files.pythonhosted.org/packages/6a/92/1c1c5a9e8677ce56d42b97bdaca337b2d4d9ebe703d8c174ede52dbabd5f/coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594", size = 248399, upload-time = "2025-09-21T20:03:38.342Z" }, + { url = "https://files.pythonhosted.org/packages/c0/54/b140edee7257e815de7426d5d9846b58505dffc29795fff2dfb7f8a1c5a0/coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a", size = 245142, upload-time = "2025-09-21T20:03:40.591Z" }, + { url = "https://files.pythonhosted.org/packages/e4/9e/6d6b8295940b118e8b7083b29226c71f6154f7ff41e9ca431f03de2eac0d/coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b", size = 246284, upload-time = "2025-09-21T20:03:42.355Z" }, + { url = "https://files.pythonhosted.org/packages/db/e5/5e957ca747d43dbe4d9714358375c7546cb3cb533007b6813fc20fce37ad/coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3", size = 244353, upload-time = "2025-09-21T20:03:44.218Z" }, + { url = "https://files.pythonhosted.org/packages/9a/45/540fc5cc92536a1b783b7ef99450bd55a4b3af234aae35a18a339973ce30/coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0", size = 244430, upload-time = "2025-09-21T20:03:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/75/0b/8287b2e5b38c8fe15d7e3398849bb58d382aedc0864ea0fa1820e8630491/coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f", size = 245311, upload-time = "2025-09-21T20:03:48.19Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1d/29724999984740f0c86d03e6420b942439bf5bd7f54d4382cae386a9d1e9/coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431", size = 220500, upload-time = "2025-09-21T20:03:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/43/11/4b1e6b129943f905ca54c339f343877b55b365ae2558806c1be4f7476ed5/coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07", size = 221408, upload-time = "2025-09-21T20:03:51.803Z" }, + { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] + +[[package]] +name = "coverage" +version = "7.14.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/54/fd/0ab2772530e946e1be1abd0bc09e647ec9b02e88f0867857601fefca8953/coverage-7.14.1.tar.gz", hash = "sha256:30c08f7d90415aa98b3c990385dea2939b0da55f38515e5b369b83655f8523be", size = 920132, upload-time = "2026-05-26T20:41:36.783Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/69/0d2ef01ff4b8fcecd4cba920d11e92fa4f96ae412441d3b56a90a258e69b/coverage-7.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3e3680291c4a1d0dadfa84a2c459576a4af5133abb617905714339a0c73138cf", size = 219722, upload-time = "2026-05-26T20:38:14.002Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ae/9afdeaa31b9d9ce98124b6abf8bb49119bf71aecae04f8567c189d91299f/coverage-7.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a5274669f37f2343635a347b91a60777621341ab3378e9c6ac9335eee704bddf", size = 220240, upload-time = "2026-05-26T20:38:17.424Z" }, + { url = "https://files.pythonhosted.org/packages/51/69/c998589871df7ea7dba865cc5ee32b5a3e1d47ba6c68ef91104c7c46fa5e/coverage-7.14.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cfe5a5fec635799ef33428f1e5e61bafa45a92a96190ba731561ba558ccc214d", size = 246981, upload-time = "2026-05-26T20:38:19.266Z" }, + { url = "https://files.pythonhosted.org/packages/fc/10/1c7d04c13040dac531d21b712bbe08f902e6dd9b58f5d77875c4d030f8f2/coverage-7.14.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:62a9f70b52e0b5a95cfef4a5c5641b06983cadc5e538a3feeb5c00211f523ac2", size = 248812, upload-time = "2026-05-26T20:38:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/c1/65/2a38a4607ef27cadcfbcee034dba5830ae2569f90144a0f4c7dbf47d30b0/coverage-7.14.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c18ebc343e15be53049b3a2dce38fe82d58f37e20ab9094b3a39c0aa4f6bb47", size = 250675, upload-time = "2026-05-26T20:38:22.159Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a2/a446ed9752a4a59b79e0fb6cbb319f6facb2183045c0725462625e66f87e/coverage-7.14.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b84ffdf877644e7096aa936991efeed873f7f3df57b9cd001312b7668ab08550", size = 252590, upload-time = "2026-05-26T20:38:23.63Z" }, + { url = "https://files.pythonhosted.org/packages/9e/fd/e81fbd7ba752365546e9842b1cbdaad3d6919d2a522c590aef16a281ec5e/coverage-7.14.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e854312c4103f2ad4c0dc023b69b77ebfd2c89db5f86c4c94dc2353f9a92167e", size = 247691, upload-time = "2026-05-26T20:38:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/53/35/f3c26fdaae9ea937d154ca4d372e5ea0a4167ff70d36c6074ac2eacb2f83/coverage-7.14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c643734307300234fafa36bf2a040a7235f8f177ea1fd6ec1423aea6fb7b929f", size = 248716, upload-time = "2026-05-26T20:38:26.406Z" }, + { url = "https://files.pythonhosted.org/packages/2e/14/940b6c49551fd343e8507ee2b0ba7af5d0aa04ed5bf768285cb7c72a9884/coverage-7.14.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:84ac9499e48700399a5dd0ea7085b5091961fec52c68d66b4ec0d3cf7f4441b1", size = 246721, upload-time = "2026-05-26T20:38:28.282Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2c/40fc0634186c28292a662dff578866b3913983d6c375a3c2a74020938719/coverage-7.14.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:7f02d09f70776579b926d889a4c9c235070a1f47c40458aeaca563fae5acfdb5", size = 250533, upload-time = "2026-05-26T20:38:29.753Z" }, + { url = "https://files.pythonhosted.org/packages/de/e3/2c26bf1e811f9df991ff2a9bdddebdd13ee0665d564df7d05979f9146297/coverage-7.14.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ce66d8e46da2bb5ee313a745cbd2e391d319176c1f7a9451bfcd3a2fb920859b", size = 246990, upload-time = "2026-05-26T20:38:31.516Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b0/060260ef56bd92363ebdce0c7095ce422b06e69aae71828efeca473ab1ca/coverage-7.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c912c259304cfb5ee584481cfb7ce1ff932b4d61e6c9140b8f19cb7b5ed82332", size = 247593, upload-time = "2026-05-26T20:38:33.065Z" }, + { url = "https://files.pythonhosted.org/packages/63/f3/501502046efeb0d6d94b5ca54941d95f1184183dd6bdb7f283985783bb4a/coverage-7.14.1-cp310-cp310-win32.whl", hash = "sha256:1238cb94638e610e972c60dac68e813f868dc7d6e982535270558443058d9d59", size = 222330, upload-time = "2026-05-26T20:38:35.36Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5d/1bf99f2c558f128faf7906817ccbdb576ba815d3b41ce2ac1719b70a3663/coverage-7.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:fc459e5d73be2d6332fcfe8dbf3d8994671fe33c700f4565988ecfa511547253", size = 223261, upload-time = "2026-05-26T20:38:37.196Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d7/477ad149490e6cb849f28abea1dabb9c823cea72e7500c81b4240ce619c0/coverage-7.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:478b5bcd63c2e1357c5c7e16c070690df7b07f676b1c114d7b93e533c664309f", size = 219848, upload-time = "2026-05-26T20:38:38.715Z" }, + { url = "https://files.pythonhosted.org/packages/91/82/a5eb47257c50601bb7b9a9d2857c67b7a3a85ad74180eb2c98bb1fbe0ce5/coverage-7.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a24a81f9715ee42ef59a316cc11611c98fe23920f7c81861315c9f3ff4a230f4", size = 220354, upload-time = "2026-05-26T20:38:40.232Z" }, + { url = "https://files.pythonhosted.org/packages/43/8b/78419b5391a5cb706b6544390507e469d83ffc9a8248b02c4011aceb9365/coverage-7.14.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:196a13319ad88d6d8ef5ab489ec4f44ddde2143c0c7d5b27786f6c3ffd56a7e1", size = 250771, upload-time = "2026-05-26T20:38:41.782Z" }, + { url = "https://files.pythonhosted.org/packages/77/63/e77aaacd491182210d639636b7a8bba23ffffa9b82aa3762da9431855fa9/coverage-7.14.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d452fd08b5c72c5167c93e6867b5c08500bd40f2a21e1e854a500550b6cc36f", size = 252683, upload-time = "2026-05-26T20:38:43.305Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/a022e3cfbec2ac241640003cb3a817e161d9c7f5aa9b49173756cdc03204/coverage-7.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23bf7fa51ac02e07fc7c96849b82946da47ae862dc8f86d183b2a4864fc38129", size = 254791, upload-time = "2026-05-26T20:38:45.361Z" }, + { url = "https://files.pythonhosted.org/packages/61/d6/967e408aca4c1ceb88cb0cc677169110ae7f5995fb5eaf5fb1f5a1bb8f5d/coverage-7.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcaa50684dcaadfa599ac48f81103c756d791cfd85c97203d2217c593d48b860", size = 256748, upload-time = "2026-05-26T20:38:46.91Z" }, + { url = "https://files.pythonhosted.org/packages/b8/be/869188f7fe28638078ec479331ace6dc5f7b40b7153eb616f47ab79404d8/coverage-7.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4ea1c034f95c9b056e856b794630b17f9fa3d57e4800ff1e503d3be0f9c9078c", size = 250907, upload-time = "2026-05-26T20:38:48.493Z" }, + { url = "https://files.pythonhosted.org/packages/07/aa/adb7d3b4278d690e68703abcd76ab1b948242e3668d921711551b78f9ddb/coverage-7.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7e057326434e441306226fbeb5d1aaf14a2637efe97ba668306635835f32ad7", size = 252483, upload-time = "2026-05-26T20:38:50.074Z" }, + { url = "https://files.pythonhosted.org/packages/43/61/331c74103c62dcb0c4b9b3a0de9a61aca016208b0a90f109592a9f9ecc28/coverage-7.14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:59baf88468dbc8d63b1887afd92bda52e40bb1561696e5819670601403810cec", size = 250545, upload-time = "2026-05-26T20:38:51.613Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b6/c5dae3c104d89be04828f61810e6b3473825482e4c288cc4ed04553e08ae/coverage-7.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d34d75f892b3ab73ba11cab5442cce7b3e168fd64162b16f0e1e0d09c508edef", size = 254310, upload-time = "2026-05-26T20:38:53.503Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a1/2b9d5863e3b83c01ad8199e3c597802fbb3a9dc90b058885804c20296d31/coverage-7.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3a56abc20a472baf0304c455721bc601477440d28ecfde8a03dde79ede07e0df", size = 250266, upload-time = "2026-05-26T20:38:55.414Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5e/0e511fbdb269359be26fe678a1c3fa1f2aa2a01573cc3f54268c8d6d4797/coverage-7.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6a3cb83d1552c0cd1b4906655b6a33fd4a8473229633a901c6b73bf86914dee9", size = 251174, upload-time = "2026-05-26T20:38:57.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/10/e55307b622b3dd9671cb321824502dc10f93e72f2802b9946159a8edadeb/coverage-7.14.1-cp311-cp311-win32.whl", hash = "sha256:10274a1fbeb8ec5d72966e17bb198a3104257aca4ac09d98667c5f8aca8c8548", size = 222354, upload-time = "2026-05-26T20:38:58.727Z" }, + { url = "https://files.pythonhosted.org/packages/71/cf/107421693cfb71e4f1ca5bf70443f64d4161878068d07a3e51c7ad21d17b/coverage-7.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:87ebdf787d4888e3f3f2d523eadc6e18c6d18c6d0eb173801a189641627fb37e", size = 223290, upload-time = "2026-05-26T20:39:00.413Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1d/3e3644585eb29e9dafefb19555078529a4d7cce12bd21929664eea989277/coverage-7.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:dd34767fa19848d35659ffc0a75314f58c7af3f1cd87ec521e8292a1238398a3", size = 221953, upload-time = "2026-05-26T20:39:02.159Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b7/bdbb725ba02c5b42825b200c940f38b7a54fcad24627b7192f78f8110d76/coverage-7.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a06c76364a9360e33d6d23769aefdf7f66f38e2ffb60ceb1baaa4989d83b695c", size = 220022, upload-time = "2026-05-26T20:39:03.702Z" }, + { url = "https://files.pythonhosted.org/packages/72/81/fdc0898a55c6219223291ec1a1fe89966ef212ce82276aa0899df84b5de0/coverage-7.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fad54e871165f6ec2f536063ac74c3104508a12963e64072ba44bd822de52b0c", size = 220379, upload-time = "2026-05-26T20:39:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/de/72/de048c4a25e13bce59ac6a339351c10bdf2515e07459afcdaf04dc3143a2/coverage-7.14.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:84b535f00655ecafe1d929d1fb00ed5d6fa3051ea643ab2c161a3887b86f294b", size = 251888, upload-time = "2026-05-26T20:39:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/28/30/300c343f68beb9d4cbb64ec81e58c5b6b80b56927f72d2b38654ac26e013/coverage-7.14.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b6b0853b895fe0e98cbfc580d1ec3393d9302b4b1e96a77b3f5c91fdab899e6", size = 254624, upload-time = "2026-05-26T20:39:09.037Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ed/7b25642496e8170b6bac14adce00537c6e5fa2d586159401a4de3e8b49e6/coverage-7.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:442cc9c952b2df400cda54bb04ab87330cf2cd08a8692cbbea36773531eb6f37", size = 255739, upload-time = "2026-05-26T20:39:10.889Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a2/abd210b8c4e29c24e4624916db97bb519097a91034aaeb767f937e7da794/coverage-7.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8270544c361ed405a27a060dbc9ed2c124b084d96dfdc2d9a2510482aef981ad", size = 257998, upload-time = "2026-05-26T20:39:12.722Z" }, + { url = "https://files.pythonhosted.org/packages/7f/24/7c50beed3792fe62f6ce0545c6686ce83379719e2c0276179333d97eae92/coverage-7.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:48b283b1dd6372e8de2a7a9a4c4d5dc06f4d4fd209b876f3c88a7a205a0c8f84", size = 252296, upload-time = "2026-05-26T20:39:14.259Z" }, + { url = "https://files.pythonhosted.org/packages/15/05/0f874628ebcbfc77ead559ff210281ef06a97db08481832e7dd39274a135/coverage-7.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5b0c99ba93a07d56f6df340bb79be53202a082b2fdb81bfe6190b741a3470d54", size = 253658, upload-time = "2026-05-26T20:39:15.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/6f/ca6ad067364b337ef997802115e7ecad2abd2248b05471464b0dea02b4d4/coverage-7.14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e471bc5769ff073b058cfadb0d736b56ce067c8560eabeb0da88462df98c23e7", size = 251803, upload-time = "2026-05-26T20:39:17.537Z" }, + { url = "https://files.pythonhosted.org/packages/c0/30/b9b4d377cd9f40baf228068f5a81faf8450c6228503011bd499708483a50/coverage-7.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f497a1ea81d4cd7c10ddcaa685135b9aabd291af3d55775a9ddf3cb7a364cdd9", size = 255873, upload-time = "2026-05-26T20:39:19.414Z" }, + { url = "https://files.pythonhosted.org/packages/3c/21/7c721a9e5e6bb88547d30a787aefb97512d3f54c1324c7488d9b3743f7f9/coverage-7.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2222be86d0b54f5dd5a38f45f17f315f737245e857bf0bdedc70734f84a13c02", size = 251372, upload-time = "2026-05-26T20:39:21.169Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f8ae5a2200130e1503cd7661a6cd3b2b7bacef98277fbf3571fb13f8b766/coverage-7.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:85e85586565842f6932abebd4c18bcb1074223dc0b3576e7d173ca710622813a", size = 253245, upload-time = "2026-05-26T20:39:23.097Z" }, + { url = "https://files.pythonhosted.org/packages/34/62/70a9024672a5f6910517d9628c52c9afbdd3cf8f46426af52bb148a56fff/coverage-7.14.1-cp312-cp312-win32.whl", hash = "sha256:4a28fd227808366b196a75476dced2eb35b351d6766ba9c858dc93319e87f4f1", size = 222567, upload-time = "2026-05-26T20:39:24.868Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/8b7cd386839b039ebe1855733b9f9449a8dec5d79564018234f185a7fa70/coverage-7.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:54acdb6674a4661768d7bf7db32dfb9f46ab1d764f8aba6df75ce1a6a088724e", size = 223372, upload-time = "2026-05-26T20:39:26.603Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ba/b44d472022f620d289d95fa830143235c0c36461c6f2437ea8d51e5481ed/coverage-7.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:99cd41ff91afd94896fea3bc002706b6ae4ce95727d06e4a0f39c0a8d8bd8b1a", size = 221989, upload-time = "2026-05-26T20:39:28.242Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9e/5f6d56327c62b185225d145191c607e07515294a0aa6338e58805cd4a5ac/coverage-7.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:be9f2c802dcfce3f71298303aa5dad0dce440a76c52f2f60dacd8656dab78793", size = 220044, upload-time = "2026-05-26T20:39:29.902Z" }, + { url = "https://files.pythonhosted.org/packages/75/92/e82aca356744cbbc0f77a0b623e38918c1872361963413a3bab5d0340393/coverage-7.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6223a72fd0e4c7156353ec0f08a5f93623e1d3034d0e2683b9bb8ea674131b1d", size = 220412, upload-time = "2026-05-26T20:39:31.561Z" }, + { url = "https://files.pythonhosted.org/packages/27/c9/385bde0bf7ed0f4bf3a7ee5367060a86b5d218718cfd6fb943c0f836b34f/coverage-7.14.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7279d2110a28cebc738b6459ecda2771735a4c18465fbbd36b3288fe5ed92247", size = 251412, upload-time = "2026-05-26T20:39:33.337Z" }, + { url = "https://files.pythonhosted.org/packages/51/8c/23faf6a2343a0d17f960a4bd56c43bc7eb4cf312f774dd6ceebd82c7d8fc/coverage-7.14.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9eeb3fcbc13ba40dfbdb22d01d196a28e9cef9ed4c29b60061a1e0e823a9929d", size = 254008, upload-time = "2026-05-26T20:39:35.009Z" }, + { url = "https://files.pythonhosted.org/packages/42/06/36f4aa9ca8a815e6036156e80706a67828bb97bd826948244f6996dda957/coverage-7.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f0cfc27c539f07cf5c0a4cfe211d0b6cae039f8f40526dbaa71944e64b50a7b", size = 255241, upload-time = "2026-05-26T20:39:36.71Z" }, + { url = "https://files.pythonhosted.org/packages/ca/79/95266316352f90f6b1c6736bb413302edfde2453fb32422d3911642691b3/coverage-7.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:221c70f316241a78e77e607c227cefc8808d4e08f28d99c04f35694690e940be", size = 257373, upload-time = "2026-05-26T20:39:38.412Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9c/58316d1f66c488b5fca8a0eb3e98348807813efa8a0d0833b9021be27488/coverage-7.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da028256b04ec30e5e0114b6f76172938c313991f0a2d3d894271315cf5d5e43", size = 251635, upload-time = "2026-05-26T20:39:40.268Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5a/ca2398a568e16fed7bb713e84ba3603a7164fb65779abe645c565ec890d5/coverage-7.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76a085d7005236a767e3426148b2c407e53ad61695c562f8a81da2d373324901", size = 253373, upload-time = "2026-05-26T20:39:42.145Z" }, + { url = "https://files.pythonhosted.org/packages/6e/2c/0396562c32deaebe7be51d865b3a41e9a87d7561acafe1a28f53b07e019a/coverage-7.14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b553d04b5e778a8e56d57eb134aff42a92718ecba45e79c4764ecfa40efd92ff", size = 251341, upload-time = "2026-05-26T20:39:43.907Z" }, + { url = "https://files.pythonhosted.org/packages/fd/8f/a94f9221184c9cae1ee115820e3798e48b6b17777a9f19e46fb9a0c8dc74/coverage-7.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:46f714d2fb8ae2f4f29f23ada7f1e79b759fff5a70f94a1dac23af204c3ec9e4", size = 255497, upload-time = "2026-05-26T20:39:46.166Z" }, + { url = "https://files.pythonhosted.org/packages/71/69/505d70e47db1eaebcd002c39759707621ef184cd6b1ae084d9f41293f323/coverage-7.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1896f5e19ff3f0431c7ce2172adc54890fd97f86b59ced8ca1649145d9ffe35d", size = 251159, upload-time = "2026-05-26T20:39:48.03Z" }, + { url = "https://files.pythonhosted.org/packages/e0/aa/58681c383aa33a9d2ed40a02d7a22fbf780d1fa4d575396365777828198c/coverage-7.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:62fd185ef9df3c33d1c8178c5af105f762afbad96038de9a4ae100aa6297ca33", size = 252934, upload-time = "2026-05-26T20:39:49.872Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fd/11c928cd6bdffc7074bb5965c173d9ebf517fb00205e1da524b98d29ef92/coverage-7.14.1-cp313-cp313-win32.whl", hash = "sha256:ab4af6352741a604c431c6072fce5bee33bf0f20dc7a56618d6bf6bb89e9810c", size = 222584, upload-time = "2026-05-26T20:39:51.68Z" }, + { url = "https://files.pythonhosted.org/packages/6f/92/fb416fc26d340dcba19518c418d6048e913186e17243982c5e435e41fa7a/coverage-7.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af486dabe8954d03b087f0021540897afe084f04e16ff5579e08cc46f871416", size = 223394, upload-time = "2026-05-26T20:39:53.472Z" }, + { url = "https://files.pythonhosted.org/packages/73/c6/02d56e3867972f77d5036de924643f26c056e848f00452cafb4dbc3c29b4/coverage-7.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:2224f89ffd0c5605ccce1ed7a584da162bc7c55f601ab1c946bc9de31a486b42", size = 222015, upload-time = "2026-05-26T20:39:55.374Z" }, + { url = "https://files.pythonhosted.org/packages/4d/9e/fcc77914050df73f7662fa1f00902774c79c075a8388ab334074574bf77e/coverage-7.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de286598cc65d2b489411174b1faec2f5a7775fb3201fd925db2a76b4030f37d", size = 220733, upload-time = "2026-05-26T20:39:57.189Z" }, + { url = "https://files.pythonhosted.org/packages/f7/67/2963cbdaf5cbadec44efa3a1e39eaa1f02df4079585f05387607a221e126/coverage-7.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:042c46ded7c288aeb07cf14a28b6c1e10b78fcba40171c3fa1e939377eeef0b5", size = 221086, upload-time = "2026-05-26T20:39:59.019Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/8701645574e11881f2f47d8930f98bc48b5d43b25eb5b4430dfc4a2f9f48/coverage-7.14.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f4ddbe407477f04c45115d1a4e5bc480f753553b534d338d4c3358b1cdd0ea52", size = 262381, upload-time = "2026-05-26T20:40:00.822Z" }, + { url = "https://files.pythonhosted.org/packages/7c/28/7a64d73598263e0c5abd5084211a8474488d31b3c552ff531c719dfcff62/coverage-7.14.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d13e6725992e2d2fd7d81d4f5241952d13740121dfd501da09201be39b2c003a", size = 264458, upload-time = "2026-05-26T20:40:02.506Z" }, + { url = "https://files.pythonhosted.org/packages/fa/d8/4969179db9f7eb4df218e69540adf829d1c835f59452513d065d15446802/coverage-7.14.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f747dc8edcfe740130f28f32f3995e955494285717e86ee25af51db2219df08a", size = 266884, upload-time = "2026-05-26T20:40:04.421Z" }, + { url = "https://files.pythonhosted.org/packages/a6/78/a45d5794dbc9bafd97afc96a4377c86c7820d78b6cf51b89bc1d4e919275/coverage-7.14.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced2f09ef276fd58611a1ef502164ad266d2b75174e5a40cabbdb4033f9f6cf2", size = 268022, upload-time = "2026-05-26T20:40:06.298Z" }, + { url = "https://files.pythonhosted.org/packages/21/cb/4f5e354e9e3e67af96bd4e57113e6db6b22298c7168b13eec408a549903d/coverage-7.14.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b84800013769a78ccb9ef4659402e26d06867e337b61ec365f77ad008adea80e", size = 261631, upload-time = "2026-05-26T20:40:08.226Z" }, + { url = "https://files.pythonhosted.org/packages/ec/49/eced49af4cb996d5d8b7e94e736175c513e4facd3398507b89892b4326d8/coverage-7.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ea8cd6ca0ee9f616aaef3afc6882e32c2cbf18b00d96313ffd76af650574034d", size = 264443, upload-time = "2026-05-26T20:40:10.137Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d8/5603a88a7c5913a6b54f6cb1a8c46f7b39cbb30f27cd3f492908da09b2d7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:aa5e304a873fabddc11e484e9b6b738bd38bd7bed17b09aa84eecf5332e8b8bb", size = 262069, upload-time = "2026-05-26T20:40:11.999Z" }, + { url = "https://files.pythonhosted.org/packages/f0/59/2ae3cb79da554a06c8619d6c88ea19dd1e4aed4b834b6a83bb1fa243bdc5/coverage-7.14.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5a1c5215be81035e629d5bc756650634d0bf31991038db7a0eccb90f025ce16d", size = 265780, upload-time = "2026-05-26T20:40:13.858Z" }, + { url = "https://files.pythonhosted.org/packages/af/5f/b130c1dc999031f2648bd25317fbce505ad8d5562079b4ed81e736a84967/coverage-7.14.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:79058c47dae6788504b5effb319961bcd72d7240551464b91d474bc0ed186d69", size = 260970, upload-time = "2026-05-26T20:40:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/87/d1/ec13ccddeb48ec963bdfa72a11224bac2584bd045ba13beca82f8113e9c7/coverage-7.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:370c5afae3fa0658e11694a32b24c2778f6bc2d17718121f94ee185e69f26b54", size = 263157, upload-time = "2026-05-26T20:40:18.382Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c2/cd91ead503045161092d3845f7bb95ea2f25131ce96d3e314dd835d91b9c/coverage-7.14.1-cp313-cp313t-win32.whl", hash = "sha256:3758dd0a7f1fa57365ef2e781df0f0731d38b6e3772259d13dae4bd8a958d4b1", size = 223259, upload-time = "2026-05-26T20:40:20.381Z" }, + { url = "https://files.pythonhosted.org/packages/71/9f/1e28d97e6bd2c76b07f38b7c02870f1371255ff6717f54eca578fcbbdd0e/coverage-7.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:6ff665fb023a77386fe11685190cee1f60a7d635994a30d9b0a061533d470fce", size = 224320, upload-time = "2026-05-26T20:40:22.316Z" }, + { url = "https://files.pythonhosted.org/packages/a9/e0/d936e908f0e1efa55e52b91e01b52f1055cef5e1ab2718493390ed8e2fb8/coverage-7.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:17a5a241e5997621a956a7f402a7433ef4221e5152809b785bec79e2323799f1", size = 222577, upload-time = "2026-05-26T20:40:24.894Z" }, + { url = "https://files.pythonhosted.org/packages/d6/34/fc2f101b151af3799a101f0550b0454aa008afdc0add677394ec4aa8ea10/coverage-7.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d5ed429d0b8edaac649e889b4ffcedb6c80b06629a3f93050e3dddfb99235bee", size = 220091, upload-time = "2026-05-26T20:40:27.249Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a7/1ebae2ab5b961b5c79bb09fe7b3ac99edb190d8be4a8c510b2cf66f46468/coverage-7.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8011224a62280e50dab346960c03cf47aca1a1e09e608c0fb33fd6e0cc8e9500", size = 220421, upload-time = "2026-05-26T20:40:30.084Z" }, + { url = "https://files.pythonhosted.org/packages/5e/90/92aca9cf0acc95123c96cd1eb1f08917897a7f5dee01e15738922971ec31/coverage-7.14.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:12c42ec1e14f553c4f817e989365982e646e27211f10a0f717855b94a79c8906", size = 251466, upload-time = "2026-05-26T20:40:32.542Z" }, + { url = "https://files.pythonhosted.org/packages/26/2b/78048cbe3b999f6cbf9cc0d90abba6a88a3e0863a8c1c6cbc762f3f8802f/coverage-7.14.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06144cd511cf2624873a035c5069cf297144f6e77a73ee3d7a55b605ec5efb42", size = 253973, upload-time = "2026-05-26T20:40:34.473Z" }, + { url = "https://files.pythonhosted.org/packages/8e/21/c2e33b29d1cfde484a19d437afc343c6cd30b08d78cbbf9f5aff14e57b2b/coverage-7.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a311d8e1da24be5c1ccf85cbfb06315dbaa1703d5a1eab3f6432c72b837917c8", size = 255318, upload-time = "2026-05-26T20:40:38.154Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ee/aad2f108d63b769121005302f16bf66db8625c88ceaba466942e09a2607e/coverage-7.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c79cead5b5bc584d9c71451cb984d0e3a84e0c0937379c8efcbf27c8d661b851", size = 257633, upload-time = "2026-05-26T20:40:40.164Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f8/11a2c29b4fd76d9849f81d0bb812ec0017a9396df3217214e38934a8c837/coverage-7.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcbf65f1f66a26cdd88c35cf68fb4729c5d1cd2e88added72420541dfb212034", size = 251488, upload-time = "2026-05-26T20:40:42.631Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b8/9a5820de4b8ac2b71d85e3b5fb49108d7469c665f0e2ad0dd7569023e305/coverage-7.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd86572566fb40189a8260446158235159bc7a82dfbc87a3b39cf4fb57fcec1c", size = 253329, upload-time = "2026-05-26T20:40:45.208Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ff/f33e4823667e27548e8fd8df44217515303f9808d0ff29817db56f87d990/coverage-7.14.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7771b601718fdde84832c3a434ca9bbf4ae9adbc49d84198b4110700c3c77c36", size = 251291, upload-time = "2026-05-26T20:40:47.502Z" }, + { url = "https://files.pythonhosted.org/packages/68/9b/489db0ebb209054766b90a9014a45f6d26eb724c02ec21311c3733b5a644/coverage-7.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:39b21e212c55af06fa375e3dbf90a8a8e38792f3a910c580066d23563830ddd5", size = 255564, upload-time = "2026-05-26T20:40:49.372Z" }, + { url = "https://files.pythonhosted.org/packages/27/b5/16bc2d4c2409b23c7737edb68c83bc89e345f378050549fe1d75ac7d34d5/coverage-7.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f2302660e32562a532b442480121aef8aa61a5bdb20b30bf0adab29f10a5a4b4", size = 251107, upload-time = "2026-05-26T20:40:51.677Z" }, + { url = "https://files.pythonhosted.org/packages/7d/0c/2629997469a00cd069d588a41c9dc887610f2775ae89d250c4791e65272a/coverage-7.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:03a6f93c1ec3b7f2e77b5dbcc5573a2c21f12529a5c6bbe0f16f72303cc2fa4d", size = 252764, upload-time = "2026-05-26T20:40:54.267Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ee/f78d63c8f079e0d7211c7e2401fa17e311514534ba61bae03e4b287ce4ab/coverage-7.14.1-cp314-cp314-win32.whl", hash = "sha256:8a3ce026d73290f42f08dafecbd82c193a74df280461fbf97300fec51fd133ee", size = 222837, upload-time = "2026-05-26T20:40:56.496Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b9/be539854f93a70dfbeec69117f33ec70dc42ff0b65b5b07ab8d40d04228e/coverage-7.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:114c95ef29302423b87d159075805f4ab973254a2638a5d7d046c94887cc87d7", size = 223650, upload-time = "2026-05-26T20:40:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9e/24e2842fef40f35ac82ba3a7719c8023d011bf3bf652d0675316a9d088a1/coverage-7.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:a07891c3f4805442b31b71e84ba3cf29ed1aa9a428284e06deeb4b23e5b46343", size = 222218, upload-time = "2026-05-26T20:41:00.321Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1d/ac0a9df5fe31c1e8bdd658074905fc12844a05c1a7e3fdb8417e97c31e23/coverage-7.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1101a5ebb083aecb625ebb6209d4105b58f647b093cb2dc8122d7b33f743cfe1", size = 220822, upload-time = "2026-05-26T20:41:02.281Z" }, + { url = "https://files.pythonhosted.org/packages/32/cf/f964fd9aff20323f9f1a726c97135f8a76bcd87b92dad141a456a43f3c64/coverage-7.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:851b9e1e4e8a4608e77c79714b2e77c0970d2ed7202a05e92ae407817481887b", size = 221084, upload-time = "2026-05-26T20:41:04.593Z" }, + { url = "https://files.pythonhosted.org/packages/d8/5e/7e5ef2aba844de2b80d678619fcf0841b42e3f37f16411226f3fe4c1016f/coverage-7.14.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d5b89cdfb2ee051b71e8c3c70bd81a9eff81100f736a269136fe1a68efe00474", size = 262454, upload-time = "2026-05-26T20:41:06.641Z" }, + { url = "https://files.pythonhosted.org/packages/64/62/75809bded87015cc4935524218a2a8ed8dd1a8498bfed30a2f4f7a4b4d34/coverage-7.14.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0177614a0370f227888b4e436a7c55686d6a9f90eb1ade2b624ba685a1686e86", size = 264578, upload-time = "2026-05-26T20:41:08.556Z" }, + { url = "https://files.pythonhosted.org/packages/f3/42/d33392dc14633525012d2d504fa1a33b05538bf535f5c1d64675e5754b78/coverage-7.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d69af5dea2de76fc485a83032a630523f985198b7e25be901ec60181587b01e", size = 266981, upload-time = "2026-05-26T20:41:10.824Z" }, + { url = "https://files.pythonhosted.org/packages/2a/49/0157c4428c2aca7f1e09d5565930586fd5ae36f1655f08b0daa7cf1fcae1/coverage-7.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35ab22d91de736e8966b980dc355cbcdd2c6dbbcfe275f9a2991bc8a91b3df65", size = 268112, upload-time = "2026-05-26T20:41:12.966Z" }, + { url = "https://files.pythonhosted.org/packages/96/26/86b9ce71f4092b1ed325ce1421698081df1286b833400b6836912834d6e0/coverage-7.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:357d4e32935c36588aaba057d734fa32428c360c9fc2e4442afbf1b646beee6e", size = 261558, upload-time = "2026-05-26T20:41:15Z" }, + { url = "https://files.pythonhosted.org/packages/20/4c/c311210c5472cf5401d8422b0d7812cdd520f24417673afabda6c323faca/coverage-7.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:51bd64741cc6fa065abd300ede1afe5a5291ece9c31da8b24884deda48bcc3f8", size = 264447, upload-time = "2026-05-26T20:41:17.369Z" }, + { url = "https://files.pythonhosted.org/packages/fb/71/59513f8710ed3e6b0ac0a050a5b7e977bb9c9e880354863b5d00d8809256/coverage-7.14.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:9132cd363a68a4c3daa7c8704a654b1e39d3360f6f5b8ddd470608a945236c07", size = 262048, upload-time = "2026-05-26T20:41:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/bceed32dc494f5bbf50f775cd2e78ca814953942b5ea28d3c1c3ac316f14/coverage-7.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07c6290b1697b862c0478eab545eec949a0d0e4d6d03497f446d706da3b4f2de", size = 265781, upload-time = "2026-05-26T20:41:21.559Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c5/9348fe40dbfd4991aaf78df2c6c3098bfb2cc834d1fd362a64b4efef855a/coverage-7.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5ea0c297e27133853b4d8a3eb799bff5a2dbd9f2f41537a240d337ac9b4df890", size = 260896, upload-time = "2026-05-26T20:41:23.428Z" }, + { url = "https://files.pythonhosted.org/packages/ca/92/1ea0f03929da7cf87206b1fa24f4c8e9c158be0455481af29ec0a1f3503f/coverage-7.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:01b7733daad0237daa01ef80fe2dfceffc911e6a17fa7b55d14aa8214eaaaecd", size = 263214, upload-time = "2026-05-26T20:41:25.419Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a9/b2493c054c0e01a643266742ab45e15744e60743f9260cd930c7142b1124/coverage-7.14.1-cp314-cp314t-win32.whl", hash = "sha256:6adc5a36984624a70bf11d7184e20fa0a49aa7c47ffab43804106a1a695ea22e", size = 223624, upload-time = "2026-05-26T20:41:27.795Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bd/3e1e6a57fccd2d7c83fcdf338e93ba98eb85c6e877dd34731ac585375490/coverage-7.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ddf799247318f34dbcd2efa8c95a8d0642674e926bb1774cf9b63dfd2a389d1c", size = 224728, upload-time = "2026-05-26T20:41:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d7/31066cf1d2f0c6c797fce911bcfa01dd35642dc6da992a950256097c5860/coverage-7.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:145986fe66647eb489f18d9a997567a3fd358584c4b5a808769113abc07466af", size = 222752, upload-time = "2026-05-26T20:41:32.123Z" }, + { url = "https://files.pythonhosted.org/packages/8a/3c/1a983b9a745d7f83d53f057bcc5bf79ba6a2bbc08266b3f0c7d6fe630c9b/coverage-7.14.1-py3-none-any.whl", hash = "sha256:a252f21c27e38347e60111a3266b03827422a7d5525951aceee313aa68bab1d2", size = 211815, upload-time = "2026-05-26T20:41:34.078Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' and python_full_version <= '3.11'" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "6.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "zipp", marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/82/f6e29c8d5c098b6be61460371c2c5591f4a335923639edec43b3830650a4/importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4", size = 53569, upload-time = "2023-06-18T21:44:35.024Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/94/64287b38c7de4c90683630338cf28f129decbba0a44f0c6db35a873c73c4/importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5", size = 22934, upload-time = "2023-06-18T21:44:33.441Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +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 = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433, upload-time = "2023-02-04T12:11:27.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695, upload-time = "2023-02-04T12:11:25.002Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +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 = "packaging" +version = "24.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/b5/b43a27ac7472e1818c4bafd44430e69605baefe1f34440593e0332ec8b4d/packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9", size = 147882, upload-time = "2024-03-10T09:39:28.33Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", size = 53488, upload-time = "2024-03-10T09:39:25.947Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pathspec" +version = "0.11.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/2a/bd167cdf116d4f3539caaa4c332752aac0b3a0cc0174cdb302ee68933e81/pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3", size = 47032, upload-time = "2023-07-29T01:05:04.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/2a/9b1be29146139ef459188f5e420a66e835dda921208db600b7037093891f/pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20", size = 29603, upload-time = "2023-07-29T01:05:02.656Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +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 = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/28/e40d24d2e2eb23135f8533ad33d582359c7825623b1e022f9d460def7c05/platformdirs-4.0.0.tar.gz", hash = "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731", size = 19914, upload-time = "2023-11-10T16:43:08.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/16/70be3b725073035aa5fc3229321d06e22e73e3e09f6af78dcfdf16c7636c/platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b", size = 17562, upload-time = "2023-11-10T16:43:06.949Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302, upload-time = "2024-09-17T19:06:50.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439, upload-time = "2024-09-17T19:06:49.212Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, +] + +[[package]] +name = "pluggy" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/42/8f2833655a29c4e9cb52ee8a2be04ceac61bcff4a680fb338cbd3d1e322d/pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3", size = 61613, upload-time = "2023-06-21T09:12:28.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/32/4a79112b8b87b21450b066e102d6608907f4c885ed7b04c3fdb085d4d6ae/pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849", size = 17695, upload-time = "2023-06-21T09:12:27.397Z" }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +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 = "probing" +version = "0.2.5" +source = { editable = "." } + +[package.optional-dependencies] +dev = [ + { name = "black", version = "23.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "black", version = "24.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "black", version = "25.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "black", version = "26.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "coverage", version = "7.2.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "coverage", version = "7.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "coverage", version = "7.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest", version = "7.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pytest", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest-cov", version = "4.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pytest-cov", version = "5.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pytest-cov", version = "7.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "ruff" }, +] +test = [ + { name = "coverage", version = "7.2.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "coverage", version = "7.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "coverage", version = "7.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest", version = "7.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pytest", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest-cov", version = "4.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pytest-cov", version = "5.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pytest-cov", version = "7.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] + +[package.metadata] +requires-dist = [ + { name = "black", marker = "python_full_version >= '3.8' and extra == 'dev'", specifier = ">=24.0" }, + { name = "black", marker = "python_full_version < '3.8' and extra == 'dev'", specifier = "<24.0" }, + { name = "coverage", marker = "extra == 'dev'", specifier = ">=7.0" }, + { name = "coverage", marker = "extra == 'test'", specifier = ">=7.0" }, + { name = "pytest", marker = "python_full_version >= '3.8' and extra == 'dev'", specifier = ">=8.0" }, + { name = "pytest", marker = "python_full_version >= '3.8' and extra == 'test'", specifier = ">=8.0" }, + { name = "pytest", marker = "python_full_version < '3.8' and extra == 'dev'", specifier = "<8.0" }, + { name = "pytest", marker = "python_full_version < '3.8' and extra == 'test'", specifier = "<8.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0" }, + { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=4.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" }, +] +provides-extras = ["test", "dev"] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "7.4.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.8' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.8'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.8'" }, + { name = "iniconfig", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "packaging", version = "24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "pluggy", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, + { name = "tomli", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", size = 1357116, upload-time = "2023-12-31T12:00:18.035Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8", size = 325287, upload-time = "2023-12-31T12:00:13.963Z" }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version == '3.8.*' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.8.*'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "packaging", version = "26.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version == '3.9.*' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.9.*'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "packaging", version = "26.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pygments", marker = "python_full_version == '3.9.*'" }, + { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", version = "26.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pygments", marker = "python_full_version >= '3.10'" }, + { name = "tomli", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +dependencies = [ + { name = "coverage", version = "7.2.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.8'" }, + { name = "pytest", version = "7.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/15/da3df99fd551507694a9b01f512a2f6cf1254f33601605843c3775f39460/pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6", size = 63245, upload-time = "2023-05-24T18:44:56.845Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/4b/8b78d126e275efa2379b1c2e09dc52cf70df16fc3b90613ef82531499d73/pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a", size = 21949, upload-time = "2023-05-24T18:44:54.079Z" }, +] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +dependencies = [ + { name = "coverage", version = "7.6.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version == '3.8.*'" }, + { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042, upload-time = "2024-03-24T20:16:34.856Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990, upload-time = "2024-03-24T20:16:32.444Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version == '3.9.*'" }, + { name = "coverage", version = "7.14.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pytest", version = "9.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/24/f206113e05cb8ef51b3850e7ef88f20da6f4bf932190ceb48bd3da103e10/pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5", size = 161522, upload-time = "2026-01-30T01:02:50.393Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e9/06a6bf1b90c2ed81a9c7d2544232fe5d2891d1cd480e8a1809ca354a8eb2/pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe", size = 246945, upload-time = "2026-01-30T01:02:52.399Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/f6fb1007a4c3d8b682d5d65b7c1fb33257587a5f782647091e3408abe0b8/pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c", size = 259525, upload-time = "2026-01-30T01:02:53.737Z" }, + { url = "https://files.pythonhosted.org/packages/04/92/086f89b4d622a18418bac74ab5db7f68cf0c21cf7cc92de6c7b919d76c88/pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7", size = 262693, upload-time = "2026-01-30T01:02:54.871Z" }, + { url = "https://files.pythonhosted.org/packages/b4/7b/8b31c347cf94a3f900bdde750b2e9131575a61fdb620d3d3c75832262137/pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2", size = 103567, upload-time = "2026-01-30T01:02:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, + { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" }, + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/4a/08/968c22e06ab6570788964e2d5a702db9a3816e20ffde380b2b1385541d64/pytokens-0.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:da5baeaf7116dced9c6bb76dc31ba04a2dc3695f3d9f74741d7910122b456edc", size = 154847, upload-time = "2026-01-30T01:03:32.268Z" }, + { url = "https://files.pythonhosted.org/packages/09/2b/2061bb4b300e6921f7968724b185237627a8a3dc4f311e34079dfadf9b65/pytokens-0.4.1-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11edda0942da80ff58c4408407616a310adecae1ddd22eef8c692fe266fa5009", size = 238610, upload-time = "2026-01-30T01:03:33.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/64/abf6e43523ea9b4aea69bfe22788a518806741107238674e5c0fb6fc8dc1/pytokens-0.4.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0fc71786e629cef478cbf29d7ea1923299181d0699dbe7c3c0f4a583811d9fc1", size = 252493, upload-time = "2026-01-30T01:03:35.715Z" }, + { url = "https://files.pythonhosted.org/packages/dc/fb/bcb6784c87d1de182afb284f37b07bc172eebec91ddc20e83aec767e4963/pytokens-0.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dcafc12c30dbaf1e2af0490978352e0c4041a7cde31f4f81435c2a5e8b9cabb6", size = 255651, upload-time = "2026-01-30T01:03:36.961Z" }, + { url = "https://files.pythonhosted.org/packages/1a/0c/0c33752be2209498661903f6f240779aea5c9adbd85d22336ce3f7718e81/pytokens-0.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:42f144f3aafa5d92bad964d471a581651e28b24434d184871bd02e3a0d956037", size = 104346, upload-time = "2026-01-30T01:03:38.069Z" }, + { url = "https://files.pythonhosted.org/packages/51/2a/f125667ce48105bf1f4e50e03cfa7b24b8c4f47684d7f1cf4dcb6f6b1c15/pytokens-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:34bcc734bd2f2d5fe3b34e7b3c0116bfb2397f2d9666139988e7a3eb5f7400e3", size = 161464, upload-time = "2026-01-30T01:03:39.11Z" }, + { url = "https://files.pythonhosted.org/packages/40/df/065a30790a7ca6bb48ad9018dd44668ed9135610ebf56a2a4cb8e513fd5c/pytokens-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941d4343bf27b605e9213b26bfa1c4bf197c9c599a9627eb7305b0defcfe40c1", size = 246159, upload-time = "2026-01-30T01:03:40.131Z" }, + { url = "https://files.pythonhosted.org/packages/a5/1c/fd09976a7e04960dabc07ab0e0072c7813d566ec67d5490a4c600683c158/pytokens-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3ad72b851e781478366288743198101e5eb34a414f1d5627cdd585ca3b25f1db", size = 259120, upload-time = "2026-01-30T01:03:41.233Z" }, + { url = "https://files.pythonhosted.org/packages/52/49/59fdc6fc5a390ae9f308eadeb97dfc70fc2d804ffc49dd39fc97604622ec/pytokens-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:682fa37ff4d8e95f7df6fe6fe6a431e8ed8e788023c6bcc0f0880a12eab80ad1", size = 262196, upload-time = "2026-01-30T01:03:42.696Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e7/d6734dccf0080e3dc00a55b0827ab5af30c886f8bc127bbc04bc3445daec/pytokens-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:30f51edd9bb7f85c748979384165601d028b84f7bd13fe14d3e065304093916a", size = 103510, upload-time = "2026-01-30T01:03:43.915Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/98/1295ad5a5aa9bc85bdcdfa5d82fe7b49c61af5657df4f227637ff9de0da6/ruff-0.15.18.tar.gz", hash = "sha256:2698a964c70e8bf402dcb99c8810472d270d141e7aa8c4e13599fd52033a2f33", size = 4761437, upload-time = "2026-06-18T18:25:39.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/d0/686e984941269621e2be72612d5c1e461f8f7b38415a2a7d7a81c8ae6715/ruff-0.15.18-py3-none-linux_armv6l.whl", hash = "sha256:8b6850172348c8381b8b3084c5915a4393c2373b9b54cd5b5e1ea15812bc10df", size = 10887308, upload-time = "2026-06-18T18:25:03.062Z" }, + { url = "https://files.pythonhosted.org/packages/ed/21/bc4123e3f5515ee99f8ce1eb93a14a0628fe4d1678663cd08f933ac16931/ruff-0.15.18-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3fccc153a85417dcd976883160cacce486997b0a0058dd18f54b8aaaac7d1ce2", size = 11281305, upload-time = "2026-06-18T18:25:30.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/93/4769464c25cf7ab2acb3c7dda9cad3d867eb41c59565b3e2a9d17249c90c/ruff-0.15.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08d4c86a68f2c3ec2c9d56380a71fb4a4f65373055cbb8caabd645e9102f38d4", size = 10641215, upload-time = "2026-06-18T18:25:15.802Z" }, + { url = "https://files.pythonhosted.org/packages/6c/42/56926d17120db2c208d76bf60a1a019644dd9e91dc27f0f95c9caddb1366/ruff-0.15.18-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37e5108745c2c0705da916d7d4de533ddf547051ef45f62888c31bae73f66318", size = 10957224, upload-time = "2026-06-18T18:25:36.955Z" }, + { url = "https://files.pythonhosted.org/packages/22/4f/d43fab8d8189afde803103022d000a8ef9f230616d436d52a8b2b8d63b50/ruff-0.15.18-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56949a6ce8b3abde54c0bcb22cebfe57e8771cadc84b407ae8b8eaf67ebdcd43", size = 10699024, upload-time = "2026-06-18T18:25:05.707Z" }, + { url = "https://files.pythonhosted.org/packages/63/42/1e3e4c68bd408b9768cf3e439acbe2c78245225faef253f7028a0cdb63e0/ruff-0.15.18-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01a754cd6a1b630d3f97e33eb452cf7a98040482318e870f8bc52a5a30e62657", size = 11491458, upload-time = "2026-06-18T18:25:20.275Z" }, + { url = "https://files.pythonhosted.org/packages/20/77/47a3484bea8521e14a203d98c389c5c97846675e4f02734672da4a69b52a/ruff-0.15.18-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ba7a07e03a44dbf10bb086ee06705b173625014ec99f73a7e6836a5e5590a0c", size = 12383752, upload-time = "2026-06-18T18:25:22.535Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ca/054159590787023d83b658a1a1819c4c8910114e7015069340b71c0961cb/ruff-0.15.18-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a2c40a41a4cadbcf5897b548ab29dfe248b20c540961c0247d98a3973c70403", size = 11577923, upload-time = "2026-06-18T18:25:10.702Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/d353d6b7bbd73cc0ec37f4463d7540e45e894338abdd9964eee0de332708/ruff-0.15.18-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f0480ce690cbb6c4db6e5d08f19fce98e10ba131a8b60c1bcdac42771e3ae2d", size = 11583925, upload-time = "2026-06-18T18:25:32.391Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4a/891f89b9c296ed3e5f3ece1a5629badc989d9a8fdaa30431aaf4774bc1c2/ruff-0.15.18-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2330215f1f393fa8733f55edce04fcf94c36a2c460fcde31f78cc84e4951e9b1", size = 11582834, upload-time = "2026-06-18T18:25:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/32/a3/ed9e370154bf85de360b93c03026157f02d4943b2d01ff4945f4429f8e8a/ruff-0.15.18-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6aa6a3d979e48ae617578183674bf264fbe7d0114a796a26bd678d67963c7ff", size = 10927328, upload-time = "2026-06-18T18:25:34.676Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d1/5cf5909329fedb5d39d555ee818ba5cf4638e1a301b89785d34f2905bfcb/ruff-0.15.18-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a81beadbbff2c9c245561ae3f77b16709d87f35eec650d0501679239d3449b22", size = 10693187, upload-time = "2026-06-18T18:25:08.245Z" }, + { url = "https://files.pythonhosted.org/packages/fd/44/ff6c635cf2c4f4e7b618b6640da057376baa36014695487d88aed4794268/ruff-0.15.18-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2186d9e940ae332ab293623a75b5f4fe49565f449954d50a72a046683aa6b809", size = 11208721, upload-time = "2026-06-18T18:25:41.327Z" }, + { url = "https://files.pythonhosted.org/packages/88/d9/5baa2a30861adfb7022cf33c1e35b2fc18085b08c16f83eff4c7b99a5f48/ruff-0.15.18-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5c2abf140438032bc77b2284a6c9944ecd8a19e5f1c7b52b1b8e4a0a80d19a7a", size = 11678599, upload-time = "2026-06-18T18:25:13.607Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1a/0725a7cfdc32ff769efb96ee782bec882e16448c5d9e3be947ec4c04ce27/ruff-0.15.18-py3-none-win32.whl", hash = "sha256:02299e6e9fa5b297a3f6d5d10d7bcd655c925b028bb8b9d4588214549c6b9ec4", size = 10901903, upload-time = "2026-06-18T18:25:24.755Z" }, + { url = "https://files.pythonhosted.org/packages/f3/51/805d9f6fb7970505c3504794a5ec350f605361b807fef4dcf214ebd35e72/ruff-0.15.18-py3-none-win_amd64.whl", hash = "sha256:dac80dc8d26b2257dbefabed62f5d255c3937b4ccb122da1fc634794fa3578b3", size = 12041189, upload-time = "2026-06-18T18:25:17.915Z" }, + { url = "https://files.pythonhosted.org/packages/29/4c/67bb45e41609eb4726f1bfeb59e083cf91d14c696d4bd14c234a980be93d/ruff-0.15.18-py3-none-win_arm64.whl", hash = "sha256:b2c9257fcbd4a3e5b977a1904e6facca016bafe2edc17df24db67cfaee03b4e4", size = 11329958, upload-time = "2026-06-18T18:25:43.686Z" }, +] + +[[package]] +name = "tomli" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164, upload-time = "2022-02-08T10:54:04.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757, upload-time = "2022-02-08T10:54:02.017Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "typed-ast" +version = "1.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/7e/a424029f350aa8078b75fd0d360a787a273ca753a678d1104c5fa4f3072a/typed_ast-1.5.5.tar.gz", hash = "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd", size = 252841, upload-time = "2023-07-04T18:38:08.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/07/5defe18d4fc16281cd18c4374270abc430c3d852d8ac29b5db6599d45cfe/typed_ast-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b", size = 223267, upload-time = "2023-07-04T18:37:00.129Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5c/e379b00028680bfcd267d845cf46b60e76d8ac6f7009fd440d6ce030cc92/typed_ast-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686", size = 208260, upload-time = "2023-07-04T18:37:03.069Z" }, + { url = "https://files.pythonhosted.org/packages/3b/99/5cc31ef4f3c80e1ceb03ed2690c7085571e3fbf119cbd67a111ec0b6622f/typed_ast-1.5.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597fc66b4162f959ee6a96b978c0435bd63791e31e4f410622d19f1686d5e769", size = 842272, upload-time = "2023-07-04T18:37:04.916Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ed/b9b8b794b37b55c9247b1e8d38b0361e8158795c181636d34d6c11b506e7/typed_ast-1.5.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d41b7a686ce653e06c2609075d397ebd5b969d821b9797d029fccd71fdec8e04", size = 824651, upload-time = "2023-07-04T18:37:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/ca/59/dbbbe5a0e91c15d14a0896b539a5ed01326b0d468e75c1a33274d128d2d1/typed_ast-1.5.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5fe83a9a44c4ce67c796a1b466c270c1272e176603d5e06f6afbc101a572859d", size = 854960, upload-time = "2023-07-04T18:37:08.474Z" }, + { url = "https://files.pythonhosted.org/packages/90/f0/0956d925f87bd81f6e0f8cf119eac5e5c8f4da50ca25bb9f5904148d4611/typed_ast-1.5.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d5c0c112a74c0e5db2c75882a0adf3133adedcdbfd8cf7c9d6ed77365ab90a1d", size = 839321, upload-time = "2023-07-04T18:37:10.417Z" }, + { url = "https://files.pythonhosted.org/packages/43/17/4bdece9795da6f3345c4da5667ac64bc25863617f19c28d81f350f515be6/typed_ast-1.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:e1a976ed4cc2d71bb073e1b2a250892a6e968ff02aa14c1f40eba4f365ffec02", size = 139380, upload-time = "2023-07-04T18:37:12.157Z" }, + { url = "https://files.pythonhosted.org/packages/75/53/b685e10da535c7b3572735f8bea0d4abb35a04722a7d44ca9c163a0cf822/typed_ast-1.5.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c631da9710271cb67b08bd3f3813b7af7f4c69c319b75475436fcab8c3d21bee", size = 223264, upload-time = "2023-07-04T18:37:13.637Z" }, + { url = "https://files.pythonhosted.org/packages/96/fd/fc8ccf19fc16a40a23e7c7802d0abc78c1f38f1abb6e2447c474f8a076d8/typed_ast-1.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b445c2abfecab89a932b20bd8261488d574591173d07827c1eda32c457358b18", size = 208158, upload-time = "2023-07-04T18:37:15.141Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/598e47f2c3ecd19d7f1bb66854d0d3ba23ffd93c846448790a92524b0a8d/typed_ast-1.5.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc95ffaaab2be3b25eb938779e43f513e0e538a84dd14a5d844b8f2932593d88", size = 878366, upload-time = "2023-07-04T18:37:16.614Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/765e8bf8b24d0ed7b9fc669f6826c5bc3eb7412fc765691f59b83ae195b2/typed_ast-1.5.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61443214d9b4c660dcf4b5307f15c12cb30bdfe9588ce6158f4a005baeb167b2", size = 860314, upload-time = "2023-07-04T18:37:18.215Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3c/4af750e6c673a0dd6c7b9f5b5e5ed58ec51a2e4e744081781c664d369dfa/typed_ast-1.5.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6eb936d107e4d474940469e8ec5b380c9b329b5f08b78282d46baeebd3692dc9", size = 898108, upload-time = "2023-07-04T18:37:20.095Z" }, + { url = "https://files.pythonhosted.org/packages/03/8d/d0a4d1e060e1e8dda2408131a0cc7633fc4bc99fca5941dcb86c461dfe01/typed_ast-1.5.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e48bf27022897577d8479eaed64701ecaf0467182448bd95759883300ca818c8", size = 881971, upload-time = "2023-07-04T18:37:21.912Z" }, + { url = "https://files.pythonhosted.org/packages/90/83/f28d2c912cd010a09b3677ac69d23181045eb17e358914ab739b7fdee530/typed_ast-1.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:83509f9324011c9a39faaef0922c6f720f9623afe3fe220b6d0b15638247206b", size = 139286, upload-time = "2023-07-04T18:37:23.625Z" }, + { url = "https://files.pythonhosted.org/packages/d5/00/635353c31b71ed307ab020eff6baed9987da59a1b2ba489f885ecbe293b8/typed_ast-1.5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2188bc33d85951ea4ddad55d2b35598b2709d122c11c75cffd529fbc9965508e", size = 222315, upload-time = "2023-07-04T18:37:36.008Z" }, + { url = "https://files.pythonhosted.org/packages/01/95/11be104446bb20212a741d30d40eab52a9cfc05ea34efa074ff4f7c16983/typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0635900d16ae133cab3b26c607586131269f88266954eb04ec31535c9a12ef1e", size = 793541, upload-time = "2023-07-04T18:37:37.614Z" }, + { url = "https://files.pythonhosted.org/packages/32/f1/75bd58fb1410cb72fbc6e8adf163015720db2c38844b46a9149c5ff6bf38/typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57bfc3cf35a0f2fdf0a88a3044aafaec1d2f24d8ae8cd87c4f58d615fb5b6311", size = 778348, upload-time = "2023-07-04T18:37:39.332Z" }, + { url = "https://files.pythonhosted.org/packages/47/97/0bb4dba688a58ff9c08e63b39653e4bcaa340ce1bb9c1d58163e5c2c66f1/typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fe58ef6a764de7b4b36edfc8592641f56e69b7163bba9f9c8089838ee596bfb2", size = 809447, upload-time = "2023-07-04T18:37:41.017Z" }, + { url = "https://files.pythonhosted.org/packages/a8/cd/9a867f5a96d83a9742c43914e10d3a2083d8fe894ab9bf60fd467c6c497f/typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d09d930c2d1d621f717bb217bf1fe2584616febb5138d9b3e8cdd26506c3f6d4", size = 796707, upload-time = "2023-07-04T18:37:42.625Z" }, + { url = "https://files.pythonhosted.org/packages/eb/06/73ca55ee5303b41d08920de775f02d2a3e1e59430371f5adf7fbb1a21127/typed_ast-1.5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:d40c10326893ecab8a80a53039164a224984339b2c32a6baf55ecbd5b1df6431", size = 138403, upload-time = "2023-07-04T18:37:44.399Z" }, + { url = "https://files.pythonhosted.org/packages/19/e3/88b65e46643006592f39e0fdef3e29454244a9fdaa52acfb047dc68cae6a/typed_ast-1.5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fd946abf3c31fb50eee07451a6aedbfff912fcd13cf357363f5b4e834cc5e71a", size = 222951, upload-time = "2023-07-04T18:37:45.745Z" }, + { url = "https://files.pythonhosted.org/packages/15/e0/182bdd9edb6c6a1c068cecaa87f58924a817f2807a0b0d940f578b3328df/typed_ast-1.5.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ed4a1a42df8a3dfb6b40c3d2de109e935949f2f66b19703eafade03173f8f437", size = 208247, upload-time = "2023-07-04T18:37:47.28Z" }, + { url = "https://files.pythonhosted.org/packages/8d/09/bba083f2c11746288eaf1859e512130420405033de84189375fe65d839ba/typed_ast-1.5.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:045f9930a1550d9352464e5149710d56a2aed23a2ffe78946478f7b5416f1ede", size = 861010, upload-time = "2023-07-04T18:37:48.847Z" }, + { url = "https://files.pythonhosted.org/packages/31/f3/38839df509b04fb54205e388fc04b47627377e0ad628870112086864a441/typed_ast-1.5.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:381eed9c95484ceef5ced626355fdc0765ab51d8553fec08661dce654a935db4", size = 840026, upload-time = "2023-07-04T18:37:50.631Z" }, + { url = "https://files.pythonhosted.org/packages/45/1e/aa5f1dae4b92bc665ae9a655787bb2fe007a881fa2866b0408ce548bb24c/typed_ast-1.5.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bfd39a41c0ef6f31684daff53befddae608f9daf6957140228a08e51f312d7e6", size = 875615, upload-time = "2023-07-04T18:37:52.27Z" }, + { url = "https://files.pythonhosted.org/packages/94/88/71a1c249c01fbbd66f9f28648f8249e737a7fe19056c1a78e7b3b9250eb1/typed_ast-1.5.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8c524eb3024edcc04e288db9541fe1f438f82d281e591c548903d5b77ad1ddd4", size = 858320, upload-time = "2023-07-04T18:37:54.23Z" }, + { url = "https://files.pythonhosted.org/packages/12/1e/19f53aad3984e351e6730e4265fde4b949a66c451e10828fdbc4dfb050f1/typed_ast-1.5.5-cp38-cp38-win_amd64.whl", hash = "sha256:7f58fabdde8dcbe764cef5e1a7fcb440f2463c1bbbec1cf2a86ca7bc1f95184b", size = 139414, upload-time = "2023-07-04T18:37:55.912Z" }, + { url = "https://files.pythonhosted.org/packages/b1/88/6e7f36f5fab6fbf0586a2dd866ac337924b7d4796a4d1b2b04443a864faf/typed_ast-1.5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:042eb665ff6bf020dd2243307d11ed626306b82812aba21836096d229fdc6a10", size = 223329, upload-time = "2023-07-04T18:37:57.344Z" }, + { url = "https://files.pythonhosted.org/packages/71/30/09d27e13824495547bcc665bd07afc593b22b9484f143b27565eae4ccaac/typed_ast-1.5.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:622e4a006472b05cf6ef7f9f2636edc51bda670b7bbffa18d26b255269d3d814", size = 208314, upload-time = "2023-07-04T18:37:59.073Z" }, + { url = "https://files.pythonhosted.org/packages/07/3d/564308b7a432acb1f5399933cbb1b376a1a64d2544b90f6ba91894674260/typed_ast-1.5.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1efebbbf4604ad1283e963e8915daa240cb4bf5067053cf2f0baadc4d4fb51b8", size = 840900, upload-time = "2023-07-04T18:38:00.562Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f4/262512d14f777ea3666a089e2675a9b1500a85b8329a36de85d63433fb0e/typed_ast-1.5.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0aefdd66f1784c58f65b502b6cf8b121544680456d1cebbd300c2c813899274", size = 823435, upload-time = "2023-07-04T18:38:02.532Z" }, + { url = "https://files.pythonhosted.org/packages/a1/25/b3ccb948166d309ab75296ac9863ebe2ff209fbc063f1122a2d3979e47c3/typed_ast-1.5.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:48074261a842acf825af1968cd912f6f21357316080ebaca5f19abbb11690c8a", size = 853125, upload-time = "2023-07-04T18:38:04.128Z" }, + { url = "https://files.pythonhosted.org/packages/1c/09/012da182242f168bb5c42284297dcc08dc0a1b3668db5b3852aec467f56f/typed_ast-1.5.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:429ae404f69dc94b9361bb62291885894b7c6fb4640d561179548c849f8492ba", size = 837280, upload-time = "2023-07-04T18:38:05.968Z" }, + { url = "https://files.pythonhosted.org/packages/30/bd/c815051404c4293265634d9d3e292f04fcf681d0502a9484c38b8f224d04/typed_ast-1.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155", size = 139486, upload-time = "2023-07-04T18:38:07.249Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.7.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/8b/0111dd7d6c1478bf83baa1cab85c686426c7a6274119aceb2bd9d35395ad/typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2", size = 72876, upload-time = "2023-07-02T14:20:55.045Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/6b/63cc3df74987c36fe26157ee12e09e8f9db4de771e0f3404263117e75b95/typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36", size = 33232, upload-time = "2023-07-02T14:20:53.275Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "zipp" +version = "3.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/27/f0ac6b846684cecce1ee93d32450c45ab607f65c2e0255f0092032d91f07/zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b", size = 18454, upload-time = "2023-02-25T02:17:22.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/fa/c9e82bbe1af6266adf08afb563905eb87cab83fde00a0a08963510621047/zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556", size = 6758, upload-time = "2023-02-25T02:17:20.807Z" }, +] From 9ed10e7e1d054e7ccfa338bb30930c41222f17af Mon Sep 17 00:00:00 2001 From: Reiase Date: Sun, 28 Jun 2026 00:43:25 +0800 Subject: [PATCH 2/2] Update dependencies and enhance error handling across modules - Added `pyo3` and updated `thiserror` to version `2.0.12` in `Cargo.lock` for improved Python integration and error management. - Refactored error handling in various modules to utilize `Context` for better error context propagation. - Updated documentation to reflect changes in modularity and signal handling, including the addition of macOS-specific signal delivery methods. - Improved overall code clarity and consistency by simplifying function signatures and enhancing comments across multiple files. --- Cargo.lock | 4 +- docs/src/design/modularity.md | 3 +- docs/src/design/modularity.zh.md | 3 +- probing/cli/src/cli/bench/runners/coldscan.rs | 6 +- probing/cli/src/cli/bench/runners/mixed.rs | 7 +- probing/cli/src/cli/bench/runners/mp.rs | 5 +- probing/cli/src/cli/bench/runners/write.rs | 4 +- probing/cli/src/cli/ctrl.rs | 6 +- probing/cli/src/cli/repl.rs | 8 +- probing/core/Cargo.toml | 4 + probing/core/src/core/cluster.rs | 4 +- probing/core/src/core/error.rs | 208 +++++----- .../src/core/federation/aggregate_pushdown.rs | 8 +- .../src/core/federation/cluster_executor.rs | 17 +- probing/core/src/core/federation/convert.rs | 21 +- probing/core/src/core/memtable_sql.rs | 24 +- probing/core/src/core/mod.rs | 2 + probing/core/src/core/plugin_advanced.rs | 10 +- probing/core/src/core/probe_extension.rs | 3 +- probing/core/src/core/semantic_catalog.rs | 9 +- probing/core/src/lib.rs | 21 +- probing/core/src/runtime.rs | 385 ++++++++++++++---- probing/core/src/signal/macos.rs | 136 +++++++ probing/core/src/signal/mod.rs | 7 + probing/core/src/storage/distributed.rs | 78 ++-- probing/core/src/storage/entity.rs | 2 +- probing/core/src/storage/mem_store.rs | 2 +- probing/core/src/storage/mod.rs | 2 +- probing/core/src/storage/remote_client.rs | 2 +- probing/core/src/trace/mod.rs | 3 +- .../cc/src/extensions/cpu/collector.rs | 17 +- .../cc/src/extensions/cpu/extension.rs | 8 +- .../extensions/cc/src/extensions/cpu/macos.rs | 72 ---- .../extensions/cc/src/extensions/cpu/mod.rs | 2 +- probing/extensions/cc/src/extensions/files.rs | 23 +- probing/extensions/cc/src/extensions/rdma.rs | 57 ++- .../gpu/src/extensions/backend/registry.rs | 5 +- .../gpu/src/extensions/collector.rs | 15 +- .../gpu/src/extensions/extension.rs | 8 +- probing/extensions/python/Cargo.toml | 4 +- .../extensions/python/src/extensions/pprof.rs | 18 +- .../python/src/extensions/python.rs | 91 +++-- .../python/src/extensions/python/exttbls.rs | 65 ++- .../python/src/extensions/python/tbls.rs | 86 ++-- .../extensions/python/src/features/config.rs | 64 ++- .../extensions/python/src/features/convert.rs | 9 +- probing/extensions/python/src/features/mod.rs | 1 + .../python/src/features/native_bridge.rs | 4 +- .../extensions/python/src/features/pprof.rs | 4 +- .../python/src/features/py_result.rs | 7 + .../python/src/features/python_api.rs | 14 +- .../python/src/features/stack_tracer.rs | 4 +- .../extensions/python/src/features/torch.rs | 9 +- .../extensions/python/src/features/tracing.rs | 2 +- probing/extensions/python/src/python.rs | 3 +- .../extensions/python/src/repl/python_repl.rs | 9 +- probing/memtable/Cargo.toml | 1 + probing/memtable/src/discover.rs | 95 +++-- probing/memtable/src/docs.rs | 8 +- probing/memtable/src/error.rs | 30 ++ probing/memtable/src/lib.rs | 5 +- probing/memtable/src/memh/table.rs | 34 +- probing/memtable/src/memtable.rs | 25 +- probing/memtable/src/raw.rs | 51 ++- probing/memtable/src/sync.rs | 11 + probing/server/Cargo.toml | 2 +- probing/server/src/engine.rs | 55 ++- probing/server/src/report.rs | 4 +- probing/server/src/server/cluster_fanout.rs | 2 +- probing/server/src/torchrun_cluster.rs | 5 +- .../probing/core/table_docs_integration.rs | 2 +- web/src/api/mod.rs | 10 +- 72 files changed, 1222 insertions(+), 713 deletions(-) create mode 100644 probing/core/src/signal/macos.rs create mode 100644 probing/core/src/signal/mod.rs create mode 100644 probing/extensions/python/src/features/py_result.rs create mode 100644 probing/memtable/src/error.rs create mode 100644 probing/memtable/src/sync.rs diff --git a/Cargo.lock b/Cargo.lock index 870ac321..66cb2e0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3367,6 +3367,7 @@ dependencies = [ "probing-memtable", "probing-nccl-profiler", "probing-proto", + "pyo3", "serde", "serde_json", "serde_yaml", @@ -3434,6 +3435,7 @@ dependencies = [ "log", "memmap2", "pco", + "thiserror 2.0.12", "xxhash-rust", ] @@ -3491,7 +3493,6 @@ dependencies = [ "log", "nix 0.31.3", "once_cell", - "probing-cc", "probing-cli", "probing-core", "probing-memtable", @@ -3506,6 +3507,7 @@ dependencies = [ "sha2", "signal-hook-registry", "tempfile", + "thiserror 2.0.12", "tokio", "uuid", ] diff --git a/docs/src/design/modularity.md b/docs/src/design/modularity.md index e48764a9..049eb57d 100644 --- a/docs/src/design/modularity.md +++ b/docs/src/design/modularity.md @@ -329,6 +329,7 @@ Use this table to decide **where a change belongs**: | mmap format, compaction | probing-memtable | `RowWriter`, `ColdStore` | | Torchrun cluster heartbeat | probing-server | `torchrun_cluster.rs`, `cluster_report_backoff.rs`, PUT `/apis/nodes` | | Mixed Python/C stack | probing-python/features | `python.backtrace`, pprof | +| macOS per-thread SIGUSR2 | probing-core | `signal::send_sigusr2_to_thread_id` | | Torch module sampling | python/probing/profiling | `python.torch_trace` | | Collective wall time | python/probing/profiling/collective | `python.comm_collective` | | NCCL wait decomposition | probing-nccl-profiler | `nccl.proxy_ops` | @@ -374,7 +375,7 @@ Track and fix incrementally: | Issue | Current | Target | |-------|---------|--------| | Python ext → CLI | `probing-python` → `probing-cli` | **Accepted** for maturin wheel (`cli_main` only); keep import surface minimal | -| Python ext → CC | `probing-python` → `probing-cc` (stack tracer SIGUSR2) | Move signal helper to L1 shared util or thin `probing-signals` crate | +| Python ext → CC | ~~`probing-python` → `probing-cc`~~ | **Done** — `send_sigusr2_to_thread_id` moved to `probing-core::signal` | | Core → NCCL/HCCL | `probing-core` → `probing-nccl-profiler` / `probing-hccl-shim` (`builtin-schema-docs` feature) for `semantic_catalog` | Register docs via collector hooks or manifest; drop L1→L2 compile deps | | Core → skills YAML | `semantic_catalog.rs` `include_str!(skills/semantic/tables.yaml)` | Accept as L4 overlay SSOT, or move YAML under `probing/core/resources/` | | Server → python `features/*` | ~~`server/profiling.rs`~~ removed | Flamegraphs via `torchextension` / `pprofextension` `ProbeExtensionCall` | diff --git a/docs/src/design/modularity.zh.md b/docs/src/design/modularity.zh.md index 096845b4..d1b36723 100644 --- a/docs/src/design/modularity.zh.md +++ b/docs/src/design/modularity.zh.md @@ -273,6 +273,7 @@ sequenceDiagram | mmap 格式、冷压缩 | probing-memtable | `RowWriter`, `ColdStore` | | Torchrun 集群心跳 | probing-server | `torchrun_cluster.rs`、`cluster_report_backoff.rs`、`PUT /apis/nodes` | | 混合 Python/C 栈 | probing-python/features | `python.backtrace`、pprof | +| macOS 线程 SIGUSR2 | probing-core | `signal::send_sigusr2_to_thread_id` | | Torch 模块采样 | python/profiling | `python.torch_trace` | | Collective 墙钟 | python/profiling/collective | `python.comm_collective` | | NCCL wait 分解 | nccl-profiler | `nccl.proxy_ops` | @@ -315,7 +316,7 @@ sequenceDiagram | 问题 | 现状 | 目标 | |------|------|------| | python → cli | `probing-python` → `probing-cli` | **可接受**(maturin wheel,仅 `cli_main`);禁止扩散 import | -| python → cc | `probing-python` → `probing-cc`(stack tracer 发 SIGUSR2) | 信号 helper 下沉 L1 或独立 `probing-signals` | +| python → cc | ~~`probing-python` → `probing-cc`~~ | **已完成** — `send_sigusr2_to_thread_id` 迁至 `probing-core::signal` | | core → NCCL/HCCL | `probing-core` → nccl/hccl shim(`builtin-schema-docs`)供 `semantic_catalog` | 改由采集器注册 docs,去掉 L1→L2 编译依赖 | | core → skills YAML | `semantic_catalog` `include_str!(skills/semantic/tables.yaml)` | 接受为 L4 overlay SSOT,或迁到 `probing/core/resources/` | | server → python features | ~~`server/profiling.rs`~~ 已删 | 火焰图走 Extension | diff --git a/probing/cli/src/cli/bench/runners/coldscan.rs b/probing/cli/src/cli/bench/runners/coldscan.rs index 19ffbe44..46161571 100644 --- a/probing/cli/src/cli/bench/runners/coldscan.rs +++ b/probing/cli/src/cli/bench/runners/coldscan.rs @@ -6,7 +6,7 @@ use std::time::Instant; -use anyhow::{bail, Result}; +use anyhow::{bail, Context, Result}; use probing_memtable::memc::{ColdStore, SegmentReader}; use crate::cli::bench::args::ColdscanArgs; @@ -48,8 +48,8 @@ pub fn run(args: &ColdscanArgs, json: bool, seed: u64) -> Result<()> { let mut rows = 0u64; let mut disk = 0u64; for path in &segments { - let reader = SegmentReader::open(path) - .map_err(|e| anyhow::anyhow!("open {}: {e}", path.display()))?; + let reader = + SegmentReader::open(path).with_context(|| format!("open {}", path.display()))?; for (i, page) in reader.pages().iter().enumerate() { disk += page.block_len as u64; let cols = reader diff --git a/probing/cli/src/cli/bench/runners/mixed.rs b/probing/cli/src/cli/bench/runners/mixed.rs index 1fbb6bda..516e429f 100644 --- a/probing/cli/src/cli/bench/runners/mixed.rs +++ b/probing/cli/src/cli/bench/runners/mixed.rs @@ -7,7 +7,7 @@ use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::Arc; use std::time::{Duration, Instant}; -use anyhow::{bail, Result}; +use anyhow::{bail, Context, Result}; use probing_memtable::memc::{ColdStore, Compactor, CompactorConfig}; use probing_memtable::{DType, MemTable}; @@ -58,7 +58,10 @@ pub fn run(args: &MixedArgs, json: bool, seed: u64) -> Result<()> { args.ring.chunk_size, args.ring.chunks, )?; - let path = creator.path().expect("shared path").to_path_buf(); + let path = creator + .path() + .context("shared memtable has no file path")? + .to_path_buf(); (Attach::File(path), creator) } }; diff --git a/probing/cli/src/cli/bench/runners/mp.rs b/probing/cli/src/cli/bench/runners/mp.rs index b96e923c..8edca3f9 100644 --- a/probing/cli/src/cli/bench/runners/mp.rs +++ b/probing/cli/src/cli/bench/runners/mp.rs @@ -78,7 +78,10 @@ fn orchestrate(args: &MpArgs, json: bool, seed: u64) -> Result<()> { args.ring.chunk_size, args.ring.chunks, )?; - let path = creator.path().expect("shared path").to_path_buf(); + let path = creator + .path() + .context("shared memtable has no file path")? + .to_path_buf(); (Attach::File(path), creator) } }; diff --git a/probing/cli/src/cli/bench/runners/write.rs b/probing/cli/src/cli/bench/runners/write.rs index 11a242fb..74ae6a55 100644 --- a/probing/cli/src/cli/bench/runners/write.rs +++ b/probing/cli/src/cli/bench/runners/write.rs @@ -7,7 +7,7 @@ use std::sync::Barrier; use std::time::Instant; -use anyhow::{bail, Result}; +use anyhow::{bail, Context, Result}; use probing_memtable::MemTable; use super::common; @@ -94,7 +94,7 @@ pub fn run(args: &WriteArgs, json: bool, seed: u64) -> Result<()> { )?; let path = creator .path() - .expect("shared table has a path") + .context("shared memtable has no file path")? .to_path_buf(); (Source::File(path), Some(creator)) } diff --git a/probing/cli/src/cli/ctrl.rs b/probing/cli/src/cli/ctrl.rs index dabb1b86..8d5ef813 100644 --- a/probing/cli/src/cli/ctrl.rs +++ b/probing/cli/src/cli/ctrl.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use std::io::Write; use http_body_util::{BodyExt, Full}; @@ -232,13 +232,13 @@ pub async fn request(ctrl: ProbeEndpoint, url: &str, body: Option) -> Re .method("POST") .uri(url) .body(Full::::from(body)) - .map_err(|e| anyhow::anyhow!("Failed to build POST request: {e}"))? + .context("Failed to build POST request")? } else { Request::builder() .method("GET") .uri(url) .body(Full::::default()) - .map_err(|e| anyhow::anyhow!("Failed to build GET request: {e}"))? + .context("Failed to build GET request")? }; let res = sender.send_request(request).await?; diff --git a/probing/cli/src/cli/repl.rs b/probing/cli/src/cli/repl.rs index 82150951..591e23f8 100644 --- a/probing/cli/src/cli/repl.rs +++ b/probing/cli/src/cli/repl.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use futures_util::sink::Sink; use futures_util::stream::Stream; use futures_util::{SinkExt, StreamExt}; @@ -45,7 +45,7 @@ pub async fn start_repl(ctrl: ProbeEndpoint) -> Result<()> { editor.read_line(&prompt) }) .await - .map_err(|e| anyhow::anyhow!("Error reading input task: {}", e))?; + .context("Error reading input task")?; match sig { Ok(Signal::Success(line)) => { @@ -155,7 +155,7 @@ async fn connect_tcp_websocket(addr: &str) -> Result { let url = format!("ws://{}/ws", addr); let (ws_stream, _) = connect_async(&url) .await - .map_err(|e| anyhow::anyhow!("WebSocket connection failed: {}", e))?; + .context("WebSocket connection failed")?; Ok(boxed_connection(ws_stream)) } @@ -182,7 +182,7 @@ async fn connect_unix_websocket(pid: i32) -> Result { let (ws_stream, _) = client_async("ws://localhost/ws", stream) .await - .map_err(|e| anyhow::anyhow!("WebSocket connection failed: {}", e))?; + .context("WebSocket connection failed")?; Ok(boxed_connection(ws_stream)) } diff --git a/probing/core/Cargo.toml b/probing/core/Cargo.toml index 2795ac16..4df89a93 100644 --- a/probing/core/Cargo.toml +++ b/probing/core/Cargo.toml @@ -26,6 +26,7 @@ similar_names = "allow" test-utils = [] default = ["builtin-schema-docs"] builtin-schema-docs = ["dep:probing-hccl-shim", "dep:probing-nccl-profiler"] +python-bridge = ["dep:pyo3"] [lib] crate-type = ["rlib"] @@ -57,6 +58,9 @@ bincode = "1.3.3" uuid = { version = "1.0", features = ["v4", "serde"] } url = "2.5" libc = "0.2" +pyo3 = { version = "0.29.0", optional = true, default-features = false, features = [ + "macros", +] } [dev-dependencies] tempfile = "3.8" diff --git a/probing/core/src/core/cluster.rs b/probing/core/src/core/cluster.rs index 3d64c0d5..4d059a06 100644 --- a/probing/core/src/core/cluster.rs +++ b/probing/core/src/core/cluster.rs @@ -59,8 +59,8 @@ static CLUSTER_VERSION: AtomicU64 = AtomicU64::new(0); fn now_micros() -> u64 { std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_micros() as u64 + .map(|d| d.as_micros() as u64) + .unwrap_or(0) } fn stale_threshold_micros() -> u64 { diff --git a/probing/core/src/core/error.rs b/probing/core/src/core/error.rs index 96d0d648..b14632e4 100644 --- a/probing/core/src/core/error.rs +++ b/probing/core/src/core/error.rs @@ -1,173 +1,185 @@ //! Error handling for the Probing engine //! -//! This module provides a comprehensive error handling system for the Probing engine. -//! It defines a structured error type hierarchy and conversion capabilities from common -//! external error types. +//! This module defines a single, structured error type ([`EngineError`]) for the +//! whole `probing-core` crate, together with the conversions that wire every +//! sub-system (storage, addressing, runtime, tracing, DataFusion, Arrow, …) into +//! one coherent propagation chain. +//! +//! Design principles +//! ----------------- +//! * **One error type per crate.** Everything funnels into [`EngineError`]; the +//! `?` operator works across layers without hand-written `map_err`. +//! * **Preserve the source chain.** Wrapping variants carry `#[source]`/`#[from]` +//! so `std::error::Error::source()` walks the real cause, instead of flattening +//! everything into an opaque string. +//! * **No stringly-typed coercion.** There is intentionally *no* +//! `From`/`From<&str>`; build errors through the explicit constructors +//! or the typed variants so categorisation is never lost by accident. +//! * **One boundary conversion.** [`EngineError`] converts to +//! [`datafusion::error::DataFusionError`] in a single place, so DataFusion trait +//! implementations can just use `?` instead of re-inventing the mapping. use thiserror::Error; -/// Core result type for all Probing engine operations +use datafusion::error::DataFusionError; + +/// Core result type for all Probing engine operations. pub type Result = std::result::Result; -/// Comprehensive error type for the Probing engine +/// Comprehensive error type for the Probing engine. /// -/// Categorizes errors into logical groups to help with error handling and reporting. +/// Variants are grouped by sub-system. Variants that wrap a foreign error keep +/// that error as their [`source`](std::error::Error::source) so the full causal +/// chain survives propagation. #[derive(Error, Debug)] pub enum EngineError { // ===== Plugin System Errors ===== - /// Generic plugin error + /// Generic plugin error. #[error("Plugin error: {0}")] PluginError(String), - /// Plugin not found error + /// Plugin not found error. #[error("Plugin not found: {0}")] PluginNotFound(String), - /// Plugin registration failure - #[error("Plugin registration failed: {0}")] - PluginRegistrationFailed(String), - // ===== Query Processing Errors ===== - /// General query execution error + /// General query execution error. #[error("Query execution error: {0}")] QueryError(String), - /// Internal engine error + /// Internal engine error. #[error("Internal engine error: {0}")] InternalError(String), - /// Error during external API call + /// Error during external API call. #[error("API call error: {0}")] CallError(String), - /// Unsupported API call + /// Unsupported API call. #[error("Unsupported API call")] UnsupportedCall, // ===== Data Processing Errors ===== - /// Apache Arrow data processing error - #[error("Arrow data error: {0}")] + /// Apache Arrow data processing error. + #[error(transparent)] ArrowError(#[from] arrow::error::ArrowError), - /// DataFusion query processing error - #[error("DataFusion error: {0}")] - DataFusionError(#[from] datafusion::error::DataFusionError), + /// DataFusion query processing error. + #[error(transparent)] + DataFusionError(#[from] DataFusionError), + + /// (De)serialization failure (bincode payloads in the storage layer). + #[error("Serialization error: {0}")] + Serialization(#[from] bincode::Error), // ===== Business Logic Errors ===== - /// Cluster management error + /// Cluster management error. #[error("Cluster error: {0}")] ClusterError(String), + /// Distributed/local storage error. + #[error("Storage error: {0}")] + Storage(String), + + /// Object addressing failure (URI parsing, allocation, …). + #[error(transparent)] + Address(#[from] crate::storage::addressing::AddressError), + // ===== System Errors ===== - /// Thread/mutex concurrency error + /// I/O error (filesystem, sockets, etc.). + #[error(transparent)] + Io(#[from] std::io::Error), + + /// Async runtime bridge failure. + #[error(transparent)] + Runtime(#[from] crate::runtime::RuntimeError), + + /// Tracing/span operation failure. + #[error(transparent)] + Trace(#[from] crate::trace::TraceError), + + /// Thread/mutex concurrency error. #[error("Concurrency error: {0}")] ConcurrencyError(String), // ===== Configuration Errors ===== - /// General configuration error + /// General configuration error. #[error("Configuration error: {0}")] ConfigError(String), - /// Unsupported configuration option + /// Unsupported configuration option. #[error("Unsupported option: {0}")] UnsupportedOption(String), - /// Invalid configuration option value + /// Invalid configuration option value. #[error("Invalid option value: {0}={1}")] InvalidOptionValue(String, String), - /// Attempt to modify read-only option + /// Attempt to modify a read-only option. #[error("Read-only option: {0}")] ReadOnlyOption(String), - /// Engine not initialized error - #[error("Engine not initialized")] - EngineNotInitialized, + /// Memtable mmap / validation failure (from `probing-memtable`). + #[error(transparent)] + Memtable(#[from] probing_memtable::MemtableError), } impl EngineError { - pub fn with_context>(self, context: C) -> EngineError { - let context = context.into(); - - // Helper macro to reduce boilerplate for string-based variants - macro_rules! add_context { - ($variant:path, $msg:expr) => { - $variant(format!("{}: {}", context, $msg)) - }; - } + /// Build a [`EngineError::PluginError`] without boilerplate. + pub fn plugin(msg: impl Into) -> Self { + Self::PluginError(msg.into()) + } - match self { - // String-based error variants that can have context added - EngineError::PluginError(msg) => add_context!(EngineError::PluginError, msg), - EngineError::PluginNotFound(msg) => add_context!(EngineError::PluginNotFound, msg), - EngineError::PluginRegistrationFailed(msg) => { - add_context!(EngineError::PluginRegistrationFailed, msg) - } - EngineError::QueryError(msg) => add_context!(EngineError::QueryError, msg), - EngineError::InternalError(msg) => add_context!(EngineError::InternalError, msg), - EngineError::CallError(msg) => add_context!(EngineError::CallError, msg), - EngineError::ClusterError(msg) => add_context!(EngineError::ClusterError, msg), - EngineError::ConcurrencyError(msg) => add_context!(EngineError::ConcurrencyError, msg), - EngineError::ConfigError(msg) => add_context!(EngineError::ConfigError, msg), - EngineError::UnsupportedOption(msg) => { - add_context!(EngineError::UnsupportedOption, msg) - } - EngineError::ReadOnlyOption(msg) => add_context!(EngineError::ReadOnlyOption, msg), - - // Error variants that cannot or should not have context added - e @ (EngineError::UnsupportedCall - | EngineError::ArrowError(_) - | EngineError::DataFusionError(_) - | EngineError::InvalidOptionValue(_, _) - | EngineError::EngineNotInitialized) => e, - } + /// Build a [`EngineError::QueryError`]. + pub fn query(msg: impl Into) -> Self { + Self::QueryError(msg.into()) } -} -// Generic lock poison error conversion -impl From> for EngineError { - fn from(err: std::sync::PoisonError) -> Self { - EngineError::ConcurrencyError(format!("Lock poisoned: {err}")) + /// Build a [`EngineError::InternalError`]. + pub fn internal(msg: impl Into) -> Self { + Self::InternalError(msg.into()) } -} -// String conversion for simple error creation -impl From for EngineError { - fn from(message: String) -> Self { - EngineError::InternalError(message) + /// Build a [`EngineError::ClusterError`]. + pub fn cluster(msg: impl Into) -> Self { + Self::ClusterError(msg.into()) } -} -impl From<&str> for EngineError { - fn from(message: &str) -> Self { - EngineError::InternalError(message.to_string()) + /// Build a [`EngineError::ConfigError`]. + pub fn config(msg: impl Into) -> Self { + Self::ConfigError(msg.into()) } -} -#[allow(unused)] -pub trait ResultExt { - fn context>(self, context: C) -> Result; -} + /// Build a [`EngineError::Storage`]. + pub fn storage(msg: impl Into) -> Self { + Self::Storage(msg.into()) + } -impl> ResultExt for std::result::Result { - fn context>(self, context: C) -> Result { - self.map_err(|e| { - let err = e.into(); - err.with_context(context.into()) - }) + /// Build a [`EngineError::InvalidOptionValue`] from an option name and detail. + pub fn invalid_option(option: impl Into, detail: impl std::fmt::Display) -> Self { + Self::InvalidOptionValue(option.into(), detail.to_string()) } } -#[allow(unused)] -pub fn ensure(condition: bool, message: impl Into) -> Result<()> { - if condition { - Ok(()) - } else { - Err(EngineError::InternalError(message.into())) +// Generic lock poison error conversion. +impl From> for EngineError { + fn from(err: std::sync::PoisonError) -> Self { + EngineError::ConcurrencyError(format!("Lock poisoned: {err}")) } } -#[allow(unused)] -pub fn bail(message: impl Into) -> Result { - Err(EngineError::InternalError(message.into())) +/// Single boundary conversion into DataFusion's error type. +/// +/// DataFusion trait implementations (`TableProvider`, `ExecutionPlan`, …) must +/// return [`DataFusionError`]. Centralising the mapping here means call sites can +/// simply use `?` / `.map_err(DataFusionError::from)` instead of hand-rolling a +/// `DataFusionError::Execution(format!(...))` everywhere. +impl From for DataFusionError { + fn from(err: EngineError) -> Self { + match err { + EngineError::DataFusionError(e) => e, + EngineError::ArrowError(e) => DataFusionError::ArrowError(Box::new(e), None), + other => DataFusionError::External(Box::new(other)), + } + } } diff --git a/probing/core/src/core/federation/aggregate_pushdown.rs b/probing/core/src/core/federation/aggregate_pushdown.rs index 2edb5b19..e6d295d0 100644 --- a/probing/core/src/core/federation/aggregate_pushdown.rs +++ b/probing/core/src/core/federation/aggregate_pushdown.rs @@ -426,10 +426,10 @@ fn build_global_merge_sql( } fn quote_ident(name: &str) -> String { - if name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') - && !name.is_empty() - && !name.chars().next().unwrap().is_ascii_digit() - { + let safe_unquoted = !name.is_empty() + && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') + && !name.as_bytes().first().is_some_and(|b| b.is_ascii_digit()); + if safe_unquoted { name.to_string() } else { format!("\"{name}\"") diff --git a/probing/core/src/core/federation/cluster_executor.rs b/probing/core/src/core/federation/cluster_executor.rs index 0562f3fd..560346dc 100644 --- a/probing/core/src/core/federation/cluster_executor.rs +++ b/probing/core/src/core/federation/cluster_executor.rs @@ -24,6 +24,10 @@ const DEFAULT_REMOTE_QUERY_TIMEOUT_SECS: u64 = 2; /// Env var to override the per-node remote query timeout (seconds). const REMOTE_QUERY_TIMEOUT_ENV: &str = "PROBING_REMOTE_QUERY_TIMEOUT_SECS"; +fn external(err: E) -> DataFusionError { + DataFusionError::External(Box::new(err)) +} + /// Per-node timeout for remote federated queries. /// /// Defaults to [`DEFAULT_REMOTE_QUERY_TIMEOUT_SECS`]; override via the @@ -179,29 +183,24 @@ impl ProbeClusterExecutor { expr: sql.to_string(), ..Default::default() }); - let body = - serde_json::to_string(&request).map_err(|e| DataFusionError::External(Box::new(e)))?; + let body = serde_json::to_string(&request).map_err(external)?; let addr_owned = addr.to_string(); let response = ureq::post(&url) .config() .timeout_global(Some(remote_query_timeout())) .build() .send(body) - .map_err(|e| DataFusionError::External(Box::new(e)))?; + .map_err(external)?; let status = response.status().as_u16(); - let text = response - .into_body() - .read_to_string() - .map_err(|e| DataFusionError::External(Box::new(e)))?; + let text = response.into_body().read_to_string().map_err(external)?; if status >= 400 { return Err(DataFusionError::Execution(format!( "remote query {addr_owned} failed: HTTP {status}: {text}" ))); } - let msg: Message = - serde_json::from_str(&text).map_err(|e| DataFusionError::External(Box::new(e)))?; + let msg: Message = serde_json::from_str(&text).map_err(external)?; match msg.payload { QueryDataFormat::DataFrame(df) => Ok(df), QueryDataFormat::Nil => Ok(DataFrame::default()), diff --git a/probing/core/src/core/federation/convert.rs b/probing/core/src/core/federation/convert.rs index 40690152..16aae064 100644 --- a/probing/core/src/core/federation/convert.rs +++ b/probing/core/src/core/federation/convert.rs @@ -32,6 +32,11 @@ pub const FEDERATION_TAG_COLUMNS: &[&str] = &[ PROBE_ROLE_COL, ]; +fn record_batch(schema: Schema, columns: Vec, ctx: &'static str) -> Result { + RecordBatch::try_new(Arc::new(schema), columns) + .map_err(|e| DataFusionError::Execution(format!("{ctx}: {e}"))) +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct FederationEndpointTags { pub host: String, @@ -189,8 +194,11 @@ pub fn proto_dataframe_to_record_batch(df: &DataFrame) -> Result { fields.push(Field::new(name, array_data_type(col), true)); columns.push(seq_to_array(col)?); } - RecordBatch::try_new(Arc::new(Schema::new(fields)), columns) - .map_err(|e| DataFusionError::Execution(format!("proto dataframe conversion failed: {e}"))) + record_batch( + Schema::new(fields), + columns, + "proto dataframe conversion failed", + ) } /// Honor the caller's column projection for `global.*` scans. @@ -252,8 +260,7 @@ pub fn dataframe_to_record_batch( columns.push(Arc::new(Int32Array::from(vec![tags.local_rank; rows]))); columns.push(Arc::new(StringArray::from(vec![tags.role; rows]))); - RecordBatch::try_new(Arc::new(Schema::new(fields)), columns) - .map_err(|e| DataFusionError::Execution(format!("dataframe conversion failed: {e}"))) + record_batch(Schema::new(fields), columns, "dataframe conversion failed") } pub fn tag_record_batch( @@ -276,8 +283,7 @@ pub fn tag_record_batch( append_batch_tags(&mut fields, &mut columns, rows, &tags)?; - RecordBatch::try_new(Arc::new(Schema::new(fields)), columns) - .map_err(|e| DataFusionError::Execution(format!("tagging batch failed: {e}"))) + record_batch(Schema::new(fields), columns, "tagging batch failed") } fn append_batch_tags( @@ -338,8 +344,7 @@ pub fn align_batch_to_schema(batch: RecordBatch, schema: &Schema) -> Result DataType { diff --git a/probing/core/src/core/memtable_sql.rs b/probing/core/src/core/memtable_sql.rs index fc59dd67..14413bda 100644 --- a/probing/core/src/core/memtable_sql.rs +++ b/probing/core/src/core/memtable_sql.rs @@ -56,7 +56,9 @@ use probing_memtable::discover::{default_dir, MappedFile}; use probing_memtable::memc::{ ColdStats, ColdStore, ColumnData, Compactor, CompactorConfig, SegmentReader, }; -use probing_memtable::{detect_table, DType, MemTableView, MemhView, TableKind, TypedValue}; +use probing_memtable::{ + detect_table, DType, MemTableView, MemhView, MemtableError, TableKind, TypedValue, +}; use super::plugin_advanced::{scan_memory_partitions, supports_filters_pushdown_for_schema}; use super::{ @@ -532,7 +534,7 @@ pub struct RingMmapTable { } impl RingMmapTable { - pub fn try_new(mapped: MappedFile) -> Result { + pub fn try_new(mapped: MappedFile) -> Result { let view = MemTableView::new(mapped.as_bytes())?; let schema = view_to_arrow_schema(&view); Ok(Self { @@ -1199,7 +1201,7 @@ impl ColdCompactor { let running = self.running.clone(); let poll = cfg.poll; - let handle = std::thread::Builder::new() + match std::thread::Builder::new() .name("memc-compactor".into()) .spawn(move || { while running.load(Ordering::SeqCst) { @@ -1224,9 +1226,15 @@ impl ColdCompactor { if let Err(e) = compactor.flush() { log::debug!("cold compactor: final flush: {e}"); } - }) - .expect("spawn memc-compactor thread"); - *lock_compactor_handle(&self.handle) = Some(handle); + }) { + Ok(handle) => { + *lock_compactor_handle(&self.handle) = Some(handle); + } + Err(e) => { + log::error!("cold compactor: failed to spawn background thread: {e}"); + self.running.store(false, Ordering::SeqCst); + } + } } /// Signal the thread to flush and exit, then join it. @@ -1925,7 +1933,7 @@ mod tests { let schema = MtSchema::new().col("ts", DType::I64).col("msg", DType::Str); let mut table = ExposedTable::create("test_metrics", &schema, 4096, 2).unwrap(); { - let mut w = table.writer(); + let mut w = table.writer().expect("valid mmap table"); w.push_row(&[Value::I64(100), Value::Str("alpha")]); w.push_row(&[Value::I64(200), Value::Str("beta")]); } @@ -1980,7 +1988,7 @@ mod tests { let dotted = mmap_filename_for("acme", "metrics_demo"); let mut ring = ExposedTable::create(&dotted, &schema, 4096, 2).unwrap(); { - let mut w = ring.writer(); + let mut w = ring.writer().expect("valid mmap table"); w.push_row(&[Value::I64(1), Value::Str("x")]); } diff --git a/probing/core/src/core/mod.rs b/probing/core/src/core/mod.rs index 0c15d98c..518806f8 100644 --- a/probing/core/src/core/mod.rs +++ b/probing/core/src/core/mod.rs @@ -19,6 +19,8 @@ pub use engine::EngineBuilder; pub use error::EngineError; pub use error::Result; +pub use probing_memtable::MemtableError; + pub use data_source::CustomNamespace; pub use data_source::CustomNamespaceDataSource; pub use data_source::CustomTable; diff --git a/probing/core/src/core/plugin_advanced.rs b/probing/core/src/core/plugin_advanced.rs index e7c87dd1..7a5bc9b0 100644 --- a/probing/core/src/core/plugin_advanced.rs +++ b/probing/core/src/core/plugin_advanced.rs @@ -10,9 +10,10 @@ use std::fmt::Debug; use std::sync::Arc; use async_trait::async_trait; +#[cfg(test)] use datafusion::arrow::array::Int64Array; use datafusion::arrow::datatypes::{DataType, Field, Schema, SchemaRef}; -use datafusion::arrow::record_batch::{RecordBatch, RecordBatchOptions}; +use datafusion::arrow::record_batch::RecordBatch; use datafusion::catalog::Session; use datafusion::common::tree_node::TreeNode; use datafusion::common::DFSchema; @@ -92,12 +93,7 @@ impl PluginAdvancedTable { DataType::Int64, true, )])); - let empty = RecordBatch::try_new_with_options( - Arc::clone(&schema), - vec![Arc::new(Int64Array::from(Vec::::new()))], - &RecordBatchOptions::new().with_row_count(Some(0)), - ) - .expect("empty batch"); + let empty = RecordBatch::new_empty(Arc::clone(&schema)); Self { label, schema, diff --git a/probing/core/src/core/probe_extension.rs b/probing/core/src/core/probe_extension.rs index 6de73418..ac8c81dc 100644 --- a/probing/core/src/core/probe_extension.rs +++ b/probing/core/src/core/probe_extension.rs @@ -422,8 +422,7 @@ impl ExtensionOptions for ProbeExtensionManager { fn set(&mut self, key: &str, value: &str) -> datafusion::error::Result<()> { use futures::executor::block_on; - block_on(self.set_option(key, value)) - .map_err(|e| datafusion::error::DataFusionError::Execution(e.to_string())) + block_on(self.set_option(key, value)).map_err(datafusion::error::DataFusionError::from) } fn entries(&self) -> Vec { diff --git a/probing/core/src/core/semantic_catalog.rs b/probing/core/src/core/semantic_catalog.rs index 6967bceb..8bf78e66 100644 --- a/probing/core/src/core/semantic_catalog.rs +++ b/probing/core/src/core/semantic_catalog.rs @@ -202,8 +202,13 @@ use datafusion::catalog::TableProvider; static SEMANTIC_COLUMN_INDEX: LazyLock>> = LazyLock::new(|| { - let yaml = parse_semantic_catalog_yaml(TABLES_YAML) - .expect("skills/semantic/tables.yaml must parse"); + let yaml = match parse_semantic_catalog_yaml(TABLES_YAML) { + Ok(yaml) => yaml, + Err(e) => { + log::error!("skills/semantic/tables.yaml failed to parse: {e}"); + return HashMap::new(); + } + }; let mut map: HashMap<(String, String), Vec> = HashMap::new(); for row in yaml.column_rows { map.entry((row.table_schema, row.table_name)) diff --git a/probing/core/src/lib.rs b/probing/core/src/lib.rs index 2753c8c0..9d03995b 100644 --- a/probing/core/src/lib.rs +++ b/probing/core/src/lib.rs @@ -4,6 +4,7 @@ pub mod config; pub mod core; pub mod diagnostics; pub mod runtime; +pub mod signal; pub mod storage; pub mod sync; pub mod trace; @@ -12,7 +13,7 @@ mod tracing; pub use diagnostics::install_panic_hook; pub use runtime::{ block_on, is_python_main_thread, register_python_main_thread, run_on_native_thread, - CORE_RUNTIME, + runtime_operational, BlockOnFallback, RuntimeError, CORE_RUNTIME, }; use self::core::Engine; @@ -22,23 +23,19 @@ pub fn create_engine() -> EngineBuilder { Engine::builder().with_default_namespace("probe") } -use anyhow::Result; use once_cell::sync::Lazy; use tokio::sync::RwLock; +use self::core::Result; + pub static ENGINE: Lazy> = Lazy::new(|| RwLock::new(Engine::default())); pub async fn initialize_engine(builder: EngineBuilder) -> Result<()> { - let engine = match builder.build().await { - Ok(engine) => engine, - Err(e) => { - log::error!("Error creating engine: {e}"); - return Err(e.into()); - } - }; - - let mut global_engine = ENGINE.write().await; - *global_engine = engine; + let engine = builder + .build() + .await + .inspect_err(|e| log::error!("Error creating engine: {e}"))?; + *ENGINE.write().await = engine; Ok(()) } diff --git a/probing/core/src/runtime.rs b/probing/core/src/runtime.rs index 0c40afd5..b42cfbe9 100644 --- a/probing/core/src/runtime.rs +++ b/probing/core/src/runtime.rs @@ -1,56 +1,252 @@ use std::cell::Cell; use std::future::Future; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{mpsc, Arc, Mutex, OnceLock}; use std::thread::{self, ThreadId}; use log; use once_cell::sync::Lazy; +use thiserror::Error; -fn build_core_runtime() -> tokio::runtime::Runtime { +/// Async bridge failure — probing continues but callers should treat results as unavailable. +#[derive(Debug, Clone, Error)] +pub enum RuntimeError { + #[error("probing runtime unavailable")] + Unavailable, + #[error("probing runtime internal error: {0}")] + Internal(String), + #[error("probing runtime panicked")] + Panicked, +} + +impl From for datafusion::error::DataFusionError { + fn from(e: RuntimeError) -> Self { + datafusion::error::DataFusionError::Execution(e.to_string()) + } +} + +/// Fallback value when [`block_on`] cannot run the future (never panics or exits). +pub trait BlockOnFallback: Send + 'static { + fn on_block_on_failure(err: RuntimeError) -> Self; +} + +impl BlockOnFallback for () { + fn on_block_on_failure(_: RuntimeError) -> Self {} +} + +impl BlockOnFallback for bool { + fn on_block_on_failure(_: RuntimeError) -> Self { + false + } +} + +impl BlockOnFallback for usize { + fn on_block_on_failure(_: RuntimeError) -> Self { + 0 + } +} + +impl BlockOnFallback for i32 { + fn on_block_on_failure(_: RuntimeError) -> Self { + 0 + } +} + +impl BlockOnFallback for Option { + fn on_block_on_failure(_: RuntimeError) -> Self { + None + } +} + +impl BlockOnFallback for Result +where + T: Send + 'static, + E: From + Send + 'static, +{ + fn on_block_on_failure(err: RuntimeError) -> Self { + Err(err.into()) + } +} + +impl BlockOnFallback for Result { + fn on_block_on_failure(err: RuntimeError) -> Self { + Err(err.to_string()) + } +} + +impl BlockOnFallback for std::collections::HashMap +where + K: Eq + std::hash::Hash + Send + 'static, + V: Send + 'static, + S: Default + std::hash::BuildHasher + Send + 'static, +{ + fn on_block_on_failure(_: RuntimeError) -> Self { + Self::default() + } +} + +impl BlockOnFallback for String { + fn on_block_on_failure(_: RuntimeError) -> Self { + String::new() + } +} + +impl BlockOnFallback for Vec { + fn on_block_on_failure(_: RuntimeError) -> Self { + Vec::new() + } +} + +#[cfg(feature = "python-bridge")] +impl BlockOnFallback for Result { + fn on_block_on_failure(err: RuntimeError) -> Self { + Err(pyo3::exceptions::PyRuntimeError::new_err(err.to_string())) + } +} + +fn try_build_core_runtime() -> Option { let worker_threads = std::env::var("PROBING_SERVER_WORKER_THREADS") .unwrap_or_else(|_| "4".to_string()) .parse::() .unwrap_or(4); - match tokio::runtime::Builder::new_multi_thread() + + if let Ok(rt) = tokio::runtime::Builder::new_multi_thread() .enable_all() .worker_threads(worker_threads) .thread_name("probing-runtime") .build() { - Ok(rt) => rt, + return Some(rt); + } + + log::error!("Failed to create probing multi-thread runtime; trying current-thread fallback"); + + match tokio::runtime::Builder::new_current_thread() + .enable_all() + .thread_name("probing-runtime") + .build() + { + Ok(rt) => Some(rt), Err(e) => { - log::error!("Failed to create probing multi-thread runtime: {e}; trying current-thread fallback"); - tokio::runtime::Builder::new_current_thread() - .enable_all() - .thread_name("probing-runtime") - .build() - .unwrap_or_else(|e2| { - log::error!( - "Failed to create probing fallback runtime: {e2}; using minimal runtime" - ); - tokio::runtime::Builder::new_current_thread() - .build() - .expect("unable to create minimal tokio runtime") - }) + log::error!( + "Failed to create probing current-thread runtime: {e}; \ + async bridge will use ephemeral executors only" + ); + None } } } +/// Shared Tokio runtime for sync→async bridges (Python bindings, local server, etc.). +pub struct CoreRuntime { + inner: Option, + degraded: AtomicBool, +} + +static FALLBACK_RUNTIME: OnceLock = OnceLock::new(); + +/// Last-resort runtime for the server-side `block_on`/`spawn` methods, which — +/// unlike the free [`block_on`] function — cannot return a `Result`. Tries a +/// bounded number of times rather than spinning forever, so a catastrophic +/// environment fails loudly instead of hanging a thread. +fn build_emergency_runtime() -> tokio::runtime::Runtime { + const MAX_ATTEMPTS: u32 = 8; + for attempt in 1..=MAX_ATTEMPTS { + match tokio::runtime::Builder::new_current_thread() + .enable_all() + .thread_name("probing-runtime-fallback") + .build() + { + Ok(rt) => return rt, + Err(e) => { + log::error!( + "probing: emergency runtime creation failed (attempt {attempt}/{MAX_ATTEMPTS}): {e}" + ); + std::thread::sleep(std::time::Duration::from_millis(100)); + } + } + } + panic!("probing: unable to create any tokio runtime after {MAX_ATTEMPTS} attempts"); +} + +impl CoreRuntime { + fn new() -> Self { + let inner = try_build_core_runtime(); + let degraded = inner.is_none(); + if degraded { + log::error!( + "probing: no tokio runtime available; marking async bridge degraded \ + (queries and config may return empty/error results)" + ); + } + Self { + inner, + degraded: AtomicBool::new(degraded), + } + } + + fn runtime_ref(&self) -> &tokio::runtime::Runtime { + if let Some(rt) = &self.inner { + return rt; + } + FALLBACK_RUNTIME.get_or_init(|| { + self.mark_degraded(); + log::error!("probing: activating emergency fallback tokio runtime"); + build_emergency_runtime() + }) + } + + /// Whether the shared runtime is healthy enough for probing async work. + pub fn is_operational(&self) -> bool { + !self.degraded.load(Ordering::Relaxed) + } + + pub fn mark_degraded(&self) { + if !self.degraded.swap(true, Ordering::Relaxed) { + log::error!( + "probing: runtime marked degraded; async/query features may return \ + empty or error results until process restart" + ); + } + } + + pub fn spawn(&self, future: F) -> tokio::task::JoinHandle + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + self.runtime_ref().spawn(future) + } + + pub fn handle(&self) -> tokio::runtime::Handle { + self.runtime_ref().handle().clone() + } + + pub fn block_on(&self, future: F) -> T + where + F: Future, + { + self.runtime_ref().block_on(future) + } +} + /// Shared Tokio runtime for all sync→async bridges (Python bindings, local server, etc.). /// /// ENGINE and CONFIG_STORE must only be accessed from this runtime. Creating ad-hoc /// runtimes (especially when Python already has an asyncio loop) can cause SIGSEGV. -pub static CORE_RUNTIME: Lazy = Lazy::new(build_core_runtime); +pub static CORE_RUNTIME: Lazy = Lazy::new(CoreRuntime::new); + +/// Whether probing's async bridge is still operational. +pub fn runtime_operational() -> bool { + CORE_RUNTIME.is_operational() +} -/// Python main thread id, registered when `probing._core` loads. static PYTHON_MAIN_THREAD: OnceLock = OnceLock::new(); -/// Record the Python main thread (call from `probing._core` module init). pub fn register_python_main_thread() { let _ = PYTHON_MAIN_THREAD.set(thread::current().id()); } -/// Whether the current thread is the Python main thread registered at `_core` load. pub fn is_python_main_thread() -> bool { PYTHON_MAIN_THREAD .get() @@ -61,6 +257,16 @@ fn is_inside_core_runtime() -> bool { tokio::runtime::Handle::try_current().is_ok() } +fn take_from_mutex_cell(cell: &Mutex>, context: &str) -> Option { + match cell.lock() { + Ok(mut guard) => guard.take(), + Err(poison) => { + log::warn!("probing {context}: mutex poisoned; recovering stored value if any"); + poison.into_inner().take() + } + } +} + fn block_on_ephemeral(future: F) -> T where F: Future, @@ -77,39 +283,52 @@ where } } -fn recover_block_on_from_cell(future_cell: &Arc>>) -> T +fn block_on_failed(context: &str) -> Result { + log::error!("probing block_on: {context}; async bridge degraded"); + CORE_RUNTIME.mark_degraded(); + Err(RuntimeError::Internal(context.to_string())) +} + +fn recover_block_on_from_cell(future_cell: &Arc>>) -> Result where F: Future, { - let fut = future_cell - .lock() - .ok() - .and_then(|mut guard| guard.take()) - .expect("block_on future missing"); - block_on_ephemeral(fut) + match take_from_mutex_cell(future_cell, "block_on recovery") { + Some(fut) => Ok(block_on_ephemeral(fut)), + None => block_on_failed("future missing during block_on recovery"), + } } -fn spawn_block_on_thread(future: F) -> T +fn spawn_block_on_thread(future: F) -> Result where F: Future + Send + 'static, T: Send + 'static, { + let (tx, rx) = mpsc::sync_channel::(1); let future_cell = Arc::new(Mutex::new(Some(future))); let worker_cell = Arc::clone(&future_cell); + + let worker = move || { + let Some(fut) = take_from_mutex_cell(&worker_cell, "block_on worker") else { + CORE_RUNTIME.mark_degraded(); + log::error!("probing block_on worker: future missing from cell"); + return; + }; + let out = CORE_RUNTIME.block_on(fut); + let _ = tx.send(out); + }; + match thread::Builder::new() .name("probing-block-on".into()) - .spawn(move || { - let fut = worker_cell - .lock() - .ok() - .and_then(|mut guard| guard.take()) - .expect("block_on future missing"); - CORE_RUNTIME.block_on(fut) - }) { + .spawn(worker) + { Ok(handle) => match handle.join() { - Ok(v) => v, + Ok(()) => match rx.recv() { + Ok(v) => Ok(v), + Err(_) => recover_block_on_from_cell(&future_cell), + }, Err(_) => { - log::error!("block_on thread panicked; using ephemeral runtime"); + log::error!("block_on thread panicked; attempting recovery"); recover_block_on_from_cell(&future_cell) } }, @@ -120,8 +339,6 @@ where } } -/// Single worker for Python↔Rust calls that must not run on the Python main thread -/// (macOS/PyArrow) or on Tokio workers (nested Python callbacks). struct NativeBridge { tx: Option>, } @@ -143,6 +360,7 @@ impl NativeBridge { })); if finished.is_err() { log::error!("probing-native bridge worker panicked"); + CORE_RUNTIME.mark_degraded(); } let _ = job.done.send(()); } @@ -155,7 +373,10 @@ impl NativeBridge { } } - fn call(&self, f: impl FnOnce() -> R + Send + 'static) -> R { + fn call( + &self, + f: impl FnOnce() -> R + Send + 'static, + ) -> R { let Some(tx) = &self.tx else { return f(); }; @@ -163,40 +384,41 @@ impl NativeBridge { let (done_tx, done_rx) = mpsc::channel(); let func_cell = Arc::new(Mutex::new(Some(f))); let worker_cell = Arc::clone(&func_cell); + + let run_direct = |context: &str| -> R { + log::error!("probing-native bridge: {context}; using direct call"); + CORE_RUNTIME.mark_degraded(); + match take_from_mutex_cell(&func_cell, "native bridge direct") { + Some(func) => func(), + None => R::on_block_on_failure(RuntimeError::Internal(context.to_string())), + } + }; + if tx .send(BridgeJob { func: Box::new(move || { - let r = worker_cell - .lock() - .ok() - .and_then(|mut guard| guard.take()) - .expect("bridge func missing")(); - let _ = result_tx.send(r); + let out = match take_from_mutex_cell(&worker_cell, "native bridge worker") { + Some(func) => func(), + None => { + log::error!("probing-native bridge worker: func missing from cell"); + CORE_RUNTIME.mark_degraded(); + return; + } + }; + let _ = result_tx.send(out); }), done: done_tx, }) .is_err() { - log::error!("probing-native bridge thread exited; using direct call"); - return func_cell - .lock() - .ok() - .and_then(|mut guard| guard.take()) - .expect("bridge func missing")(); + return run_direct("bridge thread exited before job was queued"); } if done_rx.recv().is_err() { log::error!("probing-native bridge worker dropped completion"); } match result_rx.recv() { Ok(r) => r, - Err(_) => { - log::error!("probing-native bridge worker returned no value; using direct call"); - func_cell - .lock() - .ok() - .and_then(|mut guard| guard.take()) - .expect("bridge func missing")() - } + Err(_) => run_direct("bridge worker returned no value"), } } } @@ -211,7 +433,9 @@ fn on_native_bridge() -> bool { ON_NATIVE_BRIDGE.with(|v| v.get()) } -fn run_on_native_bridge(f: impl FnOnce() -> R + Send + 'static) -> R { +fn run_on_native_bridge( + f: impl FnOnce() -> R + Send + 'static, +) -> R { if on_native_bridge() { return f(); } @@ -229,8 +453,9 @@ fn needs_native_bridge() -> bool { (is_python_main_thread() && !on_native_bridge()) || is_inside_core_runtime() } -/// Run synchronous Rust/Python bridge work off the Python main thread and Tokio workers. -pub fn run_on_native_thread(f: impl FnOnce() -> R + Send + 'static) -> R { +pub fn run_on_native_thread( + f: impl FnOnce() -> R + Send + 'static, +) -> R { if needs_native_bridge() { return run_on_native_bridge(f); } @@ -238,16 +463,31 @@ pub fn run_on_native_thread(f: impl FnOnce() -> R + Send + 's } /// Run an async future on [`CORE_RUNTIME`] from a synchronous context. -pub fn block_on(future: F) -> T +/// +/// Returns `Err(RuntimeError)` when the async bridge cannot run the future +/// (degraded runtime, panic, …). Callers must decide how to surface that — +/// the bridge never fabricates a "successful-looking" empty/default value, +/// which for a diagnostics tool would silently turn a failure into "no data". +pub fn block_on(future: F) -> Result where F: Future + Send + 'static, T: Send + 'static, { - // Never call Runtime::block_on from a probing-runtime worker (panics). if is_inside_core_runtime() { return spawn_block_on_thread(future); } - run_on_native_thread(move || CORE_RUNTIME.block_on(future)) + run_on_native_thread(move || { + match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + CORE_RUNTIME.block_on(future) + })) { + Ok(v) => Ok(v), + Err(_) => { + log::error!("probing block_on panicked on native thread"); + CORE_RUNTIME.mark_degraded(); + Err(RuntimeError::Panicked) + } + } + }) } #[cfg(test)] @@ -256,13 +496,15 @@ mod tests { #[test] fn block_on_completes_on_current_runtime() { - let value = block_on(async { 21 + 21 }); + let value = block_on(async { 21 + 21 }).expect("runtime available in tests"); assert_eq!(value, 42); } #[test] fn block_on_from_runtime_worker_does_not_panic() { - let value = block_on(async { block_on(async { 40 + 2 }) }); + let value = block_on(async { block_on(async { 40 + 2 }) }) + .expect("outer bridge") + .expect("inner bridge"); assert_eq!(value, 42); } @@ -272,4 +514,9 @@ mod tests { let b = run_on_native_bridge(|| 2); assert_eq!(a + b, 3); } + + #[test] + fn core_runtime_starts_operational_in_tests() { + assert!(runtime_operational()); + } } diff --git a/probing/core/src/signal/macos.rs b/probing/core/src/signal/macos.rs new file mode 100644 index 00000000..2d2e8d2e --- /dev/null +++ b/probing/core/src/signal/macos.rs @@ -0,0 +1,136 @@ +//! Deliver signals to live pthread/Mach threads on macOS. + +use std::io; + +#[allow(non_camel_case_types)] +mod mach { + use std::io; + use std::ptr; + + pub type kern_return_t = i32; + pub type mach_port_t = u32; + pub type integer_t = i32; + pub type thread_act_t = mach_port_t; + pub type task_t = mach_port_t; + + pub const KERN_SUCCESS: kern_return_t = 0; + pub const THREAD_IDENTIFIER_INFO: i32 = 4; + + #[repr(C)] + pub struct thread_identifier_info { + pub thread_id: u64, + pub thread_handle: u64, + pub dispatch_qaddr: u64, + } + + extern "C" { + pub fn mach_task_self() -> task_t; + pub fn task_threads( + target_task: task_t, + act_list: *mut *mut thread_act_t, + act_count: *mut u32, + ) -> kern_return_t; + pub fn thread_info( + target_act: thread_act_t, + flavor: i32, + thread_info_out: *mut integer_t, + thread_info_out_cnt: *mut u32, + ) -> kern_return_t; + pub fn mach_port_deallocate(task: task_t, name: mach_port_t) -> kern_return_t; + pub fn vm_deallocate(target_task: task_t, address: u64, size: u64) -> kern_return_t; + pub fn pthread_from_mach_thread_np(thread_port: thread_act_t) -> libc::pthread_t; + } + + pub fn thread_id_for_port(port: thread_act_t) -> Option { + let mut ident: thread_identifier_info = unsafe { std::mem::zeroed() }; + let mut ident_count = + (std::mem::size_of::() / std::mem::size_of::()) as u32; + let kr = unsafe { + thread_info( + port, + THREAD_IDENTIFIER_INFO, + &mut ident as *mut _ as *mut i32, + &mut ident_count, + ) + }; + if kr == KERN_SUCCESS { + Some(ident.thread_id) + } else { + None + } + } + + pub fn signal_sigusr2_on_port(port: thread_act_t) -> io::Result<()> { + extern "C" { + fn pthread_kill(thread: libc::pthread_t, sig: i32) -> i32; + } + unsafe { + let pthread = pthread_from_mach_thread_np(port); + if pthread == 0 { + return Err(io::Error::other( + "pthread_from_mach_thread_np returned null", + )); + } + if pthread_kill(pthread, libc::SIGUSR2) != 0 { + return Err(io::Error::last_os_error()); + } + } + Ok(()) + } + + pub fn list_thread_ports() -> io::Result> { + let mut list: *mut thread_act_t = ptr::null_mut(); + let mut count: u32 = 0; + let kr = unsafe { task_threads(mach_task_self(), &mut list, &mut count) }; + if kr != KERN_SUCCESS { + return Err(io::Error::other(format!("task_threads failed: {kr}"))); + } + if list.is_null() || count == 0 { + return Ok(Vec::new()); + } + + let ports = unsafe { std::slice::from_raw_parts(list, count as usize).to_vec() }; + let size = (count as u64).saturating_mul(std::mem::size_of::() as u64); + unsafe { + let _ = vm_deallocate(mach_task_self(), list as u64, size); + } + Ok(ports) + } +} + +use mach::{mach_port_deallocate, mach_task_self}; + +/// Deliver `SIGUSR2` to a live thread by Mach/pthread thread id. +pub fn send_sigusr2_to_thread_id(thread_id: i32) -> io::Result<()> { + if thread_id <= 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("invalid thread id {thread_id}"), + )); + } + let task = unsafe { mach_task_self() }; + let ports = mach::list_thread_ports()?; + let target = thread_id as u64; + + for port in ports { + let matched = mach::thread_id_for_port(port) + .map(|id| id == target) + .unwrap_or(false); + let result = if matched { + mach::signal_sigusr2_on_port(port) + } else { + Ok(()) + }; + unsafe { + let _ = mach_port_deallocate(task, port); + } + if matched { + return result; + } + } + + Err(io::Error::new( + io::ErrorKind::NotFound, + format!("no live thread with id {thread_id}"), + )) +} diff --git a/probing/core/src/signal/mod.rs b/probing/core/src/signal/mod.rs new file mode 100644 index 00000000..b20a18c9 --- /dev/null +++ b/probing/core/src/signal/mod.rs @@ -0,0 +1,7 @@ +//! Cross-platform signal helpers shared by collectors and runtime features. + +#[cfg(target_os = "macos")] +mod macos; + +#[cfg(target_os = "macos")] +pub use macos::send_sigusr2_to_thread_id; diff --git a/probing/core/src/storage/distributed.rs b/probing/core/src/storage/distributed.rs index d86ed394..20ef506f 100644 --- a/probing/core/src/storage/distributed.rs +++ b/probing/core/src/storage/distributed.rs @@ -2,7 +2,6 @@ use std::collections::HashMap; use std::fmt; use std::sync::Arc; -use anyhow::{anyhow, Result}; use async_trait::async_trait; use serde::{Deserialize, Serialize}; use tokio::sync::RwLock; @@ -12,6 +11,7 @@ use super::entity::{EntityId, EntityStore, PersistentEntity}; use super::mem_store::MemoryStore; use super::topology::TopologyView; use crate::core::cluster_model::{NodeId, WorkerId}; +use crate::core::{EngineError, Result}; #[async_trait] pub trait RemoteStoreClient: Send + Sync { @@ -107,32 +107,53 @@ impl DistributedEntityStore { async fn get_remote_client(&self, location: &Address) -> Result> { let shard_key = location .shard_key() - .ok_or_else(|| anyhow!("Invalid address for remote operation"))?; + .ok_or_else(|| EngineError::storage("Invalid address for remote operation"))?; let clients = self.remote_clients.read().await; clients .get(&shard_key) .cloned() - .ok_or_else(|| anyhow!("No remote client for shard: {}", shard_key)) + .ok_or_else(|| EngineError::storage(format!("No remote client for shard: {shard_key}"))) } - async fn remove(&self, id: &T::Id, locations: &[&Address]) -> Result<()> { - let key = format!("{}::{}", T::entity_type(), id.as_str()); + /// Delete `key`/`id` at a single location, dispatching to the local store or + /// the matching remote client. `?` carries any addressing/client error. + async fn del_at( + &self, + id: &T::Id, + key: &str, + location: &Address, + ) -> Result<()> { + if location.is_local(&self.worker_id) { + self.local_store.del::(id).await + } else { + self.get_remote_client(location).await?.del(key).await + } + } - let mut results = Vec::new(); + /// Write a serialized entity at a single location. + async fn put_at( + &self, + entity: &T, + key: &str, + serialized: &[u8], + location: &Address, + ) -> Result<()> { + if location.is_local(&self.worker_id) { + self.local_store.put(entity).await + } else { + self.get_remote_client(location) + .await? + .put(key, serialized) + .await + } + } + async fn remove(&self, id: &T::Id, locations: &[&Address]) -> Result<()> { + let key = format!("{}::{}", T::entity_type(), id.as_str()); for location in locations { - let result = if location.is_local(&self.worker_id) { - self.local_store.del::(id).await - } else { - match self.get_remote_client(location).await { - Ok(client) => client.del(&key).await, - Err(e) => Err(e), - } - }; - - results.push(result); + // Per-location failures are tolerated (best-effort fan-out delete). + let _ = self.del_at::(id, &key, location).await; } - Ok(()) } @@ -144,21 +165,10 @@ impl DistributedEntityStore { let serialized = bincode::serialize(entity)?; let key = format!("{}::{}", T::entity_type(), entity.id().as_str()); - let mut results = Vec::new(); - + let mut results = Vec::with_capacity(locations.len()); for location in locations { - let result = if location.is_local(&self.worker_id) { - self.local_store.put(entity).await - } else { - match self.get_remote_client(location).await { - Ok(client) => client.put(&key, &serialized).await, - Err(e) => Err(e), - } - }; - - results.push(result); + results.push(self.put_at(entity, &key, &serialized, location).await); } - Ok(results) } @@ -208,12 +218,14 @@ impl EntityStore for DistributedEntityStore { ); if write_locations.is_empty() && !locations.is_empty() { - return Err(anyhow!( - "No suitable write locations found, though addresses were allocated." + return Err(EngineError::storage( + "No suitable write locations found, though addresses were allocated.", )); } if write_locations.is_empty() && locations.is_empty() { - return Err(anyhow!("No addresses allocated for the entity.")); + return Err(EngineError::storage( + "No addresses allocated for the entity.", + )); } let write_results = self.write(entity, &write_locations).await?; diff --git a/probing/core/src/storage/entity.rs b/probing/core/src/storage/entity.rs index 68db91c2..26fb70e4 100644 --- a/probing/core/src/storage/entity.rs +++ b/probing/core/src/storage/entity.rs @@ -1,5 +1,5 @@ // probing/core/src/storage/entity.rs -use anyhow::Result; +use crate::core::Result; use async_trait::async_trait; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; diff --git a/probing/core/src/storage/mem_store.rs b/probing/core/src/storage/mem_store.rs index 4f5a93c9..4335f650 100644 --- a/probing/core/src/storage/mem_store.rs +++ b/probing/core/src/storage/mem_store.rs @@ -1,6 +1,6 @@ // probing/core/src/storage/sled_store.rs use super::entity::{EntityId, EntityStore, PersistentEntity}; -use anyhow::Result; +use crate::core::Result; use async_trait::async_trait; use std::collections::HashMap; use std::sync::Arc; diff --git a/probing/core/src/storage/mod.rs b/probing/core/src/storage/mod.rs index 86c486ab..fd72a919 100644 --- a/probing/core/src/storage/mod.rs +++ b/probing/core/src/storage/mod.rs @@ -10,7 +10,7 @@ pub use entity::{EntityId, EntityStore, PersistentEntity}; pub use mem_store::MemoryStore; // Distributed storage exports -pub use addressing::{Address, AddressAllocator}; +pub use addressing::{Address, AddressAllocator, AddressError}; pub use distributed::{ConsistencyLevel, DistributedEntityStore, RemoteStoreClient}; pub use remote_client::MemoryRemoteClient; pub use topology::{TopologyStats, TopologyView}; diff --git a/probing/core/src/storage/remote_client.rs b/probing/core/src/storage/remote_client.rs index ac63af4b..0a011058 100644 --- a/probing/core/src/storage/remote_client.rs +++ b/probing/core/src/storage/remote_client.rs @@ -1,6 +1,6 @@ use super::distributed::RemoteStoreClient; use super::mem_store::MemoryStore; // Add this line -use anyhow::Result; +use crate::core::Result; use async_trait::async_trait; use std::sync::Arc; // Add this line diff --git a/probing/core/src/trace/mod.rs b/probing/core/src/trace/mod.rs index 461523e9..6c24329f 100644 --- a/probing/core/src/trace/mod.rs +++ b/probing/core/src/trace/mod.rs @@ -10,8 +10,9 @@ pub use step::{ // --- Custom Error Type --- /// Represents errors that can occur during tracing operations. -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] pub enum TraceError { /// Indicates that an operation was attempted on a span that has already been closed. + #[error("span already closed")] SpanAlreadyClosed, } diff --git a/probing/extensions/cc/src/extensions/cpu/collector.rs b/probing/extensions/cc/src/extensions/cpu/collector.rs index 9c0bde04..214fa9e7 100644 --- a/probing/extensions/cc/src/extensions/cpu/collector.rs +++ b/probing/extensions/cc/src/extensions/cpu/collector.rs @@ -136,8 +136,8 @@ impl Default for CpuCollectorConfig { pub enum CollectorError { #[error("CPU collector already running")] AlreadyRunning, - #[error("Failed to open CPU memtables: {0}")] - OpenFailed(String), + #[error("Failed to open CPU memtables")] + OpenFailed(#[from] probing_memtable::MemtableError), #[error("CPU collector stop failed: {0}")] StopFailed(String), } @@ -267,7 +267,7 @@ struct CollectorTables { } impl CollectorTables { - fn open() -> Result { + fn open() -> probing_memtable::MemtableResult { Ok(Self { utilization: Mutex::new(ExposedTable::create( "cpu.utilization", @@ -295,12 +295,15 @@ impl CpuCollector { &INSTANCE } - fn shared_tables(&self) -> Result, std::io::Error> { + fn shared_tables(&self) -> probing_memtable::MemtableResult> { let mut guard = lock_cpu_collector(&self.tables); if guard.is_none() { *guard = Some(Arc::new(CollectorTables::open()?)); } - Ok(Arc::clone(guard.as_ref().unwrap())) + guard.as_ref().cloned().ok_or_else(|| { + log::error!("cpu collector: shared tables slot empty after init"); + probing_memtable::MemtableError::InvalidBuffer("cpu tables unavailable") + }) } #[cfg_attr(not(test), allow(dead_code))] @@ -321,9 +324,7 @@ impl CpuCollector { } let running = self.running.clone(); - let tables = self - .shared_tables() - .map_err(|e| CollectorError::OpenFailed(e.to_string()))?; + let tables = self.shared_tables()?; let handle = thread::spawn(move || { let sampler = host_sampler(); let platform = sampler.platform().to_string(); diff --git a/probing/extensions/cc/src/extensions/cpu/extension.rs b/probing/extensions/cc/src/extensions/cpu/extension.rs index 7e5c76f5..b6c2b6a3 100644 --- a/probing/extensions/cc/src/extensions/cpu/extension.rs +++ b/probing/extensions/cc/src/extensions/cpu/extension.rs @@ -64,12 +64,8 @@ impl CpuProbeExtension { _ => 8, }; - start_cpu_sampling(interval as u64, thread_top_n).map_err(|e| { - EngineError::InvalidOptionValue( - Self::OPTION_CPU_SAMPLE_INTERVAL_MS.to_string(), - format!("{e}"), - ) - })?; + start_cpu_sampling(interval as u64, thread_top_n) + .map_err(|e| EngineError::invalid_option(Self::OPTION_CPU_SAMPLE_INTERVAL_MS, e))?; self.cpu_sample_interval_ms = cpu_sample_interval_ms; Ok(()) diff --git a/probing/extensions/cc/src/extensions/cpu/macos.rs b/probing/extensions/cc/src/extensions/cpu/macos.rs index 3636ad51..925b8f95 100644 --- a/probing/extensions/cc/src/extensions/cpu/macos.rs +++ b/probing/extensions/cc/src/extensions/cpu/macos.rs @@ -77,43 +77,6 @@ mod mach { -> i32; } - pub fn thread_id_for_port(port: thread_act_t) -> Option { - let mut ident: thread_identifier_info = unsafe { std::mem::zeroed() }; - let mut ident_count = - (std::mem::size_of::() / std::mem::size_of::()) as u32; - let kr = unsafe { - thread_info( - port, - THREAD_IDENTIFIER_INFO, - &mut ident as *mut _ as *mut i32, - &mut ident_count, - ) - }; - if kr == KERN_SUCCESS { - Some(ident.thread_id) - } else { - None - } - } - - pub fn signal_sigusr2_on_port(port: thread_act_t) -> std::io::Result<()> { - extern "C" { - fn pthread_kill(thread: libc::pthread_t, sig: i32) -> i32; - } - unsafe { - let pthread = pthread_from_mach_thread_np(port); - if pthread == 0 { - return Err(std::io::Error::other( - "pthread_from_mach_thread_np returned null", - )); - } - if pthread_kill(pthread, libc::SIGUSR2) != 0 { - return Err(std::io::Error::last_os_error()); - } - } - Ok(()) - } - pub fn pthread_name(port: thread_act_t) -> Option { unsafe { let pthread = pthread_from_mach_thread_np(port); @@ -302,41 +265,6 @@ impl CpuHostSampler for MacSampler { } } -/// Deliver `SIGUSR2` to a live thread by Mach/pthread thread id (as stored in `cpu.tasks`). -pub fn send_sigusr2_to_thread_id(thread_id: i32) -> io::Result<()> { - if thread_id <= 0 { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!("invalid thread id {thread_id}"), - )); - } - let task = unsafe { mach_task_self() }; - let ports = mach::list_thread_ports()?; - let target = thread_id as u64; - - for port in ports { - let matched = mach::thread_id_for_port(port) - .map(|id| id == target) - .unwrap_or(false); - let result = if matched { - mach::signal_sigusr2_on_port(port) - } else { - Ok(()) - }; - unsafe { - let _ = mach_port_deallocate(task, port); - } - if matched { - return result; - } - } - - Err(io::Error::new( - io::ErrorKind::NotFound, - format!("no live thread with id {thread_id}"), - )) -} - #[cfg(test)] mod tests { use super::*; diff --git a/probing/extensions/cc/src/extensions/cpu/mod.rs b/probing/extensions/cc/src/extensions/cpu/mod.rs index 448a85be..53eacefb 100644 --- a/probing/extensions/cc/src/extensions/cpu/mod.rs +++ b/probing/extensions/cc/src/extensions/cpu/mod.rs @@ -12,7 +12,7 @@ mod unsupported; pub use collector::{autostart_interval_ms, start_cpu_sampling, start_cpu_sampling_from_env}; pub use extension::CpuProbeExtension; #[cfg(target_os = "macos")] -pub use macos::send_sigusr2_to_thread_id; +pub use probing_core::signal::send_sigusr2_to_thread_id; pub use sample::{ProcessSample, ThreadSample}; pub use sampler::{host_sampler, CpuHostSampler}; diff --git a/probing/extensions/cc/src/extensions/files.rs b/probing/extensions/cc/src/extensions/files.rs index 07076314..b7e666c7 100644 --- a/probing/extensions/cc/src/extensions/files.rs +++ b/probing/extensions/cc/src/extensions/files.rs @@ -21,21 +21,20 @@ impl CustomNamespace for FileList { } fn list() -> Vec { - let direntries = std::fs::read_dir(".").unwrap(); + let direntries = match std::fs::read_dir(".") { + Ok(entries) => entries, + Err(e) => { + log::error!("file namespace: read_dir failed: {e}"); + return Vec::new(); + } + }; direntries .filter_map(|entry| { - if let Ok(entry) = entry { - let filename = entry.file_name().into_string().unwrap(); - if filename.ends_with(".csv") { - Some(filename) - } else { - None - } - } else { - None - } + let entry = entry.ok()?; + let filename = entry.file_name().into_string().ok()?; + filename.ends_with(".csv").then_some(filename) }) - .collect::>() + .collect() } async fn table(expr: String) -> Result>> { diff --git a/probing/extensions/cc/src/extensions/rdma.rs b/probing/extensions/cc/src/extensions/rdma.rs index 6c247b0e..5ba21ee0 100644 --- a/probing/extensions/cc/src/extensions/rdma.rs +++ b/probing/extensions/cc/src/extensions/rdma.rs @@ -2,6 +2,7 @@ use probing_core::core::EngineError; use probing_core::core::Maybe; use probing_core::core::ProbeExtension; use probing_core::core::ProbeExtensionOption; +use probing_core::sync::lock_mutex; use datafusion::arrow::array::{GenericStringBuilder, RecordBatch}; use datafusion::arrow::datatypes::{DataType, Field, Schema, SchemaRef}; @@ -21,6 +22,20 @@ use std::time::{Duration, Instant}; static GLOBAL_HCA_NAME: OnceLock> = OnceLock::new(); static GLOBAL_HCA_SAMPLE_RATE: OnceLock> = OnceLock::new(); +fn lock_hca_name() -> std::sync::MutexGuard<'static, String> { + lock_mutex( + GLOBAL_HCA_NAME.get_or_init(|| Mutex::new(String::new())), + "rdma HCA name", + ) +} + +fn lock_sample_rate() -> std::sync::MutexGuard<'static, f64> { + lock_mutex( + GLOBAL_HCA_SAMPLE_RATE.get_or_init(|| Mutex::new(0.0)), + "rdma sample rate", + ) +} + #[derive(Default, Debug)] pub struct RdmaTable {} @@ -45,9 +60,7 @@ impl CustomTable for RdmaTable { } fn data() -> Vec { - let string = GLOBAL_HCA_NAME.get_or_init(|| Mutex::new(String::new())); - let guard = string.lock().unwrap(); - let hca_name = guard.clone(); + let hca_name = lock_hca_name().clone(); let mut monitor = RDMAMonitor::new(&hca_name); monitor.obtain_newset(); @@ -71,9 +84,7 @@ impl CustomTable for RdmaTable { np_cnp_sent.append_value(monitor.read_counter("np_cnp_sent")); np_ecn_marked_roce_packets.append_value(monitor.read_counter("np_ecn_marked_roce_packets")); - let f64_val = GLOBAL_HCA_SAMPLE_RATE.get_or_init(|| Mutex::new(0.0)); - let guard = f64_val.lock().unwrap(); - let sleep_time = *guard as u64; + let sleep_time = *lock_sample_rate() as u64; thread::sleep(Duration::from_secs(sleep_time)); @@ -141,14 +152,12 @@ fn resolve_hca_name(body: &[u8]) -> Result { if !body.is_empty() { let name = String::from_utf8_lossy(body).trim().to_string(); if !name.is_empty() { - let global = GLOBAL_HCA_NAME.get_or_init(|| Mutex::new(String::new())); - *global.lock().unwrap() = name.clone(); + *lock_hca_name() = name.clone(); return Ok(name); } } - let global = GLOBAL_HCA_NAME.get_or_init(|| Mutex::new(String::new())); - let name = global.lock().unwrap().clone(); + let name = lock_hca_name().clone(); if name.is_empty() { return Err(EngineError::InvalidOptionValue( RdmaProbeExtension::OPTION_HCA_NAME.to_string(), @@ -162,11 +171,7 @@ fn format_rdma_snapshot(hca_name: &str) -> Result { let mut monitor = RDMAMonitor::new(hca_name); monitor.obtain_newset(); - let sleep_secs = GLOBAL_HCA_SAMPLE_RATE - .get_or_init(|| Mutex::new(0.0)) - .lock() - .unwrap() - .max(0.0) as u64; + let sleep_secs = lock_sample_rate().max(0.0) as u64; if sleep_secs > 0 { thread::sleep(Duration::from_secs(sleep_secs)); } @@ -213,9 +218,7 @@ impl RdmaProbeExtension { )); } - let f64_val = GLOBAL_HCA_SAMPLE_RATE.get_or_init(|| Mutex::new(0.0)); - let mut guard = f64_val.lock().unwrap(); - *guard = rate; + *lock_sample_rate() = rate; } self.sample_rate = sample_rate; @@ -234,9 +237,7 @@ impl RdmaProbeExtension { )); } - let string = GLOBAL_HCA_NAME.get_or_init(|| Mutex::new(String::new())); - let mut guard = string.lock().unwrap(); - *guard = name.clone(); + *lock_hca_name() = name.clone(); } Ok(()) @@ -283,13 +284,10 @@ impl RDMAMonitor { previous: Option, interval: Option, ) -> f64 { - if current.is_none() || previous.is_none() || interval.is_none() { + let (Some(current), Some(previous), Some(interval)) = (current, previous, interval) else { return 0.0; - } - - let current = current.unwrap(); - let previous = previous.unwrap(); - let interval = interval.unwrap().as_secs_f64(); + }; + let interval = interval.as_secs_f64(); let diff = if current < previous { current + 2u64.pow(64) as f64 - previous @@ -313,8 +311,5 @@ fn read_file_to_f64(path: &str) -> io::Result { let mut file = File::open(path)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; - contents - .trim() - .parse::() - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) + contents.trim().parse::().map_err(io::Error::other) } diff --git a/probing/extensions/gpu/src/extensions/backend/registry.rs b/probing/extensions/gpu/src/extensions/backend/registry.rs index bee56400..957392ff 100644 --- a/probing/extensions/gpu/src/extensions/backend/registry.rs +++ b/probing/extensions/gpu/src/extensions/backend/registry.rs @@ -1,6 +1,7 @@ use std::sync::Mutex; use once_cell::sync::Lazy; +use probing_core::sync::lock_mutex; use super::traits::{GpuBackend, GpuBackendKind}; @@ -14,12 +15,12 @@ static BACKEND_FILTER: Lazy>>> = Lazy::new(|| M /// Restrict which backends are active (`None` = auto-discover all available). pub fn set_backend_filter(kinds: Option>) { - *BACKEND_FILTER.lock().unwrap() = kinds; + *lock_mutex(&BACKEND_FILTER, "GPU backend filter") = kinds; } #[cfg_attr(not(any(feature = "cuda", target_os = "macos")), allow(dead_code))] fn filter_allows(kind: GpuBackendKind) -> bool { - match BACKEND_FILTER.lock().unwrap().as_ref() { + match lock_mutex(&BACKEND_FILTER, "GPU backend filter").as_ref() { None => true, Some(list) => list.contains(&kind), } diff --git a/probing/extensions/gpu/src/extensions/collector.rs b/probing/extensions/gpu/src/extensions/collector.rs index 855f1e3e..fc2b5dae 100644 --- a/probing/extensions/gpu/src/extensions/collector.rs +++ b/probing/extensions/gpu/src/extensions/collector.rs @@ -127,8 +127,8 @@ pub enum CollectorError { AlreadyRunning, #[error("No GPU backend available")] NoBackend, - #[error("Failed to open GPU memtable: {0}")] - OpenFailed(String), + #[error("Failed to open GPU memtable")] + OpenFailed(#[from] probing_memtable::MemtableError), #[error("GPU collector stop failed: {0}")] StopFailed(String), } @@ -224,7 +224,7 @@ impl GpuCollector { &INSTANCE } - fn shared_table(&self) -> Result>, std::io::Error> { + fn shared_table(&self) -> probing_memtable::MemtableResult>> { let mut guard = lock_gpu_collector(&self.table); if guard.is_none() { *guard = Some(Arc::new(Mutex::new(ExposedTable::create( @@ -234,7 +234,10 @@ impl GpuCollector { NUM_CHUNKS, )?))); } - Ok(Arc::clone(guard.as_ref().unwrap())) + guard.as_ref().cloned().ok_or_else(|| { + log::error!("gpu collector: shared table slot empty after init"); + probing_memtable::MemtableError::InvalidBuffer("gpu table unavailable") + }) } pub fn start(&self, config: GpuCollectorConfig) -> Result<(), CollectorError> { @@ -248,9 +251,7 @@ impl GpuCollector { } let running = self.running.clone(); - let table = self - .shared_table() - .map_err(|e| CollectorError::OpenFailed(e.to_string()))?; + let table = self.shared_table()?; let handle = thread::spawn(move || { let mut iterations = config.iterations; diff --git a/probing/extensions/gpu/src/extensions/extension.rs b/probing/extensions/gpu/src/extensions/extension.rs index 9b53a128..5f1e8010 100644 --- a/probing/extensions/gpu/src/extensions/extension.rs +++ b/probing/extensions/gpu/src/extensions/extension.rs @@ -57,12 +57,8 @@ impl GpuProbeExtension { std::env::set_var("PROBING_GPU_BACKEND", backend); } - start_gpu_sampling(interval as u64).map_err(|e| { - EngineError::InvalidOptionValue( - Self::OPTION_GPU_SAMPLE_INTERVAL_MS.to_string(), - format!("{e}"), - ) - })?; + start_gpu_sampling(interval as u64) + .map_err(|e| EngineError::invalid_option(Self::OPTION_GPU_SAMPLE_INTERVAL_MS, e))?; self.gpu_sample_interval_ms = gpu_sample_interval_ms; Ok(()) diff --git a/probing/extensions/python/Cargo.toml b/probing/extensions/python/Cargo.toml index cc8b8789..77df8c27 100644 --- a/probing/extensions/python/Cargo.toml +++ b/probing/extensions/python/Cargo.toml @@ -13,14 +13,14 @@ tracing = [] default = ["extension-module", "tracing"] [dependencies] -probing-cc = { path = "../cc" } -probing-core = { path = "../../core" } +probing-core = { path = "../../core", features = ["python-bridge"] } probing-memtable = { path = "../../memtable" } probing-proto = { path = "../../proto" } probing-store = { path = "../../crates/store" } probing-cli = { path = "../../cli" } anyhow = { workspace = true } +thiserror = { workspace = true } ctor = { workspace = true } log = { workspace = true } nix = { workspace = true } diff --git a/probing/extensions/python/src/extensions/pprof.rs b/probing/extensions/python/src/extensions/pprof.rs index 512d7366..93876512 100644 --- a/probing/extensions/python/src/extensions/pprof.rs +++ b/probing/extensions/python/src/extensions/pprof.rs @@ -36,19 +36,13 @@ impl PprofProbeExtension { fn set_sample_freq(&mut self, pprof_sample_freq: Maybe) -> Result<(), EngineError> { // Clearing the option (`set probing.pprof.sample_freq=;`) or a value < 1 // disables sampling and tears the sampler down. - let disable = match pprof_sample_freq { - Maybe::Nothing => true, - Maybe::Just(freq) => freq < 1, - }; - if disable { - crate::features::pprof::reset(); - self.sample_freq = Maybe::Nothing; - return Ok(()); - } - let freq = match pprof_sample_freq { - Maybe::Just(freq) => freq, - Maybe::Nothing => unreachable!(), + Maybe::Just(freq) if freq >= 1 => freq, + _ => { + crate::features::pprof::reset(); + self.sample_freq = Maybe::Nothing; + return Ok(()); + } }; // Re-settable: `setup` bumps the sampler generation, retires the old // consumer thread, and re-arms the timer at the new rate. diff --git a/probing/extensions/python/src/extensions/python.rs b/probing/extensions/python/src/extensions/python.rs index d8312371..0fd911b3 100644 --- a/probing/extensions/python/src/extensions/python.rs +++ b/probing/extensions/python/src/extensions/python.rs @@ -1,7 +1,6 @@ use std::collections::HashMap; use std::fmt::Display; -use anyhow::Result; use async_trait::async_trait; use probing_core::core::EngineError; @@ -9,6 +8,7 @@ use probing_core::core::Maybe; use probing_core::core::ProbeExtension; use probing_core::core::ProbeExtensionCall; use probing_core::core::ProbeExtensionOption; +use probing_core::core::Result as EngineResult; use probing_core::run_on_native_thread; use probing_proto::prelude::CallFrame; use pyo3::prelude::*; @@ -74,7 +74,7 @@ impl ProbeExtensionCall for PythonExt { path: &str, params: &HashMap, body: &[u8], - ) -> Result, EngineError> { + ) -> EngineResult> { log::debug!( "Python extension call - path: {}, params: {:?}, body_size: {}", path, @@ -85,7 +85,7 @@ impl ProbeExtensionCall for PythonExt { let normalized_path = path.trim_start_matches('/'); if normalized_path.starts_with("crash/") { return crate::features::crash::handle_http(normalized_path, params, body) - .map_err(EngineError::PluginError); + .map_err(EngineError::plugin); } call_python_handler(normalized_path, params, body) } @@ -93,7 +93,7 @@ impl ProbeExtensionCall for PythonExt { impl PythonExt { /// Set up a Python crash handler - fn set_crash_handler(&mut self, crash_handler: Maybe) -> Result<(), EngineError> { + fn set_crash_handler(&mut self, crash_handler: Maybe) -> EngineResult<()> { match self.crash_handler { Maybe::Just(_) => Err(EngineError::ReadOnlyOption( Self::OPTION_CRASH_HANDLER.to_string(), @@ -129,7 +129,7 @@ impl PythonExt { } /// Set up Python monitoring - fn set_monitoring(&mut self, monitoring: Maybe) -> Result<(), EngineError> { + fn set_monitoring(&mut self, monitoring: Maybe) -> EngineResult<()> { log::debug!("Setting Python monitoring: {monitoring}"); match self.monitoring { Maybe::Just(_) => Err(EngineError::ReadOnlyOption( @@ -161,7 +161,7 @@ impl PythonExt { } /// Enable a Python extension from code string - fn set_enabled(&mut self, enabled: Maybe) -> Result<(), EngineError> { + fn set_enabled(&mut self, enabled: Maybe) -> EngineResult<()> { let ext = match &enabled { Maybe::Nothing => { return Err(EngineError::InvalidOptionValue( @@ -173,13 +173,13 @@ impl PythonExt { }; if self.enabled.0.contains_key(ext) { - return Err(EngineError::PluginError(format!( + return Err(EngineError::plugin(format!( "Python extension '{ext}' is already enabled" ))); } let pyext = execute_python_code(ext) - .map_err(|e| EngineError::InvalidOptionValue(Self::OPTION_ENABLED.to_string(), e))?; + .map_err(|e| EngineError::invalid_option(Self::OPTION_ENABLED, e))?; self.enabled.0.insert(ext.clone(), pyext); log::info!("Python extension enabled: {ext}"); @@ -189,7 +189,7 @@ impl PythonExt { } /// Disable a previously enabled Python extension - fn set_disabled(&mut self, disabled: Maybe) -> Result<(), EngineError> { + fn set_disabled(&mut self, disabled: Maybe) -> EngineResult<()> { let ext = match &disabled { Maybe::Nothing => { return Err(EngineError::InvalidOptionValue( @@ -211,10 +211,10 @@ impl PythonExt { Ok(()) } Err(e) => { - let error_msg = - format!("Failed to call deinit method on '{ext_name}': {e}"); - log::error!("{error_msg}"); - Err(EngineError::PluginError(error_msg)) + log::error!("Failed to call deinit method on '{ext_name}': {e}"); + Err(EngineError::plugin(format!( + "Failed to call deinit method on '{ext_name}': {e}" + ))) } }) }) @@ -225,33 +225,46 @@ impl PythonExt { } } +/// Convert a PyO3 result into an [`EngineError`] with a description of the failed +/// step, so the Python boundary can use `.py_context("…")?` instead of an +/// inline `.map_err(|e| EngineError::plugin(format!("…: {e}")))` at every call site. +trait PyContext { + fn py_context(self, ctx: &str) -> EngineResult; + fn py_context_with(self, ctx: impl FnOnce() -> String) -> EngineResult; +} + +impl PyContext for PyResult { + fn py_context(self, ctx: &str) -> EngineResult { + self.map_err(|e| EngineError::plugin(format!("{ctx}: {e}"))) + } + + fn py_context_with(self, ctx: impl FnOnce() -> String) -> EngineResult { + self.map_err(|e| EngineError::plugin(format!("{}: {e}", ctx()))) + } +} + /// Execute Python code and return the resulting object /// The code should return an object with init/deinit methods -pub fn execute_python_code(code: &str) -> Result, String> { +pub fn execute_python_code(code: &str) -> EngineResult> { let code = code.to_string(); run_on_native_thread(move || { Python::attach(|py| { - let pkg = py.import("probing"); - - if pkg.is_err() { - return Err(format!("Python import error: {}", pkg.err().unwrap())); - } + let pkg = py.import("probing").py_context("Python import error")?; let result = pkg - .unwrap() .call_method1("load_extension", (code.as_str(),)) - .map_err(|e| format!("Error loading Python plugin: {e}"))?; + .py_context("Error loading Python plugin")?; if !result .hasattr("init") - .map_err(|e| format!("Unable to check `init` method: {e}"))? + .py_context("Unable to check `init` method")? { - return Err("Plugin must have an `init` method".to_string()); + return Err(EngineError::plugin("Plugin must have an `init` method")); } result .call_method0("init") - .map_err(|e| format!("Error calling `init` method: {e}"))?; + .py_context("Error calling `init` method")?; log::info!("Python extension loaded successfully: {code}"); Ok(result.unbind()) @@ -259,7 +272,7 @@ pub fn execute_python_code(code: &str) -> Result, String> }) } -fn backtrace(tid: Option) -> Result> { +fn backtrace(tid: Option) -> anyhow::Result> { SignalTracer.trace(tid) } @@ -268,47 +281,41 @@ fn call_python_handler( path: &str, params: &HashMap, body: &[u8], -) -> Result, EngineError> { +) -> EngineResult> { let path = path.to_string(); let params = params.clone(); let body = body.to_vec(); run_on_native_thread(move || { Python::attach(|py| { - let router_module = py.import("probing.handlers.router").map_err(|e| { - EngineError::PluginError(format!("Failed to import router module: {e}")) - })?; + let router_module = py + .import("probing.handlers.router") + .py_context("Failed to import router module")?; - let handle_func = router_module.getattr("handle_request").map_err(|e| { - EngineError::PluginError(format!("Failed to get handle_request function: {e}")) - })?; + let handle_func = router_module + .getattr("handle_request") + .py_context("Failed to get handle_request function")?; let params_dict = pyo3::types::PyDict::new(py); for (key, value) in ¶ms { params_dict .set_item(key.as_str(), str_to_py(py, value)) - .map_err(|e| { - EngineError::PluginError(format!("Failed to set param '{key}': {e}")) - })?; + .py_context_with(|| format!("Failed to set param '{key}'"))?; } let body_arg = if body.is_empty() { py.None() } else { let body_str = std::str::from_utf8(&body).map_err(|e| { - EngineError::PluginError(format!("Request body is not valid UTF-8: {e}")) + EngineError::plugin(format!("Request body is not valid UTF-8: {e}")) })?; str_to_py(py, body_str) }; let result = handle_func .call1((str_to_py(py, &path), params_dict, body_arg)) - .map_err(|e| { - EngineError::PluginError(format!("Failed to call handle_request: {e}")) - })?; + .py_context("Failed to call handle_request")?; - let result_str: String = result - .extract() - .map_err(|e| EngineError::PluginError(format!("Failed to extract result: {e}")))?; + let result_str: String = result.extract().py_context("Failed to extract result")?; Ok(result_str.into_bytes()) }) diff --git a/probing/extensions/python/src/extensions/python/exttbls.rs b/probing/extensions/python/src/extensions/python/exttbls.rs index 76905fb1..775a3ef3 100644 --- a/probing/extensions/python/src/extensions/python/exttbls.rs +++ b/probing/extensions/python/src/extensions/python/exttbls.rs @@ -17,6 +17,7 @@ use std::sync::{Arc, Mutex, MutexGuard}; use crate::features::native_bridge::with_detached_native; use once_cell::sync::Lazy; +use probing_core::runtime::{BlockOnFallback, RuntimeError}; use probing_core::sync::lock_mutex; use probing_memtable::discover::ExposedTable; use probing_memtable::docs; @@ -25,11 +26,24 @@ use probing_proto::prelude::Ele; use pyo3::prelude::*; use pyo3::types::{PyDict, PyType}; use pyo3::{pyclass, pymethods, Bound, PyResult, Python}; +use thiserror::Error; use crate::features::convert::{ele_to_python, python_to_ele}; type PyTableRow = (Py, Vec>); +#[derive(Debug, Error)] +enum ExternTableError { + #[error("column count mismatch")] + ColumnMismatch, + #[error("table not initialized")] + NotInitialized, + #[error("push_row failed: schema mismatch or row too large")] + PushFailed, + #[error(transparent)] + Memtable(#[from] probing_memtable::MemtableError), +} + /// SQL schema (and filename prefix) for Python extern tables. pub const EXTERN_TABLE_SCHEMA: &str = "python"; @@ -143,11 +157,15 @@ impl PyExternalTableConfig { #[allow(clippy::wrong_self_convention)] // Python-facing method name, kept for API compat fn into_py(&self, py: Python<'_>) -> Py { let dict = PyDict::new(py); - dict.set_item("chunk_size", self.chunk_size).unwrap(); - dict.set_item("discard_threshold", self.discard_threshold) - .unwrap(); - dict.set_item("discard_strategy", &self.discard_strategy) - .unwrap(); + if let Err(e) = dict.set_item("chunk_size", self.chunk_size) { + log::error!("PyExternalTableConfig::into_py chunk_size: {e}"); + } + if let Err(e) = dict.set_item("discard_threshold", self.discard_threshold) { + log::error!("PyExternalTableConfig::into_py discard_threshold: {e}"); + } + if let Err(e) = dict.set_item("discard_strategy", &self.discard_strategy) { + log::error!("PyExternalTableConfig::into_py discard_strategy: {e}"); + } dict.into() } } @@ -270,7 +288,7 @@ impl ExternBacking { } } - fn ensure_registered(&mut self) -> Result<(), String> { + fn ensure_registered(&mut self) -> Result<(), ExternTableError> { if self.table.is_some() { return Ok(()); } @@ -288,8 +306,7 @@ impl ExternBacking { ); let chunk_bytes = ring_chunk_bytes(self.capacity_bytes); let filename = mmap_basename(&self.name); - let table = ExposedTable::create(&filename, &schema, chunk_bytes, NUM_CHUNKS) - .map_err(|e| format!("failed to register mmap table {filename}: {e}"))?; + let table = ExposedTable::create(&filename, &schema, chunk_bytes, NUM_CHUNKS)?; self.table = Some(table); Ok(()) } @@ -301,7 +318,7 @@ impl ExternBacking { }) } - fn ensure_table(&mut self, first_row: &[Ele]) -> Result<(), String> { + fn ensure_table(&mut self, first_row: &[Ele]) -> Result<(), ExternTableError> { if self.table.is_some() && self.row_count() > 0 { return Ok(()); } @@ -318,16 +335,15 @@ impl ExternBacking { ); let chunk_bytes = ring_chunk_bytes(self.capacity_bytes); let filename = mmap_basename(&self.name); - let table = ExposedTable::create(&filename, &schema, chunk_bytes, NUM_CHUNKS) - .map_err(|e| format!("failed to create mmap table {filename}: {e}"))?; + let table = ExposedTable::create(&filename, &schema, chunk_bytes, NUM_CHUNKS)?; self.dtypes = dtypes; self.table = Some(table); Ok(()) } - fn append(&mut self, timestamp: i64, values: &[Ele]) -> Result<(), String> { + fn append(&mut self, timestamp: i64, values: &[Ele]) -> Result<(), ExternTableError> { if values.len() != self.columns.len() { - return Err("column count mismatch".to_string()); + return Err(ExternTableError::ColumnMismatch); } self.ensure_table(values)?; @@ -344,10 +360,10 @@ impl ExternBacking { // ExposedTable::push_row validates schema and auto-advances chunks. let Some(table) = self.table.as_mut() else { - return Err("table not initialized".to_string()); + return Err(ExternTableError::NotInitialized); }; if !table.push_row(&row) { - return Err("push_row failed: schema mismatch or row too large".to_string()); + return Err(ExternTableError::PushFailed); } Ok(()) } @@ -426,6 +442,21 @@ fn lock_backing(backing: &Mutex) -> MutexGuard<'_, ExternBacking> #[derive(Clone, Debug)] pub struct ExternalTable(Arc>, usize); +impl BlockOnFallback for ExternalTable { + fn on_block_on_failure(err: RuntimeError) -> Self { + log::error!("ExternalTable bridge degraded: {err}; using no-op table"); + let backing = ExternalTable::create_backing( + "__probing_degraded__", + Vec::new(), + 4096, + "BaseMemorySize", + Some("probing degraded placeholder".into()), + HashMap::new(), + ); + ExternalTable(backing, 0) + } +} + impl ExternalTable { fn extract_eles(values: Vec>) -> Vec { Python::attach(|py| { @@ -571,7 +602,7 @@ impl ExternalTable { with_detached_native(move || { lock_backing(backing.as_ref()) .append(now_micros(), &eles) - .map_err(pyo3::exceptions::PyValueError::new_err) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string())) }) } @@ -586,7 +617,7 @@ impl ExternalTable { with_detached_native(move || { lock_backing(backing.as_ref()) .append(t, &eles) - .map_err(pyo3::exceptions::PyValueError::new_err) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string())) }) } diff --git a/probing/extensions/python/src/extensions/python/tbls.rs b/probing/extensions/python/src/extensions/python/tbls.rs index ab204d97..0b89d212 100644 --- a/probing/extensions/python/src/extensions/python/tbls.rs +++ b/probing/extensions/python/src/extensions/python/tbls.rs @@ -2,8 +2,6 @@ use std::collections::HashMap; use std::ffi::CString; use std::sync::Arc; -use anyhow::Result; - use log::{debug, error}; use probing_core::core::{ ArrayRef, CustomNamespace, DataType, Field, Float64Array, Int64Array, NamespaceProbeDataSource, @@ -20,13 +18,41 @@ use pyo3::types::PyString; use pyo3::Bound; use pyo3::PyAny; use pyo3::Python; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum PythonTableError { + #[error("invalid Python expression: {0}")] + InvalidExpression(String), + #[error("internal error building record batch: missing column {0}")] + MissingColumn(String), + #[error("record batch build failed: {0}")] + BatchBuild(String), + #[error(transparent)] + Py(#[from] pyo3::PyErr), + #[error(transparent)] + Nul(#[from] std::ffi::NulError), +} + +pub type TableResult = std::result::Result; + +impl From> for PythonTableError { + fn from(err: pyo3::CastError<'_, '_>) -> Self { + Self::Py(err.into()) + } +} + +fn try_record_batch(schema: SchemaRef, columns: Vec) -> TableResult { + RecordBatch::try_new(schema, columns).map_err(|e| PythonTableError::BatchBuild(e.to_string())) +} #[derive(Default, Debug)] pub struct PythonNamespace {} impl PythonNamespace { - fn get_backtrace_data() -> Result> { - let frames = crate::extensions::python::backtrace(None)?; + fn get_backtrace_data() -> TableResult> { + let frames = crate::extensions::python::backtrace(None) + .map_err(|e| PythonTableError::BatchBuild(e.to_string()))?; if frames.is_empty() { return Ok(vec![]); @@ -97,10 +123,10 @@ impl PythonNamespace { Arc::new(StringArray::from(frame_types)), // Added frame_type array ]; - Ok(vec![RecordBatch::try_new(schema, columns)?]) + Ok(vec![try_record_batch(schema, columns)?]) } - fn data_from_python(expr: &str) -> Result> { + fn data_from_python(expr: &str) -> TableResult> { Python::attach(|py| { let import_path = expr.split(['(', '[']).next().unwrap_or(expr); @@ -110,20 +136,16 @@ impl PythonNamespace { .collect(); if parts.is_empty() { - return Err(anyhow::anyhow!("Invalid Python expression: {}", expr)); + return Err(PythonTableError::InvalidExpression(expr.to_string())); } // Import the top-level package first. let pkg_name = parts[0]; - let pkg = py - .import(pkg_name) - .map_err(|e| anyhow::anyhow!("Failed to import {}: {:?}", pkg_name, e))?; + let pkg = py.import(pkg_name)?; // Set up locals dict with the imported package let locals = PyDict::new(py); - locals - .set_item(pkg_name, pkg) - .map_err(|e| anyhow::anyhow!("Failed to set up Python locals: {:?}", e))?; + locals.set_item(pkg_name, pkg)?; // Ensure intermediate submodules are imported so attribute access works. for depth in 2..=parts.len() { @@ -132,11 +154,7 @@ impl PythonNamespace { Ok(_) => {} Err(err) => { if depth < parts.len() { - return Err(anyhow::anyhow!( - "Failed to import {}: {:?}", - candidate, - err - )); + return Err(err.into()); } break; } @@ -144,12 +162,9 @@ impl PythonNamespace { } // Evaluate the expression - let expr = CString::new(expr) - .map_err(|e| anyhow::anyhow!("Failed to convert expression to CString: {:?}", e))?; + let expr = CString::new(expr)?; - let result = py - .eval(&expr, None, Some(&locals)) - .map_err(|e| anyhow::anyhow!("Failed to evaluate Python expression: {:?}", e))?; + let result = py.eval(&expr, None, Some(&locals))?; // Handle different Python types if let Ok(list) = result.cast::() { @@ -204,12 +219,12 @@ impl CustomNamespace for PythonNamespace { } impl PythonNamespace { - pub fn object_to_recordbatch(obj: Bound<'_, PyAny>) -> Result> { + pub fn object_to_recordbatch(obj: Bound<'_, PyAny>) -> TableResult> { let mut fields: Vec = vec![]; let mut columns: Vec = vec![]; if obj.is_instance_of::() { - let item = obj.cast::().unwrap(); + let item = obj.cast::()?; for (key, value) in item.iter() { let key_str = key.extract::()?; Self::add_field_and_array(&mut fields, &mut columns, key_str, value)?; @@ -225,7 +240,7 @@ impl PythonNamespace { } let schema = SchemaRef::new(Schema::new(fields)); - let batches = vec![RecordBatch::try_new(schema, columns)?]; + let batches = vec![try_record_batch(schema, columns)?]; Ok(batches) } @@ -236,7 +251,7 @@ impl PythonNamespace { columns: &mut Vec, name: String, value: Bound<'_, PyAny>, - ) -> Result<()> { + ) -> TableResult<()> { if value.is_instance_of::() { let array = Int64Array::from(vec![value.extract::()?]); columns.push(Arc::new(array)); @@ -257,7 +272,7 @@ impl PythonNamespace { Ok(()) } - pub fn dict_to_recordbatch(dict: &Bound<'_, PyDict>) -> Result> { + pub fn dict_to_recordbatch(dict: &Bound<'_, PyDict>) -> TableResult> { let mut fields: Vec = vec![]; let mut columns: Vec = vec![]; @@ -267,12 +282,12 @@ impl PythonNamespace { } let schema = SchemaRef::new(Schema::new(fields)); - let batches = vec![RecordBatch::try_new(schema, columns)?]; + let batches = vec![try_record_batch(schema, columns)?]; Ok(batches) } - pub fn list_to_recordbatch(list: &Bound<'_, PyList>) -> Result> { + pub fn list_to_recordbatch(list: &Bound<'_, PyList>) -> TableResult> { let mut names: Vec = vec![]; let mut datas: HashMap>>> = Default::default(); @@ -282,10 +297,10 @@ impl PythonNamespace { Some(dict.clone()) } else { match item.getattr("__dict__") { - Ok(dict) => Some(dict.cast::().unwrap().clone()), + Ok(dict) => Some(dict.cast::()?.clone()), Err(_) => { let dict = PyDict::new(item.py()); - dict.set_item("value", item).unwrap(); + dict.set_item("value", item)?; Some(dict) } } @@ -321,7 +336,10 @@ impl PythonNamespace { let mut columns: Vec = vec![]; for name in names.iter() { - let values = datas.get(name).unwrap(); + let values = datas.get(name).ok_or_else(|| { + error!("list_to_recordbatch: internal column map missing key {name}"); + PythonTableError::MissingColumn(name.clone()) + })?; let array = StringArray::from( values .iter() @@ -342,7 +360,7 @@ impl PythonNamespace { } let schema = SchemaRef::new(Schema::new(fields)); - let batches = vec![RecordBatch::try_new(schema, columns).unwrap()]; + let batches = vec![try_record_batch(schema, columns)?]; Ok(batches) } diff --git a/probing/extensions/python/src/features/config.rs b/probing/extensions/python/src/features/config.rs index 96eb1ec4..549a5143 100644 --- a/probing/extensions/python/src/features/config.rs +++ b/probing/extensions/python/src/features/config.rs @@ -6,6 +6,7 @@ use probing_core::runtime::block_on; use crate::features::convert::{ele_to_python, python_to_ele}; use crate::features::native_bridge::with_detached_native; +use crate::features::py_result::runtime_err; /// Get a configuration value. /// @@ -14,12 +15,10 @@ use crate::features::native_bridge::with_detached_native; #[pyfunction(name = "config_get")] fn get(_py: Python, key: String) -> PyResult>> { with_detached_native(move || { - Python::attach(|py| { - let ele = block_on(async move { config::get(&key).await }); - match ele { - Some(val) => Ok(Some(ele_to_python(py, &val)?)), - None => Ok(None), - } + let ele = block_on(async move { config::get(&key).await }).map_err(runtime_err)?; + Python::attach(|py| match ele { + Some(val) => Ok(Some(ele_to_python(py, &val)?)), + None => Ok(None), }) }) } @@ -28,11 +27,9 @@ fn get(_py: Python, key: String) -> PyResult>> { #[pyfunction(name = "config_write")] fn write(_py: Python, key: String, value: String) -> PyResult<()> { with_detached_native(move || { - block_on(async move { - config::write(&key, &value) - .await - .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string())) - }) + block_on(async move { config::write(&key, &value).await }) + .map_err(runtime_err)? + .map_err(runtime_err) }) } @@ -43,11 +40,8 @@ fn write(_py: Python, key: String, value: String) -> PyResult<()> { fn set(_py: Python, key: String, value: Bound<'_, PyAny>) -> PyResult<()> { let value = value.unbind(); with_detached_native(move || { - Python::attach(|py| { - let ele = python_to_ele(value.bind(py))?; - block_on(async move { config::set(&key, ele).await }); - Ok(()) - }) + let ele = Python::attach(|py| python_to_ele(value.bind(py)))?; + block_on(async move { config::set(&key, ele).await }).map_err(runtime_err) }) } @@ -57,53 +51,53 @@ fn set(_py: Python, key: String, value: Bound<'_, PyAny>) -> PyResult<()> { /// converted to string. #[pyfunction(name = "config_get_str")] fn get_str(_py: Python, key: String) -> PyResult> { - Ok(with_detached_native(move || { - block_on(async move { config::get_str(&key).await }) - })) + with_detached_native(move || { + block_on(async move { config::get_str(&key).await }).map_err(runtime_err) + }) } /// Check if a configuration key exists. #[pyfunction(name = "config_contains_key")] -fn contains_key(_py: Python, key: String) -> bool { - with_detached_native(move || block_on(async move { config::contains_key(&key).await })) +fn contains_key(_py: Python, key: String) -> PyResult { + with_detached_native(move || { + block_on(async move { config::contains_key(&key).await }).map_err(runtime_err) + }) } /// Remove a configuration key and return its value. #[pyfunction(name = "config_remove")] fn remove(_py: Python, key: String) -> PyResult>> { with_detached_native(move || { - Python::attach(|py| { - let ele = block_on(async move { config::remove(&key).await }); - match ele { - Some(val) => Ok(Some(ele_to_python(py, &val)?)), - None => Ok(None), - } + let ele = block_on(async move { config::remove(&key).await }).map_err(runtime_err)?; + Python::attach(|py| match ele { + Some(val) => Ok(Some(ele_to_python(py, &val)?)), + None => Ok(None), }) }) } /// Get all configuration keys. #[pyfunction(name = "config_keys")] -fn keys(_py: Python) -> Vec { - with_detached_native(|| block_on(config::keys())) +fn keys(_py: Python) -> PyResult> { + with_detached_native(|| block_on(config::keys()).map_err(runtime_err)) } /// Clear all configuration. #[pyfunction(name = "config_clear")] -fn clear(_py: Python) { - with_detached_native(|| block_on(config::clear())); +fn clear(_py: Python) -> PyResult<()> { + with_detached_native(|| block_on(config::clear()).map_err(runtime_err)) } /// Get the number of configuration entries. #[pyfunction(name = "config_len")] -fn len(_py: Python) -> usize { - with_detached_native(|| block_on(config::len())) +fn len(_py: Python) -> PyResult { + with_detached_native(|| block_on(config::len()).map_err(runtime_err)) } /// Check if the configuration store is empty. #[pyfunction(name = "config_is_empty")] -fn is_empty(_py: Python) -> bool { - with_detached_native(|| block_on(config::is_empty())) +fn is_empty(_py: Python) -> PyResult { + with_detached_native(|| block_on(config::is_empty()).map_err(runtime_err)) } /// Register the config functions directly to the probing Python module. diff --git a/probing/extensions/python/src/features/convert.rs b/probing/extensions/python/src/features/convert.rs index 5a24b47a..bd3ff16c 100644 --- a/probing/extensions/python/src/features/convert.rs +++ b/probing/extensions/python/src/features/convert.rs @@ -25,12 +25,15 @@ pub fn ele_to_python(py: Python, ele: &Ele) -> PyResult> { // Convert microsecond timestamp to string representation use std::time::{Duration, UNIX_EPOCH}; let datetime = UNIX_EPOCH + Duration::from_micros(*t); - let s = datetime + let secs = datetime .duration_since(UNIX_EPOCH) - .unwrap() + .unwrap_or_else(|e| { + log::error!("DataTime before UNIX epoch ({t} µs): {e}; using 0"); + Duration::ZERO + }) .as_secs() .to_string(); - PyString::new(py, &s).to_owned().unbind().into() + PyString::new(py, &secs).to_owned().unbind().into() } }; Ok(obj) diff --git a/probing/extensions/python/src/features/mod.rs b/probing/extensions/python/src/features/mod.rs index 1f33b1ab..6d6db424 100644 --- a/probing/extensions/python/src/features/mod.rs +++ b/probing/extensions/python/src/features/mod.rs @@ -5,6 +5,7 @@ pub mod crash; pub mod flamegraph; pub mod native_bridge; pub mod pprof; +pub mod py_result; pub mod python_api; pub mod spy; /// Native stack capture, Python/native merge, and signal handling (`SIGUSR2`). diff --git a/probing/extensions/python/src/features/native_bridge.rs b/probing/extensions/python/src/features/native_bridge.rs index 65ea3444..b3c9cbfe 100644 --- a/probing/extensions/python/src/features/native_bridge.rs +++ b/probing/extensions/python/src/features/native_bridge.rs @@ -3,6 +3,8 @@ use pyo3::prelude::*; use probing_core::run_on_native_thread; /// Run Rust/Python bridge work off the Python main thread and Tokio workers. -pub fn with_detached_native(f: impl FnOnce() -> R + Send + 'static) -> R { +pub fn with_detached_native( + f: impl FnOnce() -> R + Send + 'static, +) -> R { Python::attach(|py| py.detach(|| run_on_native_thread(f))) } diff --git a/probing/extensions/python/src/features/pprof.rs b/probing/extensions/python/src/features/pprof.rs index be10fbc3..0f5c9c3a 100644 --- a/probing/extensions/python/src/features/pprof.rs +++ b/probing/extensions/python/src/features/pprof.rs @@ -32,7 +32,7 @@ use std::sync::{Mutex, RwLock}; use std::thread; use std::time::Duration; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use core::ffi::{c_int, c_void}; use nix::libc; use once_cell::sync::Lazy; @@ -884,7 +884,7 @@ pub fn setup(freq: u64) -> Result<()> { thread::Builder::new() .name("probing-sampler".into()) .spawn(move || consumer_loop(my_gen)) - .map_err(|e| anyhow!("failed to spawn sampler consumer thread: {e}"))?; + .context("failed to spawn sampler consumer thread")?; arm_timer(freq); log::info!("probing: SIGPROF CPU sampler started ({freq} Hz, Python+native)"); diff --git a/probing/extensions/python/src/features/py_result.rs b/probing/extensions/python/src/features/py_result.rs new file mode 100644 index 00000000..febce592 --- /dev/null +++ b/probing/extensions/python/src/features/py_result.rs @@ -0,0 +1,7 @@ +use pyo3::exceptions::PyRuntimeError; +use pyo3::PyErr; + +/// Map a displayable error into `PyRuntimeError` (Python API boundary). +pub fn runtime_err(err: impl std::fmt::Display) -> PyErr { + PyRuntimeError::new_err(err.to_string()) +} diff --git a/probing/extensions/python/src/features/python_api.rs b/probing/extensions/python/src/features/python_api.rs index dd749171..f51911f2 100644 --- a/probing/extensions/python/src/features/python_api.rs +++ b/probing/extensions/python/src/features/python_api.rs @@ -5,6 +5,7 @@ use probing_core::runtime::block_on; use probing_core::ENGINE; use crate::features::native_bridge::with_detached_native; +use crate::features::py_result::runtime_err; use crate::features::stack_tracer::{SignalTracer, StackTracer}; use crate::repl::PythonRepl; @@ -22,10 +23,10 @@ pub fn is_enabled() -> bool { pub fn query_json(_py: Python, sql: String) -> PyResult { with_detached_native(move || { let df = block_on(async move { ENGINE.read().await.async_query(sql.as_str()).await }) - .map_err(|e| PyErr::new::(e.to_string()))? + .map_err(runtime_err)? + .map_err(runtime_err)? .unwrap_or_default(); - serde_json::to_string(&df) - .map_err(|e| PyErr::new::(e.to_string())) + serde_json::to_string(&df).map_err(runtime_err) }) } @@ -34,11 +35,8 @@ pub fn query_json(_py: Python, sql: String) -> PyResult { #[pyo3(signature = (tid=None))] pub fn api_callstack(tid: Option) -> PyResult { let tid = tid.filter(|&t| t != 0); - let frames = SignalTracer - .trace(tid) - .map_err(|e| PyErr::new::(e.to_string()))?; - serde_json::to_string(&frames) - .map_err(|e| PyErr::new::(e.to_string())) + let frames = SignalTracer.trace(tid).map_err(runtime_err)?; + serde_json::to_string(&frames).map_err(runtime_err) } /// HTTP `POST /apis/pythonext/eval` backend. diff --git a/probing/extensions/python/src/features/stack_tracer.rs b/probing/extensions/python/src/features/stack_tracer.rs index c67ffe00..e0001ed7 100644 --- a/probing/extensions/python/src/features/stack_tracer.rs +++ b/probing/extensions/python/src/features/stack_tracer.rs @@ -14,6 +14,8 @@ use pyo3::Python; use probing_proto::prelude::CallFrame; use probing_core::is_python_main_thread; +#[cfg(target_os = "macos")] +use probing_core::signal::send_sigusr2_to_thread_id; use crate::features::vm_tracer::{get_python_frames_raw, get_python_stacks_raw}; @@ -222,7 +224,7 @@ impl SignalTracer { Ok(()) } } else { - probing_cc::extensions::send_sigusr2_to_thread_id(tid) + send_sigusr2_to_thread_id(tid) }; if let Err(e) = signal_result { Self::clear_sender_slot(); diff --git a/probing/extensions/python/src/features/torch.rs b/probing/extensions/python/src/features/torch.rs index 65bcf986..d6695b93 100644 --- a/probing/extensions/python/src/features/torch.rs +++ b/probing/extensions/python/src/features/torch.rs @@ -1,6 +1,6 @@ use std::{collections::BTreeMap, collections::HashMap, thread}; -use anyhow::Result; +use anyhow::{Context, Result}; use log::{error, warn}; use serde_json::json; @@ -261,9 +261,10 @@ fn query_profiling_impl(query: &str) -> Result let result = engine .async_query(&query) .await - .map_err(|e| anyhow::anyhow!("Torch query failed: {e}"))?; + .context("Torch query failed")?; Ok(result.unwrap_or_default()) }) + .map_err(anyhow::Error::new)? } fn run_torch_query(query: &str) -> Result { @@ -278,7 +279,7 @@ fn run_torch_query(query: &str) -> Result { let engine = tokio::runtime::Builder::new_current_thread() .enable_all() .build() - .map_err(|e| anyhow::anyhow!("failed to build tokio runtime: {e}"))? + .context("failed to build tokio runtime")? .block_on(async { probing_core::create_engine() .with_data_source(PythonProbeDataSource::create("python")) @@ -288,7 +289,7 @@ fn run_torch_query(query: &str) -> Result { let rt = tokio::runtime::Builder::new_current_thread() .enable_all() .build() - .map_err(|e| anyhow::anyhow!("failed to build tokio runtime: {e}"))?; + .context("failed to build tokio runtime")?; Ok(rt .block_on(async { engine.async_query(&query).await })? .unwrap_or_default()) diff --git a/probing/extensions/python/src/features/tracing.rs b/probing/extensions/python/src/features/tracing.rs index d1e7881a..f27556a5 100644 --- a/probing/extensions/python/src/features/tracing.rs +++ b/probing/extensions/python/src/features/tracing.rs @@ -382,7 +382,7 @@ impl Span { _exc_val: Option<&Bound<'_, PyAny>>, _exc_tb: Option<&Bound<'_, PyAny>>, ) -> PyResult { - if exc_type.is_some() && !exc_type.unwrap().is_none() { + if exc_type.is_some_and(|t| !t.is_none()) { capture_span_snapshot_for_crash(); } let self_id = slf.span_id(); diff --git a/probing/extensions/python/src/python.rs b/probing/extensions/python/src/python.rs index 4fc233d8..eab77434 100644 --- a/probing/extensions/python/src/python.rs +++ b/probing/extensions/python/src/python.rs @@ -1,6 +1,7 @@ use std::ffi::CString; use std::sync::atomic::{AtomicBool, Ordering}; +use anyhow::Context; use pyo3::prelude::*; use pyo3::{types::PyDict, Bound, Python}; @@ -108,7 +109,7 @@ pub fn enable_monitoring(filename: &str) -> anyhow::Result<()> { ) })?; run_embedded(py, &code, None, None) - .map_err(|err| anyhow::anyhow!("error apply monitoring {filename}: {err}"))?; + .with_context(|| format!("error apply monitoring {filename}"))?; Ok(()) }) } diff --git a/probing/extensions/python/src/repl/python_repl.rs b/probing/extensions/python/src/repl/python_repl.rs index ff8a321e..f382c908 100644 --- a/probing/extensions/python/src/repl/python_repl.rs +++ b/probing/extensions/python/src/repl/python_repl.rs @@ -29,7 +29,14 @@ impl Default for PythonRepl { impl PythonRepl { pub fn process(&mut self, cmd: &str) -> Option { - self.console.lock().unwrap().try_execute(cmd.to_string()) + let mut guard = match self.console.lock() { + Ok(guard) => guard, + Err(poison) => { + log::warn!("python repl console mutex poisoned; recovering"); + poison.into_inner() + } + }; + guard.try_execute(cmd.to_string()) } } diff --git a/probing/memtable/Cargo.toml b/probing/memtable/Cargo.toml index d8f874cc..e3a95c6b 100644 --- a/probing/memtable/Cargo.toml +++ b/probing/memtable/Cargo.toml @@ -9,6 +9,7 @@ description = "Self-describing columnar memory table with ring buffer" [dependencies] log = "0.4" +thiserror = { workspace = true } xxhash-rust = { version = "0.8", features = ["xxh3"] } memmap2 = "0.9" libc = "0.2" diff --git a/probing/memtable/src/discover.rs b/probing/memtable/src/discover.rs index 897e9cd5..f07602ca 100644 --- a/probing/memtable/src/discover.rs +++ b/probing/memtable/src/discover.rs @@ -20,23 +20,26 @@ //! use probing_memtable::discover::{ExposedTable, discover}; //! use probing_memtable::{Schema, DType, Value}; //! -//! // Writer: expose a table as an mmap'd file -//! let schema = Schema::new().col("ts", DType::I64).col("cpu", DType::F64); -//! let mut table = ExposedTable::create("metrics", &schema, 4096, 8).unwrap(); -//! { -//! let mut w = table.writer(); -//! w.push_row(&[Value::I64(1000), Value::F64(0.85)]); -//! } +//! fn main() -> Result<(), Box> { +//! // Writer: expose a table as an mmap'd file +//! let schema = Schema::new().col("ts", DType::I64).col("cpu", DType::F64); +//! let mut table = ExposedTable::create("metrics", &schema, 4096, 8)?; +//! { +//! let mut w = table.writer()?; +//! w.push_row(&[Value::I64(1000), Value::F64(0.85)]); +//! } //! -//! // Reader (same or different process): discover and read -//! for t in discover().unwrap() { -//! if t.is_alive() { -//! let view = t.view().unwrap(); -//! for row in view.rows(view.write_chunk()) { -//! let mut c = row.cursor(); -//! println!("{} {}", c.next_i64(), c.next_f64()); +//! // Reader (same or different process): discover and read +//! for t in discover()? { +//! if t.is_alive() { +//! let view = t.view()?; +//! for row in view.rows(view.write_chunk()) { +//! let mut c = row.cursor(); +//! println!("{} {}", c.next_i64(), c.next_f64()); +//! } //! } //! } +//! Ok(()) //! } //! ``` @@ -117,7 +120,7 @@ impl ExposedTable { schema: &Schema, chunk_size: u32, num_chunks: u32, - ) -> io::Result { + ) -> crate::error::Result { Self::create_in(&default_dir(), name, schema, chunk_size, num_chunks) } @@ -130,7 +133,7 @@ impl ExposedTable { schema: &Schema, chunk_size: u32, num_chunks: u32, - ) -> io::Result { + ) -> crate::error::Result { let inner = MemTable::shared_in(base_dir, name, schema, chunk_size, num_chunks)?; crate::docs::register_from_name(name, schema); Ok(Self { inner }) @@ -144,17 +147,34 @@ impl ExposedTable { self.inner.as_bytes_mut() } - /// File path of this table. - pub fn path(&self) -> &Path { - self.inner.path().expect("ExposedTable is always shared") + /// File path of this table, when backed by a mmap file. + /// + /// Returns [`None`] if the inner table is not file-backed (should not happen + /// for tables created via [`Self::create`] / [`Self::create_in`]). + pub fn path(&self) -> Option<&Path> { + self.inner.path() + } + + /// File path of this table, logging when the backing is not a file. + pub fn try_path(&self) -> Result<&Path, crate::error::MemtableError> { + self.inner + .path() + .ok_or(crate::error::MemtableError::NotFileBacked) + .inspect_err(|e| { + log::error!("ExposedTable::path: {e}"); + }) } /// Create a [`MemTableWriter`] backed by the mmap'd region. /// /// **Note**: this re-validates the entire buffer on every call. /// Prefer [`push_row`](Self::push_row) for hot-path writes. - pub fn writer(&mut self) -> MemTableWriter<'_> { - MemTableWriter::new(self.inner.as_bytes_mut()).expect("mmap buffer validated at creation") + pub fn writer(&mut self) -> Result, crate::error::MemtableError> { + MemTableWriter::new(self.inner.as_bytes_mut()).inspect_err(|e| { + log::error!( + "ExposedTable::writer: mmap buffer invalid ({e}); table writes unavailable" + ); + }) } /// Append a row without re-validating the buffer. @@ -190,7 +210,7 @@ impl ExposedHashTable { num_buckets: u32, arena_cap: usize, hash_seed: u64, - ) -> io::Result { + ) -> crate::error::Result { Self::create_in(&default_dir(), name, num_buckets, arena_cap, hash_seed) } @@ -201,7 +221,7 @@ impl ExposedHashTable { num_buckets: u32, arena_cap: usize, hash_seed: u64, - ) -> io::Result { + ) -> crate::error::Result { let dir = base_dir.join(std::process::id().to_string()); fs::create_dir_all(&dir)?; @@ -217,8 +237,7 @@ impl ExposedHashTable { file.set_len(size as u64)?; let mut mmap = unsafe { MmapMut::map_mut(&file)? }; - memh_init_buf(&mut mmap, num_buckets, arena_cap, hash_seed) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("{e}")))?; + memh_init_buf(&mut mmap, num_buckets, arena_cap, hash_seed)?; Ok(Self { mmap, path, dir }) } @@ -235,12 +254,20 @@ impl ExposedHashTable { &self.path } - pub fn writer(&mut self) -> MemhWriter<'_> { - MemhWriter::new(&mut self.mmap).expect("mmap buffer validated at creation") + pub fn writer(&mut self) -> Result, crate::error::MemtableError> { + MemhWriter::new(&mut self.mmap) + .inspect_err(|e| { + log::error!("ExposedHashTable::writer: buffer invalid ({e}); writes unavailable"); + }) + .map_err(Into::into) } - pub fn view(&self) -> MemhView<'_> { - MemhView::new(&self.mmap).expect("mmap buffer validated at creation") + pub fn view(&self) -> Result, crate::error::MemtableError> { + MemhView::new(&self.mmap) + .inspect_err(|e| { + log::error!("ExposedHashTable::view: buffer invalid ({e}); reads unavailable"); + }) + .map_err(Into::into) } } @@ -315,7 +342,7 @@ impl DiscoveredTable { } /// Wrap the mmap'd region as a [`MemTableView`]. - pub fn view(&self) -> Result, &'static str> { + pub fn view(&self) -> Result, crate::error::MemtableError> { MemTableView::new(&self.mmap) } @@ -520,10 +547,10 @@ mod tests { { let mut table = ExposedTable::create_in(&dir, "metrics", &schema, 4096, 4).unwrap(); - assert!(table.path().exists()); + assert!(table.path().is_some_and(|p| p.exists())); { - let mut w = table.writer(); + let mut w = table.writer().expect("valid mmap table"); w.push_row(&[Value::I64(1000), Value::F64(3.14)]); w.push_row(&[Value::I64(2000), Value::F64(2.72)]); } @@ -545,7 +572,7 @@ mod tests { let mut table = ExposedTable::create_in(&dir, "test_table", &schema, 1024, 2).unwrap(); { - let mut w = table.writer(); + let mut w = table.writer().expect("valid mmap table"); w.push_row(&[Value::I32(42)]); } @@ -620,7 +647,7 @@ mod tests { let path; { let table = ExposedTable::create_in(&dir, "ephemeral", &schema, 256, 1).unwrap(); - path = table.path().to_owned(); + path = table.path().expect("file-backed ExposedTable").to_owned(); assert!(path.exists()); } assert!(!path.exists()); diff --git a/probing/memtable/src/docs.rs b/probing/memtable/src/docs.rs index ffb30f62..0abbd221 100644 --- a/probing/memtable/src/docs.rs +++ b/probing/memtable/src/docs.rs @@ -64,7 +64,7 @@ pub fn register_qualified(table_schema: &str, table_name: &str, schema: &Schema) } } - let mut reg = registry().lock().expect("table doc registry lock"); + let mut reg = crate::sync::lock_mutex(registry(), "table doc registry"); reg.insert(key, entry); } @@ -79,7 +79,7 @@ pub fn register_from_name(name: &str, schema: &Schema) { /// Snapshot all registered docs (sorted by qualified name). pub fn snapshot() -> Vec { - let reg = registry().lock().expect("table doc registry lock"); + let reg = crate::sync::lock_mutex(registry(), "table doc registry"); let mut rows: Vec = reg.values().cloned().collect(); rows.sort_by(|a, b| (&a.table_schema, &a.table_name).cmp(&(&b.table_schema, &b.table_name))); rows @@ -88,7 +88,7 @@ pub fn snapshot() -> Vec { /// Column names registered for `schema.table` (sorted, deduplicated). pub fn registered_column_names(table_schema: &str, table_name: &str) -> Vec { let key = qualified_key(table_schema, table_name); - let reg = registry().lock().expect("table doc registry lock"); + let reg = crate::sync::lock_mutex(registry(), "table doc registry"); let Some(entry) = reg.get(&key) else { return Vec::new(); }; @@ -106,7 +106,7 @@ pub fn register_column_docs( columns: &[(String, String)], ) { let key = qualified_key(table_schema, table_name); - let mut reg = registry().lock().expect("table doc registry lock"); + let mut reg = crate::sync::lock_mutex(registry(), "table doc registry"); let entry = reg.entry(key).or_insert_with(|| TableDocs { table_schema: table_schema.to_string(), table_name: table_name.to_string(), diff --git a/probing/memtable/src/error.rs b/probing/memtable/src/error.rs new file mode 100644 index 00000000..0b1d763a --- /dev/null +++ b/probing/memtable/src/error.rs @@ -0,0 +1,30 @@ +//! Unified errors for the memtable crate (L1 — no dependency on probing-core). + +use thiserror::Error; + +/// Errors from mmap tables, ring buffers, and hash-table operations. +#[derive(Debug, Error)] +pub enum MemtableError { + #[error(transparent)] + Io(#[from] std::io::Error), + + #[error("invalid memtable buffer: {0}")] + InvalidBuffer(&'static str), + + #[error("table is not file-backed")] + NotFileBacked, + + #[error(transparent)] + Memh(#[from] crate::memh::MemhValidateError), + + #[error(transparent)] + MemhInit(#[from] crate::memh::MemhInitError), +} + +pub type Result = std::result::Result; + +impl From for std::io::Error { + fn from(e: MemtableError) -> Self { + std::io::Error::new(std::io::ErrorKind::InvalidData, e) + } +} diff --git a/probing/memtable/src/lib.rs b/probing/memtable/src/lib.rs index 72580083..8a03ed59 100644 --- a/probing/memtable/src/lib.rs +++ b/probing/memtable/src/lib.rs @@ -101,6 +101,7 @@ mod cache; mod dedup; pub mod discover; pub mod docs; +pub mod error; mod layout; pub mod memc; pub mod memh; @@ -109,10 +110,12 @@ mod raw; mod refcount; mod row; mod schema; +mod sync; mod writer; pub use cache::{CachedCursor, CachedReader}; pub use docs::infer_extern_column_dtype; +pub use error::{MemtableError, Result as MemtableResult}; pub use layout::MAGIC_MEMT; pub use memh::{ init_buf as init_memh_buf, validate_memh, InsertError, InsertResult, MemhInitError, @@ -150,7 +153,7 @@ pub fn detect_table(buf: &[u8]) -> Option { if buf.len() < 4 { return None; } - let magic = u32::from_le_bytes(buf[..4].try_into().unwrap()); + let magic = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]); match magic { MAGIC_MEMT => Some(TableKind::Ring), MAGIC_MEMH => Some(TableKind::Hash), diff --git a/probing/memtable/src/memh/table.rs b/probing/memtable/src/memh/table.rs index 3fe826c2..5bc3479e 100644 --- a/probing/memtable/src/memh/table.rs +++ b/probing/memtable/src/memh/table.rs @@ -6,6 +6,7 @@ use std::sync::atomic::Ordering; use crate::schema::Value; +use crate::sync::lock_mutex; use super::codec::{ encode_inline_bytes, encode_put_inline_record, encode_put_record, encode_tombstone_record, @@ -29,6 +30,8 @@ pub enum MemhInitError { ArenaTooSmall, } +impl std::error::Error for MemhInitError {} + impl std::fmt::Display for MemhInitError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -65,6 +68,8 @@ impl std::fmt::Display for MemhValidateError { } } +impl std::error::Error for MemhValidateError {} + #[derive(Debug, PartialEq, Eq)] pub enum InsertResult { Inserted, @@ -710,32 +715,38 @@ impl SharedMemhWriter { }) } + fn lock_buf(&self) -> std::sync::MutexGuard<'_, Vec> { + lock_mutex(&self.buf, "SharedMemhWriter") + } + /// Insert or update `key` → `val`. Acquires the internal mutex for the /// duration of the operation. pub fn insert(&self, key: &str, val: &Value<'_>) -> Result { - let mut guard = self.buf.lock().expect("SharedMemhWriter mutex poisoned"); + let mut guard = self.lock_buf(); // SAFETY: buffer was validated in new(); Mutex provides exclusive access. unsafe { MemhWriter::new_unchecked(&mut guard) }.insert(key, val) } /// Remove `key`. Acquires the internal mutex. pub fn remove(&self, key: &str) -> bool { - let mut guard = self.buf.lock().expect("SharedMemhWriter mutex poisoned"); + let mut guard = self.lock_buf(); unsafe { MemhWriter::new_unchecked(&mut guard) }.remove(key) } /// Snapshot-read: calls `f` with a [`MemhView`] built from the locked buffer. /// /// Holds the mutex for the duration of `f`; keep the closure short. - pub fn with_view(&self, f: impl FnOnce(&MemhView<'_>) -> R) -> R { - let guard = self.buf.lock().expect("SharedMemhWriter mutex poisoned"); - let view = MemhView::new(&guard).expect("SharedMemhWriter: corrupt buffer"); - f(&view) + pub fn with_view(&self, f: impl FnOnce(&MemhView<'_>) -> R) -> Result { + let guard = self.lock_buf(); + let view = MemhView::new(&guard).inspect_err(|e| { + log::error!("SharedMemhWriter: corrupt buffer: {e}"); + })?; + Ok(f(&view)) } /// Access the raw buffer under the mutex. pub fn with_buf(&self, f: impl FnOnce(&[u8]) -> R) -> R { - let guard = self.buf.lock().expect("SharedMemhWriter mutex poisoned"); + let guard = self.lock_buf(); f(&guard) } } @@ -988,7 +999,8 @@ mod tests { sw.with_view(|v| { assert_eq!(v.get("k1"), Some(TypedValue::U64(1))); assert_eq!(v.get("k2"), Some(TypedValue::I32(-5))); - }); + }) + .unwrap(); } #[test] @@ -1027,7 +1039,8 @@ mod tests { assert_eq!(v.get(&key), Some(expected), "missing {key}"); } } - }); + }) + .unwrap(); } #[test] @@ -1078,6 +1091,7 @@ mod tests { "key shared{k} missing" ); } - }); + }) + .unwrap(); } } diff --git a/probing/memtable/src/memtable.rs b/probing/memtable/src/memtable.rs index a5393645..81f98f4f 100644 --- a/probing/memtable/src/memtable.rs +++ b/probing/memtable/src/memtable.rs @@ -353,6 +353,13 @@ fn shm_name_cstring(name: &str) -> io::Result { .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "shm name contains NUL")) } +fn cstring_to_shm_name(cname: std::ffi::CString) -> io::Result { + cname.into_string().map_err(|e| { + log::error!("shm name is not valid UTF-8: {e:?}"); + io::Error::new(io::ErrorKind::InvalidInput, "shm name is not valid UTF-8") + }) +} + /// `shm_open` wrapper returning an owned [`std::fs::File`]. fn shm_open_file(name: &std::ffi::CString, oflag: libc::c_int) -> io::Result { use std::os::fd::FromRawFd; @@ -398,7 +405,7 @@ impl MemTable { } /// Adopt an existing heap buffer (validates the MEMT layout). - pub fn from_buf(buf: Vec) -> Result { + pub fn from_buf(buf: Vec) -> Result { validate_buf(&buf)?; Ok(Self { backing: Backing::Heap(buf), @@ -429,7 +436,7 @@ impl MemTable { Ok(Self { backing: Backing::Shm { mmap, - name: cname.into_string().expect("validated utf-8"), + name: cstring_to_shm_name(cname)?, unlink_on_drop: true, }, }) @@ -444,12 +451,12 @@ impl MemTable { let file = shm_open_file(&cname, libc::O_RDWR)?; let mmap = unsafe { MmapMut::map_mut(&file)? }; - validate_buf(&mmap).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + validate_buf(&mmap)?; Ok(Self { backing: Backing::Shm { mmap, - name: cname.into_string().expect("validated utf-8"), + name: cstring_to_shm_name(cname)?, unlink_on_drop: false, }, }) @@ -502,7 +509,7 @@ impl MemTable { let file = OpenOptions::new().read(true).write(true).open(&path)?; let mmap = unsafe { MmapMut::map_mut(&file)? }; - validate_buf(&mmap).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + validate_buf(&mmap)?; Ok(Self { backing: Backing::File { @@ -529,7 +536,7 @@ impl MemTable { schema: &Schema, chunk_size: u32, num_chunks: u32, - ) -> io::Result { + ) -> crate::error::Result { Self::shared_in( &crate::discover::default_dir(), name, @@ -547,7 +554,7 @@ impl MemTable { schema: &Schema, chunk_size: u32, num_chunks: u32, - ) -> io::Result { + ) -> crate::error::Result { let dir = base_dir.join(std::process::id().to_string()); std::fs::create_dir_all(&dir)?; @@ -708,7 +715,7 @@ pub struct MemTableView<'a> { } impl<'a> MemTableView<'a> { - pub fn new(buf: &'a [u8]) -> Result { + pub fn new(buf: &'a [u8]) -> Result { validate_buf(buf)?; Ok(Self { buf }) } @@ -752,7 +759,7 @@ pub struct MemTableWriter<'a> { } impl<'a> MemTableWriter<'a> { - pub fn new(buf: &'a mut [u8]) -> Result { + pub fn new(buf: &'a mut [u8]) -> Result { validate_buf(buf)?; Ok(Self { buf, dedup: None }) } diff --git a/probing/memtable/src/raw.rs b/probing/memtable/src/raw.rs index 051f67be..1e662b8b 100644 --- a/probing/memtable/src/raw.rs +++ b/probing/memtable/src/raw.rs @@ -1,3 +1,4 @@ +use crate::error::{MemtableError, Result}; use crate::layout::{ chunk_header, col_desc, col_desc_mut, compute_data_offset, header, header_mut, r32, w32, ChunkHeader, ChunkState, Header, BYTE_ORDER_MARK, CHUNK_HEADER_SIZE, FLAGS_KNOWN, FLAG_DEDUP, @@ -163,13 +164,15 @@ fn validate_chunk_rows( used: usize, nc: usize, has_dedup: bool, -) -> Result<(), &'static str> { +) -> Result<()> { let data_base = cs + CHUNK_HEADER_SIZE; let mut pos = 0usize; while pos + 4 <= used { let row_len = r32(buf, data_base + pos) as usize; if pos + 4 + row_len > used { - return Err("row extends beyond chunk used region"); + return Err(MemtableError::InvalidBuffer( + "row extends beyond chunk used region", + )); } let row_start = data_base + pos + 4; let mut col_off = 0usize; @@ -190,11 +193,13 @@ fn validate_chunk_rows( ); if raw < 0 { if !has_dedup { - return Err("dedup ref in non-dedup table"); + return Err(MemtableError::InvalidBuffer("dedup ref in non-dedup table")); } let ref_off = (-raw) as usize; if ref_off < CHUNK_HEADER_SIZE || ref_off >= CHUNK_HEADER_SIZE + used { - return Err("dedup ref outside chunk data region"); + return Err(MemtableError::InvalidBuffer( + "dedup ref outside chunk data region", + )); } col_off += 4; } else { @@ -214,57 +219,61 @@ fn validate_chunk_rows( /// integrity, and dedup ref ranges. /// /// All `from_buf` / `new` constructors funnel through this function. -pub fn validate_buf(buf: &[u8]) -> Result<(), &'static str> { +pub fn validate_buf(buf: &[u8]) -> Result<()> { if buf.len() < mem::size_of::
() { - return Err("buffer too small for header"); + return Err(MemtableError::InvalidBuffer("buffer too small for header")); } let h = header(buf); if h.magic != MAGIC { - return Err("invalid magic"); + return Err(MemtableError::InvalidBuffer("invalid magic")); } if h.version != VERSION { - return Err("unsupported version"); + return Err(MemtableError::InvalidBuffer("unsupported version")); } if (h.header_size as usize) < mem::size_of::
() { - return Err("header_size too small"); + return Err(MemtableError::InvalidBuffer("header_size too small")); } let bom = u16::from_ne_bytes(BYTE_ORDER_MARK); if h.byte_order != bom { - return Err("byte order mismatch (buffer written on different-endian host)"); + return Err(MemtableError::InvalidBuffer( + "byte order mismatch (buffer written on different-endian host)", + )); } if h.flags & !FLAGS_KNOWN != 0 { - return Err("unknown feature flags set"); + return Err(MemtableError::InvalidBuffer("unknown feature flags set")); } let has_dedup = h.flags & FLAG_DEDUP != 0; let nc = h.num_cols as usize; if h.num_chunks == 0 { - return Err("num_chunks must be > 0"); + return Err(MemtableError::InvalidBuffer("num_chunks must be > 0")); } let csz = h.chunk_size as usize; if csz < CHUNK_HEADER_SIZE + 8 { - return Err("chunk_size too small"); + return Err(MemtableError::InvalidBuffer("chunk_size too small")); } let expected_off = compute_data_offset(nc); if h.data_offset as usize != expected_off { - return Err("invalid data_offset"); + return Err(MemtableError::InvalidBuffer("invalid data_offset")); } let required = expected_off + csz * h.num_chunks as usize; if buf.len() < required { - return Err("buffer too small for data"); + return Err(MemtableError::InvalidBuffer("buffer too small for data")); } for i in 0..nc { let dt = col_desc(buf, i).dtype; if !(1..=9).contains(&dt) { - return Err("invalid column dtype"); + return Err(MemtableError::InvalidBuffer("invalid column dtype")); } } let ts_col = h.ts_col as usize; if ts_col != 0 { if ts_col > nc { - return Err("ts_col out of range"); + return Err(MemtableError::InvalidBuffer("ts_col out of range")); } if DType::from_u32(col_desc(buf, ts_col - 1).dtype) != Some(DType::I64) { - return Err("ts_col must reference an I64 column"); + return Err(MemtableError::InvalidBuffer( + "ts_col must reference an I64 column", + )); } } let payload_cap = csz - CHUNK_HEADER_SIZE; @@ -273,11 +282,13 @@ pub fn validate_buf(buf: &[u8]) -> Result<(), &'static str> { let ch = chunk_header(buf, cs); let state = ch.state.load(Ordering::Acquire); if state > 2 { - return Err("invalid chunk state"); + return Err(MemtableError::InvalidBuffer("invalid chunk state")); } let used = ch.used.load(Ordering::Acquire) as usize; if used > payload_cap { - return Err("chunk used exceeds payload capacity"); + return Err(MemtableError::InvalidBuffer( + "chunk used exceeds payload capacity", + )); } if state == ChunkState::Sealed as u32 && used > 0 { let gen_before = ch.generation.load(Ordering::Acquire); diff --git a/probing/memtable/src/sync.rs b/probing/memtable/src/sync.rs new file mode 100644 index 00000000..50e2b9dc --- /dev/null +++ b/probing/memtable/src/sync.rs @@ -0,0 +1,11 @@ +use std::sync::{Mutex, MutexGuard}; + +pub(crate) fn lock_mutex<'a, T>(m: &'a Mutex, label: &str) -> MutexGuard<'a, T> { + match m.lock() { + Ok(guard) => guard, + Err(poison) => { + log::warn!("{label} mutex poisoned; recovering"); + poison.into_inner() + } + } +} diff --git a/probing/server/Cargo.toml b/probing/server/Cargo.toml index 8572fbdb..ab61bf06 100644 --- a/probing/server/Cargo.toml +++ b/probing/server/Cargo.toml @@ -31,7 +31,7 @@ nix = { workspace = true } once_cell = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -tokio = { workspace = true } +tokio = { workspace = true, features = ["fs"] } async-trait = "0.1.83" bytes = "1" diff --git a/probing/server/src/engine.rs b/probing/server/src/engine.rs index b28dddda..84a51b28 100644 --- a/probing/server/src/engine.rs +++ b/probing/server/src/engine.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use anyhow::{self, Result}; +use anyhow::{Context, Result}; use probing_proto::prelude::*; use crate::extensions as se; @@ -53,7 +53,7 @@ pub async fn initialize_engine() -> Result<()> { #[cfg(feature = "gpu")] gpu::start_gpu_sampling_from_env(); } - result + result.map_err(anyhow::Error::new) } /// Parse `SET key = value` (value may be quoted). @@ -95,9 +95,8 @@ async fn execute_set_via_config(key: &str, value: &str) -> Result<()> { } else { format!("probing.{key}") }; - config::write(&probe_key, value) - .await - .map_err(|e| anyhow::anyhow!("{e}")) + config::write(&probe_key, value).await?; + Ok(()) } pub async fn handle_query(request: Query) -> Result { @@ -106,30 +105,23 @@ pub async fn handle_query(request: Query) -> Result { // We are already running within the Axum/Tokio runtime. if is_set_expr(&expr) { - for q in expr.split(';').filter(|s| !s.trim().is_empty()) { - let trimmed_q = q.trim(); - if trimmed_q.is_empty() { - continue; - } - log::debug!("Executing SET statement: {trimmed_q}"); - if let Some((key, value)) = parse_set_assignment(trimmed_q) { - match execute_set_via_config(key, value).await { - Ok(()) => log::debug!("Successfully configured: {key}={value}"), - Err(e) => { - log::error!("Error executing SET statement '{trimmed_q}': {e}"); - return Err(anyhow::anyhow!("Failed SET query '{trimmed_q}': {e}")); - } - } + for q in expr.split(';').map(str::trim).filter(|s| !s.is_empty()) { + log::debug!("Executing SET statement: {q}"); + // NOTE: `config::write` acquires the engine write lock, so the + // `engine.sql` branch must scope its read lock to that iteration only. + let outcome = if let Some((key, value)) = parse_set_assignment(q) { + execute_set_via_config(key, value).await } else { - let engine = ENGINE.read().await; - match engine.sql(trimmed_q).await { - Ok(_) => log::debug!("Successfully executed: {trimmed_q}"), - Err(e) => { - log::error!("Error executing SET statement '{trimmed_q}': {e}"); - return Err(anyhow::anyhow!("Failed SET query '{trimmed_q}': {e}")); - } - } - } + ENGINE + .read() + .await + .sql(q) + .await + .map(|_| ()) + .map_err(Into::into) + }; + outcome.with_context(|| format!("Failed SET query '{q}'"))?; + log::debug!("Successfully executed SET statement: {q}"); } return Ok(QueryDataFormat::Nil); } @@ -176,8 +168,7 @@ pub async fn query(req: String) -> ApiResult { let reply_message = Message::new(reply_payload); // Serialize the response message - serde_json::to_string(&reply_message).map_err(|e| { - log::error!("Failed to serialize query response: {e}"); - anyhow::anyhow!("Failed to create response: {}", e).into() // Convert to ApiError - }) + serde_json::to_string(&reply_message) + .inspect_err(|e| log::error!("Failed to serialize query response: {e}")) + .map_err(|e| ApiError::internal(format!("Failed to create response: {e}"))) } diff --git a/probing/server/src/report.rs b/probing/server/src/report.rs index e5d983a7..039fdb38 100644 --- a/probing/server/src/report.rs +++ b/probing/server/src/report.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use super::vars::read_probing_address; use crate::cluster_http::{get_i32_env, put_nodes_blocking}; @@ -110,7 +110,7 @@ async fn request_remote(url: &str, nodes: Vec) -> Result) -> Result { diff --git a/probing/server/src/server/cluster_fanout.rs b/probing/server/src/server/cluster_fanout.rs index 3ce31fb9..c3f1f83c 100644 --- a/probing/server/src/server/cluster_fanout.rs +++ b/probing/server/src/server/cluster_fanout.rs @@ -51,7 +51,7 @@ pub async fn remote_query_df(addr: &str, sql: &str) -> anyhow::Result .timeout_global(Some(timeout)) .build() .send(body) - .map_err(|e| anyhow::anyhow!("{e}")) + .map_err(anyhow::Error::new) }) .await??; diff --git a/probing/server/src/torchrun_cluster.rs b/probing/server/src/torchrun_cluster.rs index 3d8a1129..4ce86b83 100644 --- a/probing/server/src/torchrun_cluster.rs +++ b/probing/server/src/torchrun_cluster.rs @@ -199,10 +199,7 @@ fn store_client() -> Option { async fn store_set(key: &str, value: &str) -> Result<()> { let store = store_client().context("MASTER_ADDR/MASTER_PORT not set")?; - store - .set(key, value) - .await - .map_err(|e| anyhow::anyhow!("{e}")) + store.set(key, value).await.map_err(anyhow::Error::new) } async fn store_get(key: &str) -> Result> { diff --git a/tests/regression/rust/probing/core/table_docs_integration.rs b/tests/regression/rust/probing/core/table_docs_integration.rs index 8a019ae5..cc5a1e30 100644 --- a/tests/regression/rust/probing/core/table_docs_integration.rs +++ b/tests/regression/rust/probing/core/table_docs_integration.rs @@ -46,7 +46,7 @@ async fn mmap_schema_docs_visible_in_semantic_catalog() -> Result<()> { { let mut exposed = ExposedTable::create(&qualified, &schema, 4096, 4)?; - let mut writer = exposed.writer(); + let mut writer = exposed.writer()?; writer.push_row(&[Value::F64(12.5), Value::I32(0)]); } diff --git a/web/src/api/mod.rs b/web/src/api/mod.rs index 4c5e0c08..4d9a4a63 100644 --- a/web/src/api/mod.rs +++ b/web/src/api/mod.rs @@ -37,10 +37,7 @@ impl ApiClient { let response = reqwest::get(&url).await?; let status = response.status(); - let body = response - .text() - .await - .map_err(|e| AppError::Api(e.to_string()))?; + let body = response.text().await?; if !status.is_success() { if let Ok(value) = serde_json::from_str::(&body) { @@ -69,10 +66,7 @@ impl ApiClient { return Err(AppError::Api(format!("HTTP error: {}", response.status()))); } - response - .text() - .await - .map_err(|e| AppError::Api(e.to_string())) + Ok(response.text().await?) } /// Send GET request (public wrapper for agent / extensions).