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/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/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..049eb57d 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,8 +325,11 @@ 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 | +| 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` | @@ -372,6 +375,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`~~ | **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` | | 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 +385,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 +433,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..d1b36723 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,8 +269,11 @@ 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 | +| 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` | @@ -313,13 +316,28 @@ sequenceDiagram | 问题 | 现状 | 目标 | |------|------|------| | python → cli | `probing-python` → `probing-cli` | **可接受**(maturin wheel,仅 `cli_main`);禁止扩散 import | +| 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 | | 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 +365,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/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/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/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/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/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 d16f0c65..4ce86b83 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}; @@ -198,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> { @@ -242,7 +240,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 +364,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 +390,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 +447,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 +508,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 +521,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 +551,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 +559,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 +568,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 +590,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 +615,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 +623,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/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/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" }, +] 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).