From 00126292ea95a86d3a6ea1845f47219a6d22492c Mon Sep 17 00:00:00 2001 From: harumiWeb Date: Thu, 5 Feb 2026 21:30:59 +0900 Subject: [PATCH 01/25] clean --- docs/agents/FEATURE_CONCEPT.md | 110 +++++++++++++++++++++++++++++++++ docs/agents/FEATURE_SPEC.md | 94 ---------------------------- docs/agents/TASKS.md | 28 --------- 3 files changed, 110 insertions(+), 122 deletions(-) create mode 100644 docs/agents/FEATURE_CONCEPT.md diff --git a/docs/agents/FEATURE_CONCEPT.md b/docs/agents/FEATURE_CONCEPT.md new file mode 100644 index 0000000..2842bfc --- /dev/null +++ b/docs/agents/FEATURE_CONCEPT.md @@ -0,0 +1,110 @@ +ExStructを用いたAIエージェント向け編集ツール設計提案 +背景と目的 +ExcelドキュメントをExStructでJSON構造に変換することで、セル値・数式・図形・チャートなどを含むシート構造をプログラム的に扱えるようになります +。MCPサーバー上では、このExStructの出力JSONをAIエージェントが読み込み・解析し、必要な編集操作を自律的に決定します。エージェント側で推論(判断)を行い、MCPサーバー側では安全にその指示を実行・結果提供する役割分担となります +。本提案の目的は、AIエージェントがユーザーの確認を最小限にしつつExcelの内容編集を行えるよう、ExStruct JSONベースの編集操作ツールを設計することです。これにより、フォームへの自動入力、数式エラーの修正、新規シート作成といった処理を安全かつ再現性高く実現できる基盤を構築します。 +MCP用ツール設計(OpenAPIスキーマベース) +AIエージェントが利用するMCPツールとして、新たにExStruct編集ツールを提案します。既存の抽出系ツール(exstruct_extract, exstruct_read_json_chunk, exstruct_validate_input +)に加えて、編集適用用のツールを設計します。ツール名・概要・パラメータは以下の通りです。 +exstruct_patch(Excel構造編集ツール) +説明: ExStructの出力JSON(構造化Excelデータ)に対する変更パッチを適用し、Excelファイルに編集を書き戻すためのツールです。エージェントが提案した修正内容(セル値更新、数式変更、新規シート追加など)をまとめて受け取り、安全にExcelデータを更新します。 +入力: +xlsx_path (string) – 編集対象とするExcelファイルのパス(読み込み元) +ops (array of object) – 適用する編集操作のリスト。各要素は以下のフィールドを持ちます: +op (string) – 操作種別(例: "set_value", "set_formula", "add_sheet") +sheet (string) – 対象シート名(新規シート作成時は作成する名前) +cell (string) – 対象セル座標(例えば "B3"。set_value/set_formulaで使用) +value (string|number|null) – 設定するセルの値(数値・文字列など。set_value用) +formula (string) – 設定するセルの数式(=から始まるExcel数式文字列。set_formula用) +(※図形テキスト等の編集にも拡張可能:例えば op: "set_shape_text", shape_id: 図形ID, text: 新テキスト など) +out_dir (string, optional) – 編集後のExcelを書き出すディレクトリ(省略時は元ファイルと同じディレクトリ) +out_name (string, optional) – 編集後Excelファイル名(省略時は元のファイル名に自動サフィックス付与) +on_conflict (string, optional) – 出力ファイル名衝突時の扱い("overwrite"/"skip"/"rename"から選択。未指定時はサーバー既定値を使用 +) +出力: +out_path (string) – 更新後のExcelファイルパス(保存先)。 +patch_diff (array of object) – 実際に適用された差分一覧(各変更の結果確認用情報。変更前後の値やシート名などを含む) +warnings (array of string, optional) – 適用時の警告メッセージ(必要に応じて) +設計の意図・特徴: +エージェントは提案する複数の編集操作をバッチ(opsリスト)で指定でき、ツール側で原子的に適用します。例えば、一度の呼び出しで複数セルの入力補完やシート追加と初期値入力を同時に行えます。xlsx_pathで指定した元ファイルは読み込み専用とし、out_pathへ結果を書き出すことで元データを保持しつつ変更版を別ファイルとして生成します(オプション指定がなければ自動リネーム保存)。このツール自体はExcelファイルを直接操作し、ExStructのJSON構造に対応する変更をExcelに反映します。Windows環境ではExcel COMまたはopenpyxlを用いてセル値・数式を書き込み、シートの追加等を行います(非Windows環境ではopenpyxlで対応)。処理完了後、どの項目がどのように変わったかをpatch_diffとして返し、エージェントはそれを用いて変更結果を検証したりユーザーに報告したりできます。 +ユースケース別の処理フロー +上記ツールを用いて、想定ユースケース毎にAIエージェントがどのように編集を行うか、そのフローを示します。各ケースで、エージェントはまずExStructで既存Excelを解析し、その出力JSONを基に編集箇所と内容を決定、exstruct_patchツールにより変更を適用します。入力にはユーザーの指示や補足データ、出力にはExStruct JSON形式の変更パッチ(差分)が含まれます。 +ユースケース1: 入力フォームの自動入力 +想定シナリオ: ユーザーがExcel内のフォームシートに対し、「氏名」「住所」などの項目に与えられたデータを自動入力してほしいと依頼したケースです。フォームのレイアウトとして、例えばA列に項目名、B列の対応セルが空欄になっていると想定します。 処理フロー: +Excel内容の取得 – エージェントはまずexstruct_extractツールでExcelをJSON変換し +、フォームシートの構造と既存値を把握します。例えば、「氏名」や「住所」という文字列がどのセルにあるかをJSONから検索します(ExStructではセル内容はrows配列内の各行オブジェクトに格納されています +)。 +入力箇所の特定 – 抽出JSONを解析し、フォーム項目名に対応する入力セル位置を特定します。具体的には、例えば「氏名」という文字列がセルA2にあり、その右隣B2が空欄であればB2が入力対象と判断されます。同様に「住所」がA3にありB3が空欄ならB3が対象になります。 +パッチの生成 – ユーザーから提供されたデータ(氏名=山田太郎、住所=東京都… 等)を、対応するセルに埋め込むパッチを構築します。例えば以下のような変更リストを用意します(擬似JSON形式): +"ops": [ +{"op": "set_value", "sheet": "フォーム", "cell": "B2", "value": "山田太郎"}, +{"op": "set_value", "sheet": "フォーム", "cell": "B3", "value": "東京都新宿区..."} +] +これによりフォームシート「フォーム」のB2セルに「山田太郎」、B3セルに住所テキストを入力する指示となります。 +パッチの適用 – エージェントは構築した変更パッチをexstruct_patchツールに渡し実行します。ツール内部では、指定Excelファイルの該当セルを書き換え、新しいExcelを保存します。出力には、例えばB2が空白から「山田太郎」に変わったこと、B3が空白から住所文字列に変わったことを示す差分情報が含まれます。 +結果の検証・利用 – エージェントは返されたpatch_diffを確認し、想定通り入力が行われたか検証します。問題なければユーザーに対し「フォームに指定のデータを入力しました」と回答し、必要に応じて更新後のExcelファイル(out_pathで示されるファイル)を提供します。変更内容は差分として記録されており、後から監査可能です。 +※内部的には、ExStruct JSONのrowsデータ(セル値マップ)でB2, B3に相当する箇所が更新されるイメージです +。ユーザー確認なしでも誤入力を防ぐため、エージェントは項目名と入力内容の対応を二重チェックし、フォーマット不整合(例えば数値フィールドに文字列を入れる等)が無いか検証します。 +ユースケース2: 数式エラーの修正 +想定シナリオ: Excelシート内のあるセルが#DIV/0!や#REF!等のエラーを示しており、ユーザーが「この数式エラーを修正してほしい」と依頼したケースです。例えば、シート「計算」で総計を求める数式が誤参照によりエラーになっている状況を考えます。 処理フロー: +エラーの検出 – エージェントはまずexstruct_extractでブック全体をJSON化し、formulas_mapなどを参照してエラーのある数式を探します。ExStructのformulas_mapには「数式文字列 → セル座標」の対応が含まれており +、これを使って該当セルの数式テキストを取得できます(例えばエラーが発生しているセルC10の数式が=SUM(A1:A9)だった等を把握)。また、rows内の該当セル値がエラー文字列("#DIV/0!" 等)になっていることも確認します。 +原因分析 – 抽出した数式文字列および周辺データからエラー原因を推測します。例えば、#REF!であれば参照範囲のシート名・セル範囲の誤り、#DIV/0!であれば除数が0になり得ることが原因かもしれません。エージェントは他のセル値もrowsデータから参照し、問題の数式が何をしようとしているか理解します。必要に応じてユーザーから追加情報(正しい参照先など)を引き出すことも考えられます。 +修正内容の決定 – 原因に応じて新しい数式を提案します。例えば、#REF!なら参照すべき正しいシート名に修正、#DIV/0!なら除数が0の場合に0を返すようなIF関数の追加、などです。ここではシート「計算」のC10が=SUM(Shee1!A:A)となっておりシート名のタイポで#REF!になっている例を考えます(正しくはSheet1)。修正数式は =SUM(Sheet1!A:A) に直す方針と決定します。 +パッチの生成 – 決定した修正を反映するパッチを作成します。上記例ではシート「計算」のセルC10の数式を書き換える操作となります。パッチ例: +"ops": [ +{"op": "set_formula", "sheet": "計算", "cell": "C10", "formula": "=SUM(Sheet1!A:A)"} +] +これによりC10セルの数式文字列を新しいものに置き換えます(ExStructのformulas_map上も旧数式キーから新数式キーへの更新が行われるイメージです)。必要に応じ、数式結果の再計算はExcel側で開く際に行われるため、ここでは値の更新は行いません(値はユーザーがExcelを開いて再計算した際に得られる)。 +パッチの適用 – エージェントはexstruct_patchを使い上記パッチを適用します。Excelファイル上でセルC10の数式が書き換わり、新しいExcelファイルが保存されます。patch_diffには「C10の数式: =SUM(Shee1!A:A) から =SUM(Sheet1!A:A) に変更した」旨の記録が残ります。 +結果の検証 – エージェントは返された差分を確認し、確かに意図した変更が行われたことを確認します。可能であればexstruct_extractで再度JSONを取得し、エラー表示が消えたか(ExStruct上はセルC10の値が今度はエラー文字列以外になっているか)をチェックしても良いでしょう。修正が正しければ、ユーザーに対し「数式の参照ミスを修正しました。これでエラーは解消するはずです」と伝え、更新後のExcelを提供します。 +※ExStructではformulas_mapにより複数セルで同一数式が使われている場合まとめられていますが +、個別セルの修正では該当エントリのみを差し替えます(内部実装では旧数式キーの該当セル座標を削除し、新数式キーにセル座標を追加する処理となります)。エージェントはこのような低レベル実装詳細を意識せず高水準な操作指定(「セルC10の数式を書き換え」)を行い、ツール側で正確な差分反映を行います。 +ユースケース3: ゼロからのシート作成 +想定シナリオ: 空の新規シートを追加し、そこに必要な表や値を自動生成してほしいケースです。ユーザーが「売上データを集計した新しいシートを作って」と依頼するといった状況を考えます。 処理フロー: +内容と構成のプランニング – エージェントはユーザーの要求を解析し、新シートに何を作るかを決めます。例えば「売上集計」シートを作成し、既存の「売上明細」シートから月別売上合計を計算してまとめる、など具体的なプランを立てます。この段階で必要ならexstruct_extractやread_json_chunkで元データ(売上明細シートの内容)を取得し、計算に使います。 +シート名の決定と競合確認 – 新規シート名を決めます(例: 「売上集計」)。既に同名のシートが存在しないか、ExStruct JSONのsheetsマップを確認して競合を避けます +(存在する場合は自動で別名を付けるかユーザーに確認します)。 +初期データ/書式の生成 – シートに配置するデータやヘッダ行、計算式などを決定します。例えば1行目にヘッダ「月」「売上合計」を入れ、2行目以降に1月~12月の売上合計値を計算して入れる、というようにレイアウトと内容を準備します。計算式もここで用意し(例えばB2に=SUM('売上明細'!B2:B31)など)、論理チェックを行います。 +パッチの生成 – 新シート追加とセル内容設定を含む変更パッチを構築します。例えば以下のようになります: +"ops": [ +{"op": "add_sheet", "sheet": "売上集計"}, +{"op": "set_value", "sheet": "売上集計", "cell": "A1", "value": "月"}, +{"op": "set_value", "sheet": "売上集計", "cell": "B1", "value": "売上合計"}, +{"op": "set_value", "sheet": "売上集計", "cell": "A2", "value": "1月"}, +{"op": "set_formula", "sheet": "売上集計", "cell": "B2", "formula": "=SUM('売上明細'!B2:B31)"} +// 以下 A3に"2月", B3にSUM(...), ... と続く +] +最初のadd_sheetによりブックに名前「売上集計」のシートを追加し、続くset_valueおよびset_formulaでそのシート上のセルを順次設定していきます(ヘッダ行と1月の集計式のみ例示)。パッチ内の操作順序は考慮され、add_sheetが他の操作より先に適用されることで、新シート上に値を書き込む処理が可能になります。 +パッチの適用 – exstruct_patchを呼び出し変更を適用します。これによって元Excelに「売上集計」シートが追加され、指定のセルに値・数式が書き込まれた新しいExcelファイルが生成されます。patch_diffには新規シート作成とセル値入力が一覧化されます。例えば「新規シート '売上集計' を追加」「'売上集計'!A1 に '月' を設定」「'売上集計'!B2 に数式 '=SUM('売上明細'!B2:B31)' を設定」などと記録されます。 +結果確認 – エージェントは差分ログや再抽出結果を検証し、シート追加とデータ配置が指示通りになっていることを確認します。必要ならば新シートの内容をexstruct_read_json_chunkで読み出し、意図した値・式になっているかチェックします。問題なければユーザーに「新しい集計シート '売上集計' を追加しました」と伝え、ファイルパスを共有します。 +この処理により、ゼロからシートを生成する操作も自動化できます。opsリストにまとめて記述できるため、一括適用による一貫性確保(シート追加と内容設定の一体化)や、過程の中間状態がユーザー確認なしでも整合するメリットがあります。 +編集対象フィールドと変更内容の対応 +各ユースケースで操作するExStruct内部フィールドを整理します。提案ツールはExStruct出力JSONを直接修正・拡張する形でExcel編集を行います。 +セル値 (Cells) – フォーム自動入力や値修正では、JSON構造内の各シートのrows配列にある該当セル値を更新します +。例えば上記ケース1ではシート「フォーム」の該当行オブジェクト内のcマップでキー「B」に対応する値を書き換える操作に相当します。新規値はJSON上もExcel上も文字列または数値として保存されます。 +数式 (Formulas) – 数式の変更では、Excel上はセルの数式文字列を書き換えます。ExStruct JSON上はformulas_mapに格納されたエントリを更新することに対応します +(内部的には旧数式のエントリから該当セルを削除し、新数式文字列のキーにセルを紐付ける操作となります)。またExcelファイル上は新しい数式が設定されます。注: ExStructの抽出JSONはセルの計算結果値も保持しますが、再計算が必要な場合この値は古いままの場合があります。エージェントは必要ならユーザーにExcel上で再計算(F9押下等)するよう注意喚起します。 +シート (Sheets) – 新規シートの追加では、JSONのworkbook["sheets"]オブジェクトに新しいキー(シート名)を追加し、その値として空のSheetData構造を作る操作に相当します +。内部ではExcelファイルに対してシートを追加し、ExStructエンジンで扱える構造(空のrows配列等を持つ)を初期化します。その後のセル入力はこの新規シート内のデータ構造に対する更新です。既存シート名の変更や削除も将来的には対応可能ですが、本提案範囲では扱いません。 +図形・その他オブジェクト (Shapes etc) – ユースケースには直接登場しませんでしたが、ExStruct JSONには図形やスマートアート、チャート等の情報も含まれます +。テキストボックス型の図形に入力された文字列を編集する、といった操作も考えられます。この場合、該当シートのshapes配列から対象オブジェクト(例えばidで特定)を見つけ、そのtextフィールドを更新するパッチを用意します。提案ツールではop: "set_shape_text"等の拡張的な操作もサポート可能であり、他の操作と同様に差分として記録・適用します。チャートやSmartArtに対する大規模な構造変更は複雑ですが、例えばチャートタイトルのテキスト差し替え程度であれば同様のアプローチで実現できます。 +安全性とトレーサビリティ +AIエージェントによる自律的なExcel編集においては、誤操作の防止と変更履歴の追跡が極めて重要です。本設計では以下の工夫により安全性とトレーサビリティを確保します。 +ルートディレクトリ固定とパス検証: MCPサーバー起動時に--rootオプションで許可ディレクトリを定め、エージェントはその配下のファイルしか操作できません +。ツール実行時も内部でパス正規化とdeny_globチェックを行い、不正なパスや想定外のファイルアクセスをブロックします +。これにより、エージェントが誤って機密ファイルを編集したり、悪意ある指示で外部に影響を及ぼすリスクを低減します。 +オープンAPIスキーマによる入力制約: 提案ツールのパラメータはOpenAPIスキーマに基づき厳格に定義されています。例えばops配列内の各フィールド型・必須/省略可能要素は事前に決められており、エージェントのリクエストは自動検証されます。スキーマ不一致の入力(存在しないシート名や無効なセル番地形式など)は即座にエラーとなり、実ファイルに手を加える前に検出されます。これにより、エージェントのバグやLLMの勘違いによる不正確な操作を未然に防ぎます。 +差分適用の原子性: 一連の編集opsはすべて成功した場合にのみコミットされます。途中で無効な参照(例: 存在しないセル)や競合が判明した場合、ツールはエラーを返しExcelは変更されません。これにより、部分的変更でデータ不整合が生じることを防ぎます。 +出力ファイルの競合回避: 既定ではexstruct_patchは元ファイルを上書きせず新規ファイルとして保存します。--on-conflict設定により同名ファイルがある場合はリネームやスキップも可能です +。安全策としてデフォルトはrename(連番等で新ファイル名生成)とし、うっかり既存ファイルを消してしまう事故を防ぎます。ユーザーが明示的に許可した場合のみ上書きモードを使う運用を推奨します。 +変更履歴のログ: exstruct_patchは適用内容をpatch_diffとして詳細に返します。各エントリには変更対象(シート名・セル座標など)、変更前の値(可能な場合)と変更後の値が含まれます。これをサーバー側でログ蓄積することで、後から「どのセルをどう変えたか」を追跡できます。ユーザーは履歴をレビューしたり、不備が見つかった場合に該当変更のみをUndo(取り消し)することも可能です。Undoは、記録された変更前値を用いて逆パッチを適用すれば実現できます。例えば、「B2を空白から山田太郎に変更」という記録があれば、その逆操作「B2を山田太郎から空白に」がUndoに相当します。エージェントは自律動作中でも必要に応じて過去ログを参照し、二重入力や再変更による矛盾を避けます。 +ユーザー確認ポイントの最小化と確保: 基本的にエージェントは上記仕組みにより自律的に編集を完了できますが、リスクの高い操作(大量のセル削除や大幅なレイアウト変更など)は事前にユーザー確認を要求することも検討できます。本提案ではフォーム入力や局所的な計算修正といったスコープ限定の変更を対象としており、通常は自動承認で進めます。一方、新規シート追加のようにユーザーの意図を汲み取る必要があるケースでは、エージェントが推測に基づく内容(例: 集計方法)を実装する前に「この方針で進めますがよろしいですか?」と質問することも可能です。ただしこれはプロンプト設計レベルで制御し、MCPツール自体はユーザー確認のフローを持ちません。必要な確認はエージェントが対話を通じて行い、確定したら一括適用する方針です。 +例外とエラー処理: ツール実行時に発生し得るエラー(例えばExcelファイル破損、シート名の競合、権限不足など)はツールからエージェントへ明示的に返されます(warningsやエラーメッセージとして)。エージェントはこれを解釈し、ユーザーへの報告やリトライ(別名で再作成する等)を行います。こうしたフェイルセーフ設計により、意図しないSilent failure(失敗に気付かない状態)を防ぎます。 +以上の対策により、AIエージェントによるExcel編集操作は、閉じた安全な環境内でトレース可能な形で実行されます。ログとパッチによる再現性確保により、後からでも変更を再適用したり原因分析したりが容易です。ユーザーは安心してエージェントにExcel編集を任せることができ、必要に応じて結果だけを確認するという使い方が可能になります。 +プロンプト例(LLMへの指示) +最後に、想定ユースケースに対してユーザーがAIエージェントに与える指示(プロンプト)の代表例を示します。エージェントはこれらの指示を受け取り、内部でExStruct MCPツールを駆使して前述の編集フローを実行します。 +フォーム自動入力の例: 「添付のExcelフォームに、社員情報を自動入力してください。氏名は山田太郎、住所は東京都新宿区西新宿2-8-1、電話番号は03-1234-5678です。」 +数式エラー修正の例: 「Excelの『計算』シートでセルC10に#REF!エラーが出ています。正しい参照範囲は同ブックの『Sheet1』シートです。このエラーを修正してください。」 +新規シート作成の例: 「現在のExcelブックに、新しく『売上集計』シートを追加してください。既存の『売上明細』シートから月ごとの売上合計を計算し、月別に一覧でまとめて配置してください。」 +これらのプロンプトに対し、エージェントは適切にExStructの抽出結果を分析し、設計したツールを用いて安全に編集を行います。例えばフォーム入力の依頼では、自動で対応セルを見つけ出し値を埋め、ユーザーに完成したフォームを提示します。数式修正では、誤った参照を正しいものに直し、新規シート作成では要求通りの集計シートを構築します。全ての操作はExStruct JSON中間表現を介して行われるため、AIエージェントはExcelのGUI操作をすることなく構造的かつ再現性のある編集を実現できます。ユーザーは最終的に編集後のExcelファイルと、必要なら変更概要の説明を受け取り、従来手作業では困難だった高度な処理も自動化された形で享受できます。 diff --git a/docs/agents/FEATURE_SPEC.md b/docs/agents/FEATURE_SPEC.md index c92baed..e26d105 100644 --- a/docs/agents/FEATURE_SPEC.md +++ b/docs/agents/FEATURE_SPEC.md @@ -4,97 +4,3 @@ --- -## MCPサーバー機能追加 - -### 目的 - -- MCP クライアント(Codex / Claude / VS Code Copilot / Gemini CLI 等)から ExStruct を「ツール」として安全に呼び出せるようにする -- 推論はエージェント側で行い、MCP は制御面(実行・結果参照)に徹する - -### スコープ(MVP) - -- stdio トランスポートの MCP サーバー -- ツール: `exstruct_extract` -- 抽出結果は **必ずファイル出力**(MCP 応答はパス + 軽いメタ情報) -- 安全なパス制約(allowlist / deny glob) - -### 前提・制約 - -- 1MB 程度の Excel を想定 -- 処理時間は長くなっても高品質重視 -- Windows 以外は COM なしの簡易読み取り(ライブラリのスタンスに準拠) - -### 出力 JSON の仕様 - -- `mode` で出力粒度を選択: `light` / `standard` / `verbose` -- 互換方針: 追加は OK、破壊的変更は NG - -#### `light` - -- 軽量メタデータ中心(シート名、件数、主要範囲など) -- 大きなセル本文や詳細構造は含めない - -#### `standard` - -- 通常運用向けの基本情報 -- セル情報は要約・圧縮前提 - -#### `verbose` - -- 詳細な構造情報を含む -- 大容量になりやすいため、ファイル出力+チャンク取得前提 - -### MCP ツール仕様(案) - -#### `exstruct_extract` - -- 入力: `xlsx_path`, `mode`, `format`, `out_dir?`, `out_name?`, `options?` -- 出力: `out_path`, `workbook_meta`, `warnings`, `engine` -- 実装: 内部 API を優先、フォールバックで CLI サブプロセス - -#### `exstruct_read_json_chunk`(実用化フェーズ) - -- 入力: `out_path`, `sheet?`, `max_bytes?`, `filter?`, `cursor?` -- 出力: `chunk`, `next_cursor?` -- 方針: 返却サイズを抑制し、段階的に取得できること - -#### `exstruct_validate_input`(実用化フェーズ) - -- 入力: `xlsx_path` -- 出力: `is_readable`, `warnings`, `errors` - -### サーバー設計 - -- stdio 優先 -- ログは stderr / ファイル(stdio を汚さない) -- `--root` によりアクセス範囲を固定 -- `--deny-glob` により防御的に除外 -- `--on-conflict` で出力衝突方針を指定(overwrite / skip / rename) - -### ディレクトリ構成(案) - -``` -src/exstruct/ - mcp/ - __init__.py - server.py # MCP server entrypoint (stdio) - tools.py # tool definitions + handlers - io.py # path validation, safe read/write - extract_runner.py # internal API call or subprocess fallback - chunk_reader.py # JSON partial read / pointer / sheet filters -``` - ---- - -## 今後のオプション検討メモ - -- 表検知スコアリングの閾値を CLI/環境変数で調整可能にする -- 出力モード(light/standard/verbose)に応じてテーブル候補数を制限するオプション - ---- - -## 実装方針 - -- 小さなステップごとにテスト追加、または既存フィクスチャで手動確認 -- 短い関数・責務分割でスコアリング調整をしやすくする -- 外部公開前なので、破壊的変更はコメントや仕様に明示して段階的に移行する diff --git a/docs/agents/TASKS.md b/docs/agents/TASKS.md index 497ec31..f2b0652 100644 --- a/docs/agents/TASKS.md +++ b/docs/agents/TASKS.md @@ -1,31 +1,3 @@ # Task List 未完了 [ ], 完了 [x] - -## MCPサーバー(MVP) - -- [x] 仕様反映: `docs/agents/FEATURE_SPEC.md` を更新 -- [x] 依存追加: `pyproject.toml` に `exstruct[mcp]` の extras を追加 -- [x] エントリポイント: `exstruct-mcp = exstruct.mcp.server:main` を定義 -- [x] MCP 基盤: `src/exstruct/mcp/server.py` を追加(stdio サーバー起動) -- [x] ツール定義: `src/exstruct/mcp/tools.py` に `exstruct_extract` を実装 -- [x] パス制約: `src/exstruct/mcp/io.py` に allowlist / deny glob を実装 -- [x] 抽出実行: `src/exstruct/mcp/extract_runner.py` に内部 API 優先の実行層を実装 -- [x] 出力モデル: Pydantic で入出力モデルを定義(mypy strict / Ruff 遵守) -- [x] ログ: stderr / ファイル出力の設定を追加 -- [x] ドキュメント: README または docs に起動例(`exstruct-mcp --root ...`)を追記 - -## MCPサーバー(実用化) - -- [x] `exstruct_read_json_chunk` を追加(大容量 JSON 対応) -- [x] `exstruct_validate_input` を追加(事前検証) -- [x] `--on-conflict` の出力衝突ポリシー実装 -- [x] Windows/非Windows の読み取り差分を明文化 -- [x] 最低限のテスト追加(パス制約 / 入出力モデル / 例外) - -## PR #47 レビュー対応 - -- [x] cells.py の列幅縮小ヒューリスティックを再検討(遅い行に境界があるケースで早期縮小しない方針に修正) -- [x] 上記修正に対応するテストを追加(遅い行・右端に表があるケースを openpyxl で検証) -- [x] Codecov 指摘の不足分を埋めるテスト追加(mcp: chunk_reader/extract_runner/server/tools/validate_input/io、core/cells) -- [x] CodeRabbit: Docstring coverage 80% を満たすよう不足分の docstring を追加 From 0447eda5f3b6909b432587e56dc451a79b9bc754 Mon Sep 17 00:00:00 2001 From: harumiWeb Date: Thu, 5 Feb 2026 21:55:34 +0900 Subject: [PATCH 02/25] Implement patch runner for Excel workbooks with support for set_value, set_formula, and add_sheet operations; add comprehensive tests for patch functionality and validation. --- docs/agents/FEATURE_SPEC.md | 155 +++++++- docs/agents/TASKS.md | 9 + src/exstruct/mcp/__init__.py | 20 + src/exstruct/mcp/patch_runner.py | 604 +++++++++++++++++++++++++++++++ src/exstruct/mcp/server.py | 43 +++ src/exstruct/mcp/tools.py | 68 ++++ tests/mcp/test_patch_runner.py | 178 +++++++++ tests/mcp/test_server.py | 19 + tests/mcp/test_tool_models.py | 12 +- tests/mcp/test_tools_handlers.py | 24 ++ uv.lock | 305 ++++++++++++++++ 11 files changed, 1435 insertions(+), 2 deletions(-) create mode 100644 src/exstruct/mcp/patch_runner.py create mode 100644 tests/mcp/test_patch_runner.py diff --git a/docs/agents/FEATURE_SPEC.md b/docs/agents/FEATURE_SPEC.md index e26d105..9e5ec14 100644 --- a/docs/agents/FEATURE_SPEC.md +++ b/docs/agents/FEATURE_SPEC.md @@ -1,6 +1,159 @@ # Feature Spec for AI Agent (Phase-by-Phase) -本ドキュメントは AI エージェント向けに、段階的に実装を進めるための仕様メモです。 +本ドキュメントは、ExStruct MCP に新規追加する編集ツール `exstruct_patch` の MVP 仕様を定義します。 --- +## 1. 目的・スコープ + +- **MVP対象**: セル値更新 / 数式更新 / 新規シート追加 +- **非対象**: 図形・チャート編集、大規模レイアウト変更 +- **再計算**: 行わない(Excel側の再計算に委ねる) + +--- + +## 2. ツールI/F定義(OpenAPI / Pydantic想定) + +### Tool名 +- `exstruct_patch` + +### Input +- `xlsx_path: str` +- `ops: list[PatchOp]` +- `out_dir: str | None` +- `out_name: str | None` +- `on_conflict: "overwrite" | "skip" | "rename" | None` + +### Output +- `out_path: str` +- `patch_diff: list[PatchDiffItem]` +- `warnings: list[str]` + +--- + +## 3. 操作種別(PatchOp) + +### set_value +- `op: "set_value"` +- `sheet: str` +- `cell: str` (A1形式) +- `value: str | int | float | None`(nullでクリア) + +### set_formula +- `op: "set_formula"` +- `sheet: str` +- `cell: str` (A1形式) +- `formula: str`(必ず `=` から開始) + +### add_sheet +- `op: "add_sheet"` +- `sheet: str`(新規シート名) + +--- + +## 4. バリデーション規則 + +- `xlsx_path` + - 存在するファイルであること + - 許可パス内であること(PathPolicy) + - 拡張子は `.xlsx` / `.xlsm` / `.xls` +- `sheet` + - 空文字禁止 + - `add_sheet` は既存名と重複禁止 + - `set_value` / `set_formula` は存在シートのみ +- `cell` + - A1形式を必須(例: `B3`) +- `set_value` + - `value` が `=` から始まる文字列は拒否 +- `set_formula` + - `formula` は必ず `=` から開始 + +--- + +## 5. 実行セマンティクス + +- opsは **順序通り** に評価・適用 +- **全て成功した場合のみ保存**(原子性) +- 失敗時は **Excelを変更せずエラー** を返す +- `add_sheet` 実行後にのみ、そのシートへの `set_value` / `set_formula` を許可 + +--- + +## 6. 出力差分(patch_diff) + +最低限の構造: + +- `op: "set_value" | "set_formula" | "add_sheet"` +- `sheet: str` +- `cell: str | null` +- `before: PatchValue | null` +- `after: PatchValue | null` +- `status: "applied" | "skipped"` + +`PatchValue`: + +- `kind: "value" | "formula" | "sheet"` +- `value: str | int | float | null` + +--- + +## 7. バックエンド方針 + +- **Windows + COM利用可能**: COM優先 +- **それ以外**: openpyxl +- openpyxl使用時は、図形/チャート等の保持制限を `warnings` で通知 + +--- + +## 8. 競合ポリシー + +- `on_conflict` 未指定時は **rename** を既定値とする +- `skip` の場合: + - `patch_diff` は空配列 + - `warnings` にスキップ理由を記載 + +出力名の既定: +- `out_name` 未指定時は `"{stem}_patched{suffix}"` + +--- + +## 9. エラー処理 + +- パス違反 / シート不在 / セル不正 / 数式不正: `ValueError` +- 読み込み不能: `FileNotFoundError` / `OSError` +- バックエンド例外: `RuntimeError` + +--- + +## 10. 例 + +### set_value +```json +{ + "xlsx_path": "book.xlsx", + "ops": [ + {"op": "set_value", "sheet": "フォーム", "cell": "B2", "value": "山田太郎"} + ] +} +``` + +### set_formula +```json +{ + "xlsx_path": "book.xlsx", + "ops": [ + {"op": "set_formula", "sheet": "計算", "cell": "C10", "formula": "=SUM(Sheet1!A:A)"} + ] +} +``` + +### add_sheet +```json +{ + "xlsx_path": "book.xlsx", + "ops": [ + {"op": "add_sheet", "sheet": "売上集計"}, + {"op": "set_value", "sheet": "売上集計", "cell": "A1", "value": "月"} + ] +} +``` diff --git a/docs/agents/TASKS.md b/docs/agents/TASKS.md index f2b0652..9fb7b6e 100644 --- a/docs/agents/TASKS.md +++ b/docs/agents/TASKS.md @@ -1,3 +1,12 @@ # Task List 未完了 [ ], 完了 [x] + +- [x] `FEATURE_SPEC.md` に exstruct_patch の詳細仕様(I/F、検証、差分、エラー)を記載 +- [x] `mcp/patch_runner.py` を新設し、`PatchRequest/Result` を定義 +- [x] `mcp/tools.py` に `PatchToolInput/Output` と `run_patch_tool` を追加 +- [x] `mcp/server.py` に `exstruct_patch` 登録を追加 +- [x] パス制御・競合ポリシー・保存ロジックを実装 +- [x] openpyxlベースの適用処理を実装 +- [x] COM利用時の分岐(可能なら)とwarning出力 +- [x] ユニットテスト(非COM)を追加 diff --git a/src/exstruct/mcp/__init__.py b/src/exstruct/mcp/__init__.py index bbdfd8c..7829f69 100644 --- a/src/exstruct/mcp/__init__.py +++ b/src/exstruct/mcp/__init__.py @@ -16,14 +16,25 @@ run_extract, ) from .io import PathPolicy +from .patch_runner import ( + PatchDiffItem, + PatchOp, + PatchRequest, + PatchResult, + PatchValue, + run_patch, +) from .tools import ( ExtractToolInput, ExtractToolOutput, + PatchToolInput, + PatchToolOutput, ReadJsonChunkToolInput, ReadJsonChunkToolOutput, ValidateInputToolInput, ValidateInputToolOutput, run_extract_tool, + run_patch_tool, run_read_json_chunk_tool, run_validate_input_tool, ) @@ -39,6 +50,13 @@ "ExtractOptions", "ExtractToolInput", "ExtractToolOutput", + "PatchDiffItem", + "PatchOp", + "PatchRequest", + "PatchResult", + "PatchToolInput", + "PatchToolOutput", + "PatchValue", "PathPolicy", "ReadJsonChunkFilter", "ReadJsonChunkRequest", @@ -54,6 +72,8 @@ "validate_input", "run_extract", "run_extract_tool", + "run_patch", + "run_patch_tool", "run_read_json_chunk_tool", "run_validate_input_tool", ] diff --git a/src/exstruct/mcp/patch_runner.py b/src/exstruct/mcp/patch_runner.py new file mode 100644 index 0000000..d6f1c57 --- /dev/null +++ b/src/exstruct/mcp/patch_runner.py @@ -0,0 +1,604 @@ +from __future__ import annotations + +from collections.abc import Iterator +from contextlib import contextmanager +from pathlib import Path +import re +from typing import Literal, Protocol, runtime_checkable + +from pydantic import BaseModel, Field, field_validator, model_validator +import xlwings as xw + +from exstruct.cli.availability import get_com_availability + +from .extract_runner import OnConflictPolicy +from .io import PathPolicy + +PatchOpType = Literal["set_value", "set_formula", "add_sheet"] +PatchStatus = Literal["applied", "skipped"] +PatchValueKind = Literal["value", "formula", "sheet"] + +_ALLOWED_EXTENSIONS = {".xlsx", ".xlsm", ".xls"} +_A1_PATTERN = re.compile(r"^[A-Za-z]{1,3}[1-9][0-9]*$") + + +@runtime_checkable +class OpenpyxlCellProtocol(Protocol): + """Protocol for openpyxl cell access used by patch runner.""" + + value: str | int | float | None + data_type: str | None + + +@runtime_checkable +class OpenpyxlWorksheetProtocol(Protocol): + """Protocol for openpyxl worksheet access used by patch runner.""" + + def __getitem__(self, key: str) -> OpenpyxlCellProtocol: ... + + +@runtime_checkable +class OpenpyxlWorkbookProtocol(Protocol): + """Protocol for openpyxl workbook access used by patch runner.""" + + sheetnames: list[str] + + def __getitem__(self, key: str) -> OpenpyxlWorksheetProtocol: ... + + def create_sheet(self, title: str) -> OpenpyxlWorksheetProtocol: ... + + def save(self, filename: str | Path) -> None: ... + + def close(self) -> None: ... + + +@runtime_checkable +class XlwingsRangeProtocol(Protocol): + """Protocol for xlwings range access used by patch runner.""" + + value: str | int | float | None + formula: str | None + + +@runtime_checkable +class XlwingsSheetProtocol(Protocol): + """Protocol for xlwings sheet access used by patch runner.""" + + name: str + + def range(self, cell: str) -> XlwingsRangeProtocol: ... + + +@runtime_checkable +class XlwingsSheetsProtocol(Protocol): + """Protocol for xlwings sheets collection.""" + + def __iter__(self) -> Iterator[XlwingsSheetProtocol]: ... + + def __len__(self) -> int: ... + + def __getitem__(self, index: int) -> XlwingsSheetProtocol: ... + + def add( + self, name: str, after: XlwingsSheetProtocol | None = None + ) -> XlwingsSheetProtocol: ... + + +@runtime_checkable +class XlwingsWorkbookProtocol(Protocol): + """Protocol for xlwings workbook access used by patch runner.""" + + sheets: XlwingsSheetsProtocol + + def save(self, filename: str) -> None: ... + + def close(self) -> None: ... + + +class PatchOp(BaseModel): + """Single patch operation for an Excel workbook.""" + + op: PatchOpType + sheet: str + cell: str | None = None + value: str | int | float | None = None + formula: str | None = None + + @field_validator("sheet") + @classmethod + def _validate_sheet(cls, value: str) -> str: + if not value.strip(): + raise ValueError("sheet must not be empty.") + return value + + @field_validator("cell") + @classmethod + def _validate_cell(cls, value: str | None) -> str | None: + if value is None: + return None + candidate = value.strip() + if not _A1_PATTERN.match(candidate): + raise ValueError(f"Invalid cell reference: {value}") + return candidate.upper() + + @model_validator(mode="after") + def _validate_op(self) -> PatchOp: + if self.op == "add_sheet": + _validate_add_sheet(self) + return self + _validate_cell_required(self) + if self.op == "set_value": + _validate_set_value(self) + return self + if self.op == "set_formula": + _validate_set_formula(self) + return self + return self + + +def _validate_add_sheet(op: PatchOp) -> None: + """Validate add_sheet operation.""" + if op.cell is not None: + raise ValueError("add_sheet does not accept cell.") + if op.value is not None: + raise ValueError("add_sheet does not accept value.") + if op.formula is not None: + raise ValueError("add_sheet does not accept formula.") + + +def _validate_cell_required(op: PatchOp) -> None: + """Validate that the operation has a cell value.""" + if op.cell is None: + raise ValueError(f"{op.op} requires cell.") + + +def _validate_set_value(op: PatchOp) -> None: + """Validate set_value operation.""" + if op.formula is not None: + raise ValueError("set_value does not accept formula.") + if isinstance(op.value, str) and op.value.startswith("="): + raise ValueError("set_value rejects values starting with '='.") + + +def _validate_set_formula(op: PatchOp) -> None: + """Validate set_formula operation.""" + if op.value is not None: + raise ValueError("set_formula does not accept value.") + if op.formula is None: + raise ValueError("set_formula requires formula.") + if not op.formula.startswith("="): + raise ValueError("set_formula requires formula starting with '='.") + + +class PatchValue(BaseModel): + """Normalized before/after value in patch diff.""" + + kind: PatchValueKind + value: str | int | float | None + + +class PatchDiffItem(BaseModel): + """Applied change record for patch operations.""" + + op: PatchOpType + sheet: str + cell: str | None = None + before: PatchValue | None = None + after: PatchValue | None = None + status: PatchStatus = "applied" + + +class PatchRequest(BaseModel): + """Input model for ExStruct MCP patch.""" + + xlsx_path: Path + ops: list[PatchOp] + out_dir: Path | None = None + out_name: str | None = None + on_conflict: OnConflictPolicy = "rename" + + +class PatchResult(BaseModel): + """Output model for ExStruct MCP patch.""" + + out_path: str + patch_diff: list[PatchDiffItem] = Field(default_factory=list) + warnings: list[str] = Field(default_factory=list) + + +def run_patch( + request: PatchRequest, *, policy: PathPolicy | None = None +) -> PatchResult: + """Run a patch operation and write the updated workbook. + + Args: + request: Patch request payload. + policy: Optional path policy for access control. + + Returns: + Patch result with output path and diff. + + Raises: + FileNotFoundError: If the input file does not exist. + ValueError: If validation fails or the path violates policy. + RuntimeError: If a backend operation fails. + """ + resolved_input = _resolve_input_path(request.xlsx_path, policy=policy) + _ensure_supported_extension(resolved_input) + output_path = _resolve_output_path( + resolved_input, + out_dir=request.out_dir, + out_name=request.out_name, + policy=policy, + ) + output_path, warning, skipped = _apply_conflict_policy( + output_path, request.on_conflict + ) + warnings: list[str] = [] + if warning: + warnings.append(warning) + if skipped: + return PatchResult(out_path=str(output_path), patch_diff=[], warnings=warnings) + + com = get_com_availability() + if com.available: + try: + diff = _apply_ops_xlwings( + resolved_input, + output_path, + request.ops, + ) + return PatchResult( + out_path=str(output_path), patch_diff=diff, warnings=warnings + ) + except ValueError: + raise + except Exception as exc: + fallback = _maybe_fallback_openpyxl( + resolved_input, + output_path, + request.ops, + warnings, + reason=f"COM patch failed; falling back to openpyxl. ({exc!r})", + ) + if fallback is not None: + return fallback + raise RuntimeError(f"COM patch failed: {exc}") from exc + + if com.reason: + warnings.append(f"COM unavailable: {com.reason}") + return _apply_with_openpyxl(resolved_input, output_path, request.ops, warnings) + + +def _apply_with_openpyxl( + input_path: Path, + output_path: Path, + ops: list[PatchOp], + warnings: list[str], +) -> PatchResult: + """Apply patch operations using openpyxl.""" + try: + diff = _apply_ops_openpyxl(input_path, output_path, ops) + except ValueError: + raise + except FileNotFoundError: + raise + except OSError: + raise + except Exception as exc: + raise RuntimeError(f"openpyxl patch failed: {exc}") from exc + + warnings.append("openpyxl editing may drop shapes/charts or unsupported elements.") + return PatchResult(out_path=str(output_path), patch_diff=diff, warnings=warnings) + + +def _maybe_fallback_openpyxl( + input_path: Path, + output_path: Path, + ops: list[PatchOp], + warnings: list[str], + *, + reason: str, +) -> PatchResult | None: + """Attempt openpyxl fallback after COM failure.""" + if input_path.suffix.lower() == ".xls": + warnings.append(reason) + return None + warnings.append(reason) + return _apply_with_openpyxl(input_path, output_path, ops, warnings) + + +def _resolve_input_path(path: Path, *, policy: PathPolicy | None) -> Path: + """Resolve and validate the input path.""" + resolved = policy.ensure_allowed(path) if policy else path.resolve() + if not resolved.exists(): + raise FileNotFoundError(f"Input file not found: {resolved}") + if not resolved.is_file(): + raise ValueError(f"Input path is not a file: {resolved}") + return resolved + + +def _ensure_supported_extension(path: Path) -> None: + """Validate that the input file extension is supported.""" + if path.suffix.lower() not in _ALLOWED_EXTENSIONS: + raise ValueError(f"Unsupported file extension: {path.suffix}") + + +def _resolve_output_path( + input_path: Path, + *, + out_dir: Path | None, + out_name: str | None, + policy: PathPolicy | None, +) -> Path: + """Build and validate the output path.""" + target_dir = out_dir or input_path.parent + target_dir = policy.ensure_allowed(target_dir) if policy else target_dir.resolve() + name = _normalize_output_name(input_path, out_name) + output_path = (target_dir / name).resolve() + if policy is not None: + output_path = policy.ensure_allowed(output_path) + return output_path + + +def _normalize_output_name(input_path: Path, out_name: str | None) -> str: + """Normalize output filename with a safe suffix.""" + if out_name: + candidate = Path(out_name) + return ( + candidate.name + if candidate.suffix + else f"{candidate.name}{input_path.suffix}" + ) + return f"{input_path.stem}_patched{input_path.suffix}" + + +def _apply_conflict_policy( + output_path: Path, on_conflict: OnConflictPolicy +) -> tuple[Path, str | None, bool]: + """Apply output conflict policy to a resolved output path.""" + if not output_path.exists(): + return output_path, None, False + if on_conflict == "skip": + return ( + output_path, + f"Output exists; skipping write: {output_path.name}", + True, + ) + if on_conflict == "rename": + renamed = _next_available_path(output_path) + return ( + renamed, + f"Output exists; renamed to: {renamed.name}", + False, + ) + return output_path, None, False + + +def _next_available_path(path: Path) -> Path: + """Return the next available path by appending a numeric suffix.""" + if not path.exists(): + return path + stem = path.stem + suffix = path.suffix + for idx in range(1, 10_000): + candidate = path.with_name(f"{stem}_{idx}{suffix}") + if not candidate.exists(): + return candidate + raise RuntimeError(f"Failed to resolve unique path for {path}") + + +def _apply_ops_openpyxl( + input_path: Path, output_path: Path, ops: list[PatchOp] +) -> list[PatchDiffItem]: + """Apply operations using openpyxl.""" + try: + from openpyxl import load_workbook + except ImportError as exc: + raise RuntimeError(f"openpyxl is not available: {exc}") from exc + + if input_path.suffix.lower() == ".xls": + raise ValueError("openpyxl cannot edit .xls files.") + + workbook = load_workbook(input_path) + try: + diff = _apply_ops_to_openpyxl_workbook(workbook, ops) + workbook.save(output_path) + finally: + workbook.close() + return diff + + +def _apply_ops_to_openpyxl_workbook( + workbook: OpenpyxlWorkbookProtocol, ops: list[PatchOp] +) -> list[PatchDiffItem]: + """Apply ops to an openpyxl workbook instance.""" + sheets = _openpyxl_sheet_map(workbook) + diff: list[PatchDiffItem] = [] + for op in ops: + diff.append(_apply_openpyxl_op(workbook, sheets, op)) + return diff + + +def _openpyxl_sheet_map( + workbook: OpenpyxlWorkbookProtocol, +) -> dict[str, OpenpyxlWorksheetProtocol]: + """Build a sheet map for openpyxl workbooks.""" + sheet_names = getattr(workbook, "sheetnames", None) + if not isinstance(sheet_names, list): + raise ValueError("Invalid workbook: sheetnames missing.") + return {name: workbook[name] for name in sheet_names} + + +def _apply_openpyxl_op( + workbook: OpenpyxlWorkbookProtocol, + sheets: dict[str, OpenpyxlWorksheetProtocol], + op: PatchOp, +) -> PatchDiffItem: + """Apply a single op to openpyxl workbook.""" + if op.op == "add_sheet": + if op.sheet in sheets: + raise ValueError(f"Sheet already exists: {op.sheet}") + sheet = workbook.create_sheet(title=op.sheet) + sheets[op.sheet] = sheet + return PatchDiffItem( + op=op.op, + sheet=op.sheet, + cell=None, + before=None, + after=PatchValue(kind="sheet", value=op.sheet), + ) + + existing_sheet = sheets.get(op.sheet) + if existing_sheet is None: + raise ValueError(f"Sheet not found: {op.sheet}") + cell_ref = op.cell + if cell_ref is None: + raise ValueError(f"{op.op} requires cell.") + cell = existing_sheet[cell_ref] + before = _openpyxl_cell_value(cell) + if op.op == "set_value": + cell.value = op.value + after = PatchValue(kind="value", value=op.value) + return PatchDiffItem( + op=op.op, + sheet=op.sheet, + cell=cell_ref, + before=before, + after=after, + ) + if op.op == "set_formula": + formula = op.formula + if formula is None: + raise ValueError("set_formula requires formula.") + cell.value = formula + after = PatchValue(kind="formula", value=formula) + return PatchDiffItem( + op=op.op, + sheet=op.sheet, + cell=cell_ref, + before=before, + after=after, + ) + raise ValueError(f"Unsupported op: {op.op}") + + +def _openpyxl_cell_value(cell: OpenpyxlCellProtocol) -> PatchValue | None: + """Normalize an openpyxl cell value into PatchValue.""" + value = getattr(cell, "value", None) + if value is None: + return None + data_type = getattr(cell, "data_type", None) + if data_type == "f": + text = _normalize_formula(value) + return PatchValue(kind="formula", value=text) + return PatchValue(kind="value", value=value) + + +def _normalize_formula(value: object) -> str: + """Ensure formula string starts with '='.""" + text = str(value) + return text if text.startswith("=") else f"={text}" + + +def _apply_ops_xlwings( + input_path: Path, output_path: Path, ops: list[PatchOp] +) -> list[PatchDiffItem]: + """Apply operations using Excel COM via xlwings.""" + diff: list[PatchDiffItem] = [] + try: + with _xlwings_workbook(input_path) as workbook: + sheets = {sheet.name: sheet for sheet in workbook.sheets} + for op in ops: + diff.append(_apply_xlwings_op(workbook, sheets, op)) + workbook.save(str(output_path)) + except ValueError: + raise + except Exception as exc: + raise RuntimeError(f"COM patch failed: {exc}") from exc + return diff + + +def _apply_xlwings_op( + workbook: XlwingsWorkbookProtocol, + sheets: dict[str, XlwingsSheetProtocol], + op: PatchOp, +) -> PatchDiffItem: + """Apply a single op to an xlwings workbook.""" + if op.op == "add_sheet": + if op.sheet in sheets: + raise ValueError(f"Sheet already exists: {op.sheet}") + last = workbook.sheets[-1] if workbook.sheets else None + sheet = workbook.sheets.add(name=op.sheet, after=last) + sheets[op.sheet] = sheet + return PatchDiffItem( + op=op.op, + sheet=op.sheet, + cell=None, + before=None, + after=PatchValue(kind="sheet", value=op.sheet), + ) + + existing_sheet = sheets.get(op.sheet) + if existing_sheet is None: + raise ValueError(f"Sheet not found: {op.sheet}") + cell_ref = op.cell + if cell_ref is None: + raise ValueError(f"{op.op} requires cell.") + rng = existing_sheet.range(cell_ref) + before = _xlwings_cell_value(rng) + if op.op == "set_value": + rng.value = op.value + after = PatchValue(kind="value", value=op.value) + return PatchDiffItem( + op=op.op, + sheet=op.sheet, + cell=cell_ref, + before=before, + after=after, + ) + if op.op == "set_formula": + formula = op.formula + if formula is None: + raise ValueError("set_formula requires formula.") + rng.formula = formula + after = PatchValue(kind="formula", value=formula) + return PatchDiffItem( + op=op.op, + sheet=op.sheet, + cell=cell_ref, + before=before, + after=after, + ) + raise ValueError(f"Unsupported op: {op.op}") + + +def _xlwings_cell_value(cell: XlwingsRangeProtocol) -> PatchValue | None: + """Normalize an xlwings cell value into PatchValue.""" + formula = getattr(cell, "formula", None) + if isinstance(formula, str) and formula.startswith("="): + return PatchValue(kind="formula", value=formula) + value = getattr(cell, "value", None) + if value is None: + return None + return PatchValue(kind="value", value=value) + + +@contextmanager +def _xlwings_workbook(file_path: Path) -> Iterator[XlwingsWorkbookProtocol]: + """Open an Excel workbook with a dedicated COM app.""" + app = xw.App(add_book=False, visible=False) + app.display_alerts = False + app.screen_updating = False + workbook = app.books.open(str(file_path)) + try: + yield workbook + finally: + try: + workbook.close() + except Exception: + pass + try: + app.quit() + except Exception: + pass diff --git a/src/exstruct/mcp/server.py b/src/exstruct/mcp/server.py index 5cede48..db46bcf 100644 --- a/src/exstruct/mcp/server.py +++ b/src/exstruct/mcp/server.py @@ -19,11 +19,14 @@ from .tools import ( ExtractToolInput, ExtractToolOutput, + PatchToolInput, + PatchToolOutput, ReadJsonChunkToolInput, ReadJsonChunkToolOutput, ValidateInputToolInput, ValidateInputToolOutput, run_extract_tool, + run_patch_tool, run_read_json_chunk_tool, run_validate_input_tool, ) @@ -194,6 +197,7 @@ def _register_tools( app: FastMCP application instance. policy: Path policy for filesystem access. """ + patch_default_on_conflict: OnConflictPolicy = "rename" async def _extract_tool( # pylint: disable=redefined-builtin xlsx_path: str, @@ -297,6 +301,45 @@ async def _validate_input_tool(xlsx_path: str) -> ValidateInputToolOutput: validate_tool = app.tool(name="exstruct_validate_input") validate_tool(_validate_input_tool) + async def _patch_tool( + xlsx_path: str, + ops: list[dict[str, Any]], + out_dir: str | None = None, + out_name: str | None = None, + on_conflict: OnConflictPolicy | None = None, + ) -> PatchToolOutput: + """Handle the ExStruct patch tool call. + + Args: + xlsx_path: Path to the Excel workbook. + ops: Patch operations to apply. + out_dir: Optional output directory. + out_name: Optional output filename. + on_conflict: Optional conflict policy override. + + Returns: + Patch result payload. + """ + payload = PatchToolInput( + xlsx_path=xlsx_path, + ops=ops, + out_dir=out_dir, + out_name=out_name, + on_conflict=on_conflict, + ) + effective_on_conflict = on_conflict or patch_default_on_conflict + work = functools.partial( + run_patch_tool, + payload, + policy=policy, + on_conflict=effective_on_conflict, + ) + result = cast(PatchToolOutput, await anyio.to_thread.run_sync(work)) + return result + + patch_tool = app.tool(name="exstruct_patch") + patch_tool(_patch_tool) + def _coerce_filter(filter_data: dict[str, Any] | None) -> dict[str, Any] | None: """Normalize filter input for chunk reading. diff --git a/src/exstruct/mcp/tools.py b/src/exstruct/mcp/tools.py index c857b22..0cc4857 100644 --- a/src/exstruct/mcp/tools.py +++ b/src/exstruct/mcp/tools.py @@ -22,6 +22,13 @@ run_extract, ) from .io import PathPolicy +from .patch_runner import ( + PatchDiffItem, + PatchOp, + PatchRequest, + PatchResult, + run_patch, +) from .validate_input import ( ValidateInputRequest, ValidateInputResult, @@ -82,6 +89,24 @@ class ValidateInputToolOutput(BaseModel): errors: list[str] = Field(default_factory=list) +class PatchToolInput(BaseModel): + """MCP tool input for patching Excel files.""" + + xlsx_path: str + ops: list[PatchOp] + out_dir: str | None = None + out_name: str | None = None + on_conflict: OnConflictPolicy | None = None + + +class PatchToolOutput(BaseModel): + """MCP tool output for patching Excel files.""" + + out_path: str + patch_diff: list[PatchDiffItem] = Field(default_factory=list) + warnings: list[str] = Field(default_factory=list) + + def run_extract_tool( payload: ExtractToolInput, *, @@ -150,6 +175,33 @@ def run_validate_input_tool( return _to_validate_input_output(result) +def run_patch_tool( + payload: PatchToolInput, + *, + policy: PathPolicy | None = None, + on_conflict: OnConflictPolicy | None = None, +) -> PatchToolOutput: + """Run the patch tool handler. + + Args: + payload: Tool input payload. + policy: Optional path policy for access control. + on_conflict: Optional conflict policy override. + + Returns: + Tool output payload. + """ + request = PatchRequest( + xlsx_path=Path(payload.xlsx_path), + ops=payload.ops, + out_dir=Path(payload.out_dir) if payload.out_dir else None, + out_name=payload.out_name, + on_conflict=payload.on_conflict or on_conflict or "rename", + ) + result = run_patch(request, policy=policy) + return _to_patch_tool_output(result) + + def _to_tool_output(result: ExtractResult) -> ExtractToolOutput: """Convert internal result to tool output model. @@ -201,3 +253,19 @@ def _to_validate_input_output( warnings=result.warnings, errors=result.errors, ) + + +def _to_patch_tool_output(result: PatchResult) -> PatchToolOutput: + """Convert internal result to patch tool output. + + Args: + result: Internal patch result. + + Returns: + Tool output payload. + """ + return PatchToolOutput( + out_path=result.out_path, + patch_diff=result.patch_diff, + warnings=result.warnings, + ) diff --git a/tests/mcp/test_patch_runner.py b/tests/mcp/test_patch_runner.py new file mode 100644 index 0000000..13fc5b1 --- /dev/null +++ b/tests/mcp/test_patch_runner.py @@ -0,0 +1,178 @@ +from __future__ import annotations + +from pathlib import Path + +from openpyxl import Workbook, load_workbook +from pydantic import ValidationError +import pytest + +from exstruct.cli.availability import ComAvailability +from exstruct.mcp import patch_runner +from exstruct.mcp.io import PathPolicy +from exstruct.mcp.patch_runner import PatchOp, PatchRequest, run_patch + + +def _create_workbook(path: Path) -> None: + workbook = Workbook() + sheet = workbook.active + sheet.title = "Sheet1" + sheet["A1"] = "old" + sheet["B1"] = 1 + workbook.save(path) + workbook.close() + + +def _disable_com(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr( + patch_runner, + "get_com_availability", + lambda: ComAvailability(available=False, reason="test"), + ) + + +def test_run_patch_set_value_and_formula( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _disable_com(monkeypatch) + input_path = tmp_path / "book.xlsx" + _create_workbook(input_path) + ops = [ + PatchOp(op="set_value", sheet="Sheet1", cell="A1", value="new"), + PatchOp(op="set_formula", sheet="Sheet1", cell="B1", formula="=SUM(1,1)"), + ] + request = PatchRequest(xlsx_path=input_path, ops=ops, on_conflict="rename") + result = run_patch(request, policy=PathPolicy(root=tmp_path)) + assert Path(result.out_path).exists() + workbook = load_workbook(result.out_path) + try: + sheet = workbook["Sheet1"] + assert sheet["A1"].value == "new" + formula_value = sheet["B1"].value + if isinstance(formula_value, str) and not formula_value.startswith("="): + formula_value = f"={formula_value}" + assert formula_value == "=SUM(1,1)" + finally: + workbook.close() + assert len(result.patch_diff) == 2 + assert result.patch_diff[0].after is not None + + +def test_run_patch_add_sheet_and_set_value( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _disable_com(monkeypatch) + input_path = tmp_path / "book.xlsx" + _create_workbook(input_path) + ops = [ + PatchOp(op="add_sheet", sheet="NewSheet"), + PatchOp(op="set_value", sheet="NewSheet", cell="A1", value="ok"), + ] + request = PatchRequest(xlsx_path=input_path, ops=ops, on_conflict="rename") + result = run_patch(request, policy=PathPolicy(root=tmp_path)) + workbook = load_workbook(result.out_path) + try: + assert "NewSheet" in workbook.sheetnames + assert workbook["NewSheet"]["A1"].value == "ok" + finally: + workbook.close() + + +def test_run_patch_add_sheet_rejects_duplicate( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _disable_com(monkeypatch) + input_path = tmp_path / "book.xlsx" + _create_workbook(input_path) + ops = [PatchOp(op="add_sheet", sheet="Sheet1")] + request = PatchRequest(xlsx_path=input_path, ops=ops, on_conflict="rename") + with pytest.raises(ValueError): + run_patch(request, policy=PathPolicy(root=tmp_path)) + + +def test_run_patch_rejects_value_starting_with_equal() -> None: + with pytest.raises(ValidationError): + PatchOp(op="set_value", sheet="Sheet1", cell="A1", value="=SUM(1,1)") + + +def test_run_patch_rejects_formula_without_equal() -> None: + with pytest.raises(ValidationError): + PatchOp(op="set_formula", sheet="Sheet1", cell="A1", formula="SUM(1,1)") + + +def test_run_patch_rejects_path_outside_root( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _disable_com(monkeypatch) + root = tmp_path / "root" + root.mkdir() + input_path = tmp_path / "book.xlsx" + _create_workbook(input_path) + ops = [PatchOp(op="set_value", sheet="Sheet1", cell="A1", value="x")] + request = PatchRequest(xlsx_path=input_path, ops=ops, on_conflict="rename") + with pytest.raises(ValueError): + run_patch(request, policy=PathPolicy(root=root)) + + +def test_run_patch_conflict_rename( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _disable_com(monkeypatch) + input_path = tmp_path / "book.xlsx" + _create_workbook(input_path) + default_out = tmp_path / "book_patched.xlsx" + default_out.write_text("dummy", encoding="utf-8") + ops = [PatchOp(op="set_value", sheet="Sheet1", cell="A1", value="x")] + request = PatchRequest(xlsx_path=input_path, ops=ops, on_conflict="rename") + result = run_patch(request, policy=PathPolicy(root=tmp_path)) + assert result.out_path != str(default_out) + assert Path(result.out_path).exists() + + +def test_run_patch_conflict_skip( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _disable_com(monkeypatch) + input_path = tmp_path / "book.xlsx" + _create_workbook(input_path) + default_out = tmp_path / "book_patched.xlsx" + default_out.write_text("dummy", encoding="utf-8") + ops = [PatchOp(op="set_value", sheet="Sheet1", cell="A1", value="x")] + request = PatchRequest(xlsx_path=input_path, ops=ops, on_conflict="skip") + result = run_patch(request, policy=PathPolicy(root=tmp_path)) + assert result.out_path == str(default_out) + assert result.patch_diff == [] + assert any("skipping" in warning for warning in result.warnings) + + +def test_run_patch_conflict_overwrite( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _disable_com(monkeypatch) + input_path = tmp_path / "book.xlsx" + _create_workbook(input_path) + default_out = tmp_path / "book_patched.xlsx" + _create_workbook(default_out) + ops = [PatchOp(op="set_value", sheet="Sheet1", cell="A1", value="new")] + request = PatchRequest(xlsx_path=input_path, ops=ops, on_conflict="overwrite") + result = run_patch(request, policy=PathPolicy(root=tmp_path)) + assert result.out_path == str(default_out) + workbook = load_workbook(result.out_path) + try: + assert workbook["Sheet1"]["A1"].value == "new" + finally: + workbook.close() + + +def test_run_patch_atomicity(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + _disable_com(monkeypatch) + input_path = tmp_path / "book.xlsx" + _create_workbook(input_path) + ops = [ + PatchOp(op="set_value", sheet="Sheet1", cell="A1", value="x"), + PatchOp(op="set_value", sheet="Missing", cell="A1", value="y"), + ] + request = PatchRequest(xlsx_path=input_path, ops=ops, on_conflict="rename") + output_path = tmp_path / "book_patched.xlsx" + with pytest.raises(ValueError): + run_patch(request, policy=PathPolicy(root=tmp_path)) + assert not output_path.exists() diff --git a/tests/mcp/test_server.py b/tests/mcp/test_server.py index b90131f..993dcbd 100644 --- a/tests/mcp/test_server.py +++ b/tests/mcp/test_server.py @@ -14,6 +14,8 @@ from exstruct.mcp.tools import ( ExtractToolInput, ExtractToolOutput, + PatchToolInput, + PatchToolOutput, ReadJsonChunkToolInput, ReadJsonChunkToolOutput, ValidateInputToolInput, @@ -126,6 +128,15 @@ def fake_run_validate_input_tool( calls["validate"] = (payload, policy) return ValidateInputToolOutput(is_readable=True) + def fake_run_patch_tool( + payload: PatchToolInput, + *, + policy: PathPolicy, + on_conflict: OnConflictPolicy, + ) -> PatchToolOutput: + calls["patch"] = (payload, policy, on_conflict) + return PatchToolOutput(out_path="out.xlsx", patch_diff=[]) + async def fake_run_sync(func: Callable[[], object]) -> object: return func() @@ -134,6 +145,7 @@ async def fake_run_sync(func: Callable[[], object]) -> object: server, "run_read_json_chunk_tool", fake_run_read_json_chunk_tool ) monkeypatch.setattr(server, "run_validate_input_tool", fake_run_validate_input_tool) + monkeypatch.setattr(server, "run_patch_tool", fake_run_patch_tool) monkeypatch.setattr(anyio.to_thread, "run_sync", fake_run_sync) server._register_tools(app, policy, default_on_conflict="rename") @@ -152,10 +164,17 @@ async def fake_run_sync(func: Callable[[], object]) -> object: Callable[..., Awaitable[object]], app.tools["exstruct_validate_input"] ) anyio.run(_call_async, validate_tool, {"xlsx_path": "in.xlsx"}) + patch_tool = cast(Callable[..., Awaitable[object]], app.tools["exstruct_patch"]) + anyio.run( + _call_async, + patch_tool, + {"xlsx_path": "in.xlsx", "ops": [{"op": "add_sheet", "sheet": "New"}]}, + ) assert calls["extract"][2] == "rename" chunk_call = cast(tuple[ReadJsonChunkToolInput, PathPolicy], calls["chunk"]) assert chunk_call[0].filter is not None + assert calls["patch"][2] == "rename" def test_run_server_sets_env(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: diff --git a/tests/mcp/test_tool_models.py b/tests/mcp/test_tool_models.py index 71637cf..4955b15 100644 --- a/tests/mcp/test_tool_models.py +++ b/tests/mcp/test_tool_models.py @@ -3,7 +3,7 @@ from pydantic import ValidationError import pytest -from exstruct.mcp.tools import ExtractToolInput, ReadJsonChunkToolInput +from exstruct.mcp.tools import ExtractToolInput, PatchToolInput, ReadJsonChunkToolInput def test_extract_tool_input_defaults() -> None: @@ -17,3 +17,13 @@ def test_extract_tool_input_defaults() -> None: def test_read_json_chunk_rejects_invalid_max_bytes() -> None: with pytest.raises(ValidationError): ReadJsonChunkToolInput(out_path="out.json", max_bytes=0) + + +def test_patch_tool_input_defaults() -> None: + payload = PatchToolInput( + xlsx_path="input.xlsx", + ops=[{"op": "add_sheet", "sheet": "New"}], + ) + assert payload.out_dir is None + assert payload.out_name is None + assert payload.on_conflict is None diff --git a/tests/mcp/test_tools_handlers.py b/tests/mcp/test_tools_handlers.py index c77ca1b..e723622 100644 --- a/tests/mcp/test_tools_handlers.py +++ b/tests/mcp/test_tools_handlers.py @@ -11,6 +11,7 @@ ReadJsonChunkResult, ) from exstruct.mcp.extract_runner import ExtractRequest, ExtractResult +from exstruct.mcp.patch_runner import PatchRequest, PatchResult from exstruct.mcp.validate_input import ValidateInputRequest, ValidateInputResult @@ -91,3 +92,26 @@ def _fake_validate_input( request = captured["request"] assert isinstance(request, ValidateInputRequest) assert request.xlsx_path == Path("input.xlsx") + + +def test_run_patch_tool_builds_request( + monkeypatch: pytest.MonkeyPatch, +) -> None: + captured: dict[str, object] = {} + + def _fake_run_patch( + request: PatchRequest, *, policy: object | None = None + ) -> PatchResult: + captured["request"] = request + return PatchResult(out_path="out.xlsx", patch_diff=[]) + + monkeypatch.setattr(tools, "run_patch", _fake_run_patch) + payload = tools.PatchToolInput( + xlsx_path="input.xlsx", + ops=[{"op": "add_sheet", "sheet": "New"}], + ) + tools.run_patch_tool(payload, on_conflict="rename") + request = captured["request"] + assert isinstance(request, PatchRequest) + assert request.xlsx_path == Path("input.xlsx") + assert request.on_conflict == "rename" diff --git a/uv.lock b/uv.lock index 265284d..039196c 100644 --- a/uv.lock +++ b/uv.lock @@ -106,6 +106,7 @@ dependencies = [ { name = "beautifulsoup4" }, { name = "exstruct" }, { name = "lxml" }, + { name = "matplotlib" }, { name = "openai" }, { name = "openpyxl" }, { name = "pandas" }, @@ -128,6 +129,7 @@ requires-dist = [ { name = "beautifulsoup4", specifier = ">=4.14.3" }, { name = "exstruct", editable = "." }, { name = "lxml", specifier = ">=6.0.2" }, + { name = "matplotlib", specifier = ">=3.8.0" }, { name = "openai", specifier = ">=2.15.0" }, { name = "openpyxl", specifier = ">=3.1.5" }, { name = "pandas", specifier = ">=2.3.3" }, @@ -344,6 +346,88 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + [[package]] name = "coverage" version = "7.12.0" @@ -498,6 +582,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, ] +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + [[package]] name = "distlib" version = "0.4.0" @@ -622,6 +715,55 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, ] +[[package]] +name = "fonttools" +version = "4.61.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/12/bf9f4eaa2fad039356cc627587e30ed008c03f1cebd3034376b5ee8d1d44/fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09", size = 2852213, upload-time = "2025-12-12T17:29:46.675Z" }, + { url = "https://files.pythonhosted.org/packages/ac/49/4138d1acb6261499bedde1c07f8c2605d1d8f9d77a151e5507fd3ef084b6/fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37", size = 2401689, upload-time = "2025-12-12T17:29:48.769Z" }, + { url = "https://files.pythonhosted.org/packages/e5/fe/e6ce0fe20a40e03aef906af60aa87668696f9e4802fa283627d0b5ed777f/fonttools-4.61.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb", size = 5058809, upload-time = "2025-12-12T17:29:51.701Z" }, + { url = "https://files.pythonhosted.org/packages/79/61/1ca198af22f7dd22c17ab86e9024ed3c06299cfdb08170640e9996d501a0/fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9", size = 5036039, upload-time = "2025-12-12T17:29:53.659Z" }, + { url = "https://files.pythonhosted.org/packages/99/cc/fa1801e408586b5fce4da9f5455af8d770f4fc57391cd5da7256bb364d38/fonttools-4.61.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87", size = 5034714, upload-time = "2025-12-12T17:29:55.592Z" }, + { url = "https://files.pythonhosted.org/packages/bf/aa/b7aeafe65adb1b0a925f8f25725e09f078c635bc22754f3fecb7456955b0/fonttools-4.61.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56", size = 5158648, upload-time = "2025-12-12T17:29:57.861Z" }, + { url = "https://files.pythonhosted.org/packages/99/f9/08ea7a38663328881384c6e7777bbefc46fd7d282adfd87a7d2b84ec9d50/fonttools-4.61.1-cp311-cp311-win32.whl", hash = "sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a", size = 2280681, upload-time = "2025-12-12T17:29:59.943Z" }, + { url = "https://files.pythonhosted.org/packages/07/ad/37dd1ae5fa6e01612a1fbb954f0927681f282925a86e86198ccd7b15d515/fonttools-4.61.1-cp311-cp311-win_amd64.whl", hash = "sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7", size = 2331951, upload-time = "2025-12-12T17:30:02.254Z" }, + { url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593, upload-time = "2025-12-12T17:30:04.225Z" }, + { url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231, upload-time = "2025-12-12T17:30:06.47Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103, upload-time = "2025-12-12T17:30:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d", size = 5004295, upload-time = "2025-12-12T17:30:10.56Z" }, + { url = "https://files.pythonhosted.org/packages/14/e8/7424ced75473983b964d09f6747fa09f054a6d656f60e9ac9324cf40c743/fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8", size = 4944109, upload-time = "2025-12-12T17:30:12.874Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8b/6391b257fa3d0b553d73e778f953a2f0154292a7a7a085e2374b111e5410/fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0", size = 5093598, upload-time = "2025-12-12T17:30:15.79Z" }, + { url = "https://files.pythonhosted.org/packages/d9/71/fd2ea96cdc512d92da5678a1c98c267ddd4d8c5130b76d0f7a80f9a9fde8/fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261", size = 2269060, upload-time = "2025-12-12T17:30:18.058Z" }, + { url = "https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9", size = 2319078, upload-time = "2025-12-12T17:30:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c", size = 2846454, upload-time = "2025-12-12T17:30:24.938Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e", size = 2398191, upload-time = "2025-12-12T17:30:27.343Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5", size = 4928410, upload-time = "2025-12-12T17:30:29.771Z" }, + { url = "https://files.pythonhosted.org/packages/b0/8d/6fb3494dfe61a46258cd93d979cf4725ded4eb46c2a4ca35e4490d84daea/fonttools-4.61.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd", size = 4984460, upload-time = "2025-12-12T17:30:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/f7/f1/a47f1d30b3dc00d75e7af762652d4cbc3dff5c2697a0dbd5203c81afd9c3/fonttools-4.61.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3", size = 4925800, upload-time = "2025-12-12T17:30:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/a7/01/e6ae64a0981076e8a66906fab01539799546181e32a37a0257b77e4aa88b/fonttools-4.61.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d", size = 5067859, upload-time = "2025-12-12T17:30:36.593Z" }, + { url = "https://files.pythonhosted.org/packages/73/aa/28e40b8d6809a9b5075350a86779163f074d2b617c15d22343fce81918db/fonttools-4.61.1-cp313-cp313-win32.whl", hash = "sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c", size = 2267821, upload-time = "2025-12-12T17:30:38.478Z" }, + { url = "https://files.pythonhosted.org/packages/1a/59/453c06d1d83dc0951b69ef692d6b9f1846680342927df54e9a1ca91c6f90/fonttools-4.61.1-cp313-cp313-win_amd64.whl", hash = "sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b", size = 2318169, upload-time = "2025-12-12T17:30:40.951Z" }, + { url = "https://files.pythonhosted.org/packages/32/8f/4e7bf82c0cbb738d3c2206c920ca34ca74ef9dabde779030145d28665104/fonttools-4.61.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd", size = 2846094, upload-time = "2025-12-12T17:30:43.511Z" }, + { url = "https://files.pythonhosted.org/packages/71/09/d44e45d0a4f3a651f23a1e9d42de43bc643cce2971b19e784cc67d823676/fonttools-4.61.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e", size = 2396589, upload-time = "2025-12-12T17:30:45.681Z" }, + { url = "https://files.pythonhosted.org/packages/89/18/58c64cafcf8eb677a99ef593121f719e6dcbdb7d1c594ae5a10d4997ca8a/fonttools-4.61.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c", size = 4877892, upload-time = "2025-12-12T17:30:47.709Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ec/9e6b38c7ba1e09eb51db849d5450f4c05b7e78481f662c3b79dbde6f3d04/fonttools-4.61.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75", size = 4972884, upload-time = "2025-12-12T17:30:49.656Z" }, + { url = "https://files.pythonhosted.org/packages/5e/87/b5339da8e0256734ba0dbbf5b6cdebb1dd79b01dc8c270989b7bcd465541/fonttools-4.61.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063", size = 4924405, upload-time = "2025-12-12T17:30:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/0b/47/e3409f1e1e69c073a3a6fd8cb886eb18c0bae0ee13db2c8d5e7f8495e8b7/fonttools-4.61.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2", size = 5035553, upload-time = "2025-12-12T17:30:54.823Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b6/1f6600161b1073a984294c6c031e1a56ebf95b6164249eecf30012bb2e38/fonttools-4.61.1-cp314-cp314-win32.whl", hash = "sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c", size = 2271915, upload-time = "2025-12-12T17:30:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/52/7b/91e7b01e37cc8eb0e1f770d08305b3655e4f002fc160fb82b3390eabacf5/fonttools-4.61.1-cp314-cp314-win_amd64.whl", hash = "sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c", size = 2323487, upload-time = "2025-12-12T17:30:59.804Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/908ad78e46c61c3e3ed70c3b58ff82ab48437faf84ec84f109592cabbd9f/fonttools-4.61.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa", size = 2929571, upload-time = "2025-12-12T17:31:02.574Z" }, + { url = "https://files.pythonhosted.org/packages/bd/41/975804132c6dea64cdbfbaa59f3518a21c137a10cccf962805b301ac6ab2/fonttools-4.61.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91", size = 2435317, upload-time = "2025-12-12T17:31:04.974Z" }, + { url = "https://files.pythonhosted.org/packages/b0/5a/aef2a0a8daf1ebaae4cfd83f84186d4a72ee08fd6a8451289fcd03ffa8a4/fonttools-4.61.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19", size = 4882124, upload-time = "2025-12-12T17:31:07.456Z" }, + { url = "https://files.pythonhosted.org/packages/80/33/d6db3485b645b81cea538c9d1c9219d5805f0877fda18777add4671c5240/fonttools-4.61.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba", size = 5100391, upload-time = "2025-12-12T17:31:09.732Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d6/675ba631454043c75fcf76f0ca5463eac8eb0666ea1d7badae5fea001155/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7", size = 4978800, upload-time = "2025-12-12T17:31:11.681Z" }, + { url = "https://files.pythonhosted.org/packages/7f/33/d3ec753d547a8d2bdaedd390d4a814e8d5b45a093d558f025c6b990b554c/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118", size = 5006426, upload-time = "2025-12-12T17:31:13.764Z" }, + { url = "https://files.pythonhosted.org/packages/b4/40/cc11f378b561a67bea850ab50063366a0d1dd3f6d0a30ce0f874b0ad5664/fonttools-4.61.1-cp314-cp314t-win32.whl", hash = "sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5", size = 2335377, upload-time = "2025-12-12T17:31:16.49Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ff/c9a2b66b39f8628531ea58b320d66d951267c98c6a38684daa8f50fb02f8/fonttools-4.61.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b", size = 2400613, upload-time = "2025-12-12T17:31:18.769Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, +] + [[package]] name = "ghp-import" version = "2.1.0" @@ -923,6 +1065,96 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] +[[package]] +name = "kiwisolver" +version = "1.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, + { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, + { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, + { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, + { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, + { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, + { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, + { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, + { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" }, + { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" }, + { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" }, + { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" }, + { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" }, + { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" }, + { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" }, + { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" }, + { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" }, + { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" }, + { url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" }, + { url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" }, + { url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" }, + { url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" }, + { url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" }, + { url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" }, + { url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" }, + { url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" }, + { url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" }, + { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" }, + { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" }, + { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, + { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, +] + [[package]] name = "librt" version = "0.7.3" @@ -1183,6 +1415,70 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "matplotlib" +version = "3.10.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" }, + { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" }, + { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, + { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, + { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" }, + { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" }, + { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" }, + { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" }, + { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" }, + { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" }, + { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" }, + { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" }, + { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" }, + { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" }, + { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, +] + [[package]] name = "mcp" version = "1.25.0" @@ -1921,6 +2217,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dd/c3/d0047678146c294469c33bae167c8ace337deafb736b0bf97b9bc481aa65/pymupdf-1.26.7-cp310-abi3-win_amd64.whl", hash = "sha256:425b1befe40d41b72eb0fe211711c7ae334db5eb60307e9dd09066ed060cceba", size = 18405952, upload-time = "2025-12-11T21:48:02.947Z" }, ] +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + [[package]] name = "pypdfium2" version = "5.1.0" From 01113b78b1f78d64e53502126ba55619e02a43fe Mon Sep 17 00:00:00 2001 From: harumiWeb Date: Thu, 5 Feb 2026 22:53:33 +0900 Subject: [PATCH 03/25] feat: Enhance patch functionality with auto_formula support and structured error handling --- docs/agents/FEATURE_SPEC.md | 17 +++- src/exstruct/mcp/__init__.py | 2 + src/exstruct/mcp/patch_runner.py | 134 ++++++++++++++++++++++++---- src/exstruct/mcp/server.py | 2 + src/exstruct/mcp/tools.py | 5 ++ tests/com/test_charts_extraction.py | 87 +++++++++--------- tests/com/test_shapes_extraction.py | 107 +++++++++++----------- tests/conftest.py | 31 +++++++ tests/mcp/test_patch_runner.py | 49 ++++++++-- 9 files changed, 312 insertions(+), 122 deletions(-) diff --git a/docs/agents/FEATURE_SPEC.md b/docs/agents/FEATURE_SPEC.md index 9e5ec14..c82da98 100644 --- a/docs/agents/FEATURE_SPEC.md +++ b/docs/agents/FEATURE_SPEC.md @@ -23,11 +23,13 @@ - `out_dir: str | None` - `out_name: str | None` - `on_conflict: "overwrite" | "skip" | "rename" | None` +- `auto_formula: bool`(default: false) ### Output - `out_path: str` - `patch_diff: list[PatchDiffItem]` - `warnings: list[str]` +- `error: PatchErrorDetail | null` --- @@ -64,7 +66,8 @@ - `cell` - A1形式を必須(例: `B3`) - `set_value` - - `value` が `=` から始まる文字列は拒否 + - `value` が `=` から始まる場合は通常は拒否 + - `auto_formula=true` のときは `set_formula` 相当として処理 - `set_formula` - `formula` は必ず `=` から開始 @@ -84,6 +87,7 @@ 最低限の構造: - `op: "set_value" | "set_formula" | "add_sheet"` +- `op_index: int` - `sheet: str` - `cell: str | null` - `before: PatchValue | null` @@ -95,6 +99,14 @@ - `kind: "value" | "formula" | "sheet"` - `value: str | int | float | null` +`PatchErrorDetail`: + +- `op_index: int` +- `op: "set_value" | "set_formula" | "add_sheet"` +- `sheet: str` +- `cell: str | null` +- `message: str` + --- ## 7. バックエンド方針 @@ -119,7 +131,8 @@ ## 9. エラー処理 -- パス違反 / シート不在 / セル不正 / 数式不正: `ValueError` +- パス違反 / セル不正 / 数式不正: `ValueError` +- シート不在 / 操作不正: `PatchErrorDetail` を `error` に格納 - 読み込み不能: `FileNotFoundError` / `OSError` - バックエンド例外: `RuntimeError` diff --git a/src/exstruct/mcp/__init__.py b/src/exstruct/mcp/__init__.py index 7829f69..68fd69d 100644 --- a/src/exstruct/mcp/__init__.py +++ b/src/exstruct/mcp/__init__.py @@ -18,6 +18,7 @@ from .io import PathPolicy from .patch_runner import ( PatchDiffItem, + PatchErrorDetail, PatchOp, PatchRequest, PatchResult, @@ -51,6 +52,7 @@ "ExtractToolInput", "ExtractToolOutput", "PatchDiffItem", + "PatchErrorDetail", "PatchOp", "PatchRequest", "PatchResult", diff --git a/src/exstruct/mcp/patch_runner.py b/src/exstruct/mcp/patch_runner.py index d6f1c57..fec9474 100644 --- a/src/exstruct/mcp/patch_runner.py +++ b/src/exstruct/mcp/patch_runner.py @@ -156,8 +156,6 @@ def _validate_set_value(op: PatchOp) -> None: """Validate set_value operation.""" if op.formula is not None: raise ValueError("set_value does not accept formula.") - if isinstance(op.value, str) and op.value.startswith("="): - raise ValueError("set_value rejects values starting with '='.") def _validate_set_formula(op: PatchOp) -> None: @@ -180,6 +178,7 @@ class PatchValue(BaseModel): class PatchDiffItem(BaseModel): """Applied change record for patch operations.""" + op_index: int op: PatchOpType sheet: str cell: str | None = None @@ -188,6 +187,16 @@ class PatchDiffItem(BaseModel): status: PatchStatus = "applied" +class PatchErrorDetail(BaseModel): + """Structured error details for patch failures.""" + + op_index: int + op: PatchOpType + sheet: str + cell: str | None + message: str + + class PatchRequest(BaseModel): """Input model for ExStruct MCP patch.""" @@ -196,6 +205,7 @@ class PatchRequest(BaseModel): out_dir: Path | None = None out_name: str | None = None on_conflict: OnConflictPolicy = "rename" + auto_formula: bool = False class PatchResult(BaseModel): @@ -204,6 +214,7 @@ class PatchResult(BaseModel): out_path: str patch_diff: list[PatchDiffItem] = Field(default_factory=list) warnings: list[str] = Field(default_factory=list) + error: PatchErrorDetail | None = None def run_patch( @@ -247,12 +258,18 @@ def run_patch( resolved_input, output_path, request.ops, + request.auto_formula, ) return PatchResult( out_path=str(output_path), patch_diff=diff, warnings=warnings ) - except ValueError: - raise + except PatchOpError as exc: + return PatchResult( + out_path=str(output_path), + patch_diff=[], + warnings=warnings, + error=exc.detail, + ) except Exception as exc: fallback = _maybe_fallback_openpyxl( resolved_input, @@ -260,6 +277,7 @@ def run_patch( request.ops, warnings, reason=f"COM patch failed; falling back to openpyxl. ({exc!r})", + auto_formula=request.auto_formula, ) if fallback is not None: return fallback @@ -267,7 +285,13 @@ def run_patch( if com.reason: warnings.append(f"COM unavailable: {com.reason}") - return _apply_with_openpyxl(resolved_input, output_path, request.ops, warnings) + return _apply_with_openpyxl( + resolved_input, + output_path, + request.ops, + warnings, + auto_formula=request.auto_formula, + ) def _apply_with_openpyxl( @@ -275,10 +299,19 @@ def _apply_with_openpyxl( output_path: Path, ops: list[PatchOp], warnings: list[str], + *, + auto_formula: bool, ) -> PatchResult: """Apply patch operations using openpyxl.""" try: - diff = _apply_ops_openpyxl(input_path, output_path, ops) + diff = _apply_ops_openpyxl(input_path, output_path, ops, auto_formula) + except PatchOpError as exc: + return PatchResult( + out_path=str(output_path), + patch_diff=[], + warnings=warnings, + error=exc.detail, + ) except ValueError: raise except FileNotFoundError: @@ -299,13 +332,20 @@ def _maybe_fallback_openpyxl( warnings: list[str], *, reason: str, + auto_formula: bool, ) -> PatchResult | None: """Attempt openpyxl fallback after COM failure.""" if input_path.suffix.lower() == ".xls": warnings.append(reason) return None warnings.append(reason) - return _apply_with_openpyxl(input_path, output_path, ops, warnings) + return _apply_with_openpyxl( + input_path, + output_path, + ops, + warnings, + auto_formula=auto_formula, + ) def _resolve_input_path(path: Path, *, policy: PathPolicy | None) -> Path: @@ -389,7 +429,10 @@ def _next_available_path(path: Path) -> Path: def _apply_ops_openpyxl( - input_path: Path, output_path: Path, ops: list[PatchOp] + input_path: Path, + output_path: Path, + ops: list[PatchOp], + auto_formula: bool, ) -> list[PatchDiffItem]: """Apply operations using openpyxl.""" try: @@ -402,7 +445,7 @@ def _apply_ops_openpyxl( workbook = load_workbook(input_path) try: - diff = _apply_ops_to_openpyxl_workbook(workbook, ops) + diff = _apply_ops_to_openpyxl_workbook(workbook, ops, auto_formula) workbook.save(output_path) finally: workbook.close() @@ -410,13 +453,16 @@ def _apply_ops_openpyxl( def _apply_ops_to_openpyxl_workbook( - workbook: OpenpyxlWorkbookProtocol, ops: list[PatchOp] + workbook: OpenpyxlWorkbookProtocol, ops: list[PatchOp], auto_formula: bool ) -> list[PatchDiffItem]: """Apply ops to an openpyxl workbook instance.""" sheets = _openpyxl_sheet_map(workbook) diff: list[PatchDiffItem] = [] - for op in ops: - diff.append(_apply_openpyxl_op(workbook, sheets, op)) + for index, op in enumerate(ops): + try: + diff.append(_apply_openpyxl_op(workbook, sheets, op, index, auto_formula)) + except ValueError as exc: + raise PatchOpError.from_op(index, op, exc) from exc return diff @@ -434,6 +480,8 @@ def _apply_openpyxl_op( workbook: OpenpyxlWorkbookProtocol, sheets: dict[str, OpenpyxlWorksheetProtocol], op: PatchOp, + index: int, + auto_formula: bool, ) -> PatchDiffItem: """Apply a single op to openpyxl workbook.""" if op.op == "add_sheet": @@ -442,6 +490,7 @@ def _apply_openpyxl_op( sheet = workbook.create_sheet(title=op.sheet) sheets[op.sheet] = sheet return PatchDiffItem( + op_index=index, op=op.op, sheet=op.sheet, cell=None, @@ -458,9 +507,16 @@ def _apply_openpyxl_op( cell = existing_sheet[cell_ref] before = _openpyxl_cell_value(cell) if op.op == "set_value": - cell.value = op.value - after = PatchValue(kind="value", value=op.value) + if isinstance(op.value, str) and op.value.startswith("="): + if not auto_formula: + raise ValueError("set_value rejects values starting with '='.") + cell.value = op.value + after = PatchValue(kind="formula", value=op.value) + else: + cell.value = op.value + after = PatchValue(kind="value", value=op.value) return PatchDiffItem( + op_index=index, op=op.op, sheet=op.sheet, cell=cell_ref, @@ -474,6 +530,7 @@ def _apply_openpyxl_op( cell.value = formula after = PatchValue(kind="formula", value=formula) return PatchDiffItem( + op_index=index, op=op.op, sheet=op.sheet, cell=cell_ref, @@ -502,15 +559,23 @@ def _normalize_formula(value: object) -> str: def _apply_ops_xlwings( - input_path: Path, output_path: Path, ops: list[PatchOp] + input_path: Path, + output_path: Path, + ops: list[PatchOp], + auto_formula: bool, ) -> list[PatchDiffItem]: """Apply operations using Excel COM via xlwings.""" diff: list[PatchDiffItem] = [] try: with _xlwings_workbook(input_path) as workbook: sheets = {sheet.name: sheet for sheet in workbook.sheets} - for op in ops: - diff.append(_apply_xlwings_op(workbook, sheets, op)) + for index, op in enumerate(ops): + try: + diff.append( + _apply_xlwings_op(workbook, sheets, op, index, auto_formula) + ) + except ValueError as exc: + raise PatchOpError.from_op(index, op, exc) from exc workbook.save(str(output_path)) except ValueError: raise @@ -523,6 +588,8 @@ def _apply_xlwings_op( workbook: XlwingsWorkbookProtocol, sheets: dict[str, XlwingsSheetProtocol], op: PatchOp, + index: int, + auto_formula: bool, ) -> PatchDiffItem: """Apply a single op to an xlwings workbook.""" if op.op == "add_sheet": @@ -532,6 +599,7 @@ def _apply_xlwings_op( sheet = workbook.sheets.add(name=op.sheet, after=last) sheets[op.sheet] = sheet return PatchDiffItem( + op_index=index, op=op.op, sheet=op.sheet, cell=None, @@ -548,9 +616,16 @@ def _apply_xlwings_op( rng = existing_sheet.range(cell_ref) before = _xlwings_cell_value(rng) if op.op == "set_value": - rng.value = op.value - after = PatchValue(kind="value", value=op.value) + if isinstance(op.value, str) and op.value.startswith("="): + if not auto_formula: + raise ValueError("set_value rejects values starting with '='.") + rng.formula = op.value + after = PatchValue(kind="formula", value=op.value) + else: + rng.value = op.value + after = PatchValue(kind="value", value=op.value) return PatchDiffItem( + op_index=index, op=op.op, sheet=op.sheet, cell=cell_ref, @@ -564,6 +639,7 @@ def _apply_xlwings_op( rng.formula = formula after = PatchValue(kind="formula", value=formula) return PatchDiffItem( + op_index=index, op=op.op, sheet=op.sheet, cell=cell_ref, @@ -602,3 +678,23 @@ def _xlwings_workbook(file_path: Path) -> Iterator[XlwingsWorkbookProtocol]: app.quit() except Exception: pass + + +class PatchOpError(ValueError): + """Patch operation error with structured detail.""" + + def __init__(self, detail: PatchErrorDetail) -> None: + super().__init__(detail.message) + self.detail = detail + + @classmethod + def from_op(cls, index: int, op: PatchOp, exc: Exception) -> PatchOpError: + """Build a PatchOpError from an op and exception.""" + detail = PatchErrorDetail( + op_index=index, + op=op.op, + sheet=op.sheet, + cell=op.cell, + message=str(exc), + ) + return cls(detail) diff --git a/src/exstruct/mcp/server.py b/src/exstruct/mcp/server.py index db46bcf..46d7a90 100644 --- a/src/exstruct/mcp/server.py +++ b/src/exstruct/mcp/server.py @@ -307,6 +307,7 @@ async def _patch_tool( out_dir: str | None = None, out_name: str | None = None, on_conflict: OnConflictPolicy | None = None, + auto_formula: bool = False, ) -> PatchToolOutput: """Handle the ExStruct patch tool call. @@ -326,6 +327,7 @@ async def _patch_tool( out_dir=out_dir, out_name=out_name, on_conflict=on_conflict, + auto_formula=auto_formula, ) effective_on_conflict = on_conflict or patch_default_on_conflict work = functools.partial( diff --git a/src/exstruct/mcp/tools.py b/src/exstruct/mcp/tools.py index 0cc4857..6e864f5 100644 --- a/src/exstruct/mcp/tools.py +++ b/src/exstruct/mcp/tools.py @@ -24,6 +24,7 @@ from .io import PathPolicy from .patch_runner import ( PatchDiffItem, + PatchErrorDetail, PatchOp, PatchRequest, PatchResult, @@ -97,6 +98,7 @@ class PatchToolInput(BaseModel): out_dir: str | None = None out_name: str | None = None on_conflict: OnConflictPolicy | None = None + auto_formula: bool = False class PatchToolOutput(BaseModel): @@ -105,6 +107,7 @@ class PatchToolOutput(BaseModel): out_path: str patch_diff: list[PatchDiffItem] = Field(default_factory=list) warnings: list[str] = Field(default_factory=list) + error: PatchErrorDetail | None = None def run_extract_tool( @@ -197,6 +200,7 @@ def run_patch_tool( out_dir=Path(payload.out_dir) if payload.out_dir else None, out_name=payload.out_name, on_conflict=payload.on_conflict or on_conflict or "rename", + auto_formula=payload.auto_formula, ) result = run_patch(request, policy=policy) return _to_patch_tool_output(result) @@ -268,4 +272,5 @@ def _to_patch_tool_output(result: PatchResult) -> PatchToolOutput: out_path=result.out_path, patch_diff=result.patch_diff, warnings=result.warnings, + error=result.error, ) diff --git a/tests/com/test_charts_extraction.py b/tests/com/test_charts_extraction.py index f83431c..b89486b 100644 --- a/tests/com/test_charts_extraction.py +++ b/tests/com/test_charts_extraction.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from contextlib import contextmanager from pathlib import Path import pytest @@ -8,52 +11,54 @@ pytestmark = pytest.mark.com -def _ensure_excel() -> None: - try: - app = xw.App(add_book=False, visible=False) - app.quit() - except Exception: - pytest.skip("Excel COM is unavailable; skipping chart extraction tests.") - - -def _make_workbook_with_chart(path: Path) -> None: +@contextmanager +def _excel_app() -> xw.App: app = xw.App(add_book=False, visible=False) try: - wb = app.books.add() - sht = wb.sheets[0] - sht.name = "Sheet1" - sht.range("A1").value = ["Month", "Sales"] - sht.range("A2").value = [ - ["Jan", 100], - ["Feb", 120], - ["Mar", 150], - ["Apr", 180], - ] - - chart = sht.charts.add(left=200, top=50, width=300, height=200) - chart.chart_type = "column_clustered" - chart.set_source_data(sht.range("A1:B5")) - chart_name = chart.name - chart_com = sht.api.ChartObjects(chart_name).Chart - chart_com.HasTitle = True - chart_com.ChartTitle.Text = "Sales Chart" - y_axis = chart_com.Axes(2, 1) - y_axis.HasTitle = True - y_axis.AxisTitle.Text = "Amount" - y_axis.MinimumScale = 0 - y_axis.MaximumScale = 200 - - wb.save(str(path)) - wb.close() + yield app finally: try: app.quit() except Exception: - pass + try: + app.kill() + except Exception: + pass -def test_チャートの基本メタ情報が抽出される(tmp_path: Path) -> None: - _ensure_excel() +def _make_workbook_with_chart(path: Path) -> None: + with _excel_app() as app: + wb = app.books.add() + try: + sht = wb.sheets[0] + sht.name = "Sheet1" + sht.range("A1").value = ["Month", "Sales"] + sht.range("A2").value = [ + ["Jan", 100], + ["Feb", 120], + ["Mar", 150], + ["Apr", 180], + ] + + chart = sht.charts.add(left=200, top=50, width=300, height=200) + chart.chart_type = "column_clustered" + chart.set_source_data(sht.range("A1:B5")) + chart_name = chart.name + chart_com = sht.api.ChartObjects(chart_name).Chart + chart_com.HasTitle = True + chart_com.ChartTitle.Text = "Sales Chart" + y_axis = chart_com.Axes(2, 1) + y_axis.HasTitle = True + y_axis.AxisTitle.Text = "Amount" + y_axis.MinimumScale = 0 + y_axis.MaximumScale = 200 + + wb.save(str(path)) + finally: + wb.close() + + +def test_chart_basic_metadata(tmp_path: Path) -> None: path = tmp_path / "chart.xlsx" _make_workbook_with_chart(path) @@ -69,8 +74,7 @@ def test_チャートの基本メタ情報が抽出される(tmp_path: Path) -> assert ch.error is None -def test_系列情報が参照式として抽出される(tmp_path: Path) -> None: - _ensure_excel() +def test_chart_series_ranges(tmp_path: Path) -> None: path = tmp_path / "chart_series.xlsx" _make_workbook_with_chart(path) @@ -83,8 +87,7 @@ def test_系列情報が参照式として抽出される(tmp_path: Path) -> Non assert s.y_range is None or s.y_range.endswith("Sheet1!$B$2:$B$5") -def test_verboseでチャートのサイズが取得される(tmp_path: Path) -> None: - _ensure_excel() +def test_chart_verbose_size(tmp_path: Path) -> None: path = tmp_path / "chart_verbose.xlsx" _make_workbook_with_chart(path) diff --git a/tests/com/test_shapes_extraction.py b/tests/com/test_shapes_extraction.py index a9b7a98..5ecd3db 100644 --- a/tests/com/test_shapes_extraction.py +++ b/tests/com/test_shapes_extraction.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +from contextlib import contextmanager from pathlib import Path import pytest @@ -9,61 +12,64 @@ pytestmark = pytest.mark.com -def _ensure_excel() -> None: - try: - app = xw.App(add_book=False, visible=False) - app.quit() - except Exception: - pytest.skip("Excel COM is unavailable; skipping shapes extraction tests.") - - -def _make_workbook_with_shapes(path: Path) -> None: +@contextmanager +def _excel_app() -> xw.App: app = xw.App(add_book=False, visible=False) try: - wb = app.books.add() - sht = wb.sheets[0] - sht.name = "Sheet1" - - rect = sht.api.Shapes.AddShape(1, 50, 50, 120, 60) # msoShapeRectangle - rect.TextFrame2.TextRange.Text = "rect" - - _ = sht.api.Shapes.AddShape(5, 300, 50, 80, 40) # msoShapeOval (no text) - - line = sht.api.Shapes.AddLine(10, 10, 110, 10) - line.Line.EndArrowheadStyle = 3 # msoArrowheadTriangle - - outer = sht.api.Shapes.AddShape(1, 200, 200, 150, 100) - inner = sht.api.Shapes.AddShape(1, 230, 230, 80, 40) - inner.TextFrame2.TextRange.Text = "inner" - sht.api.Shapes.Range([outer.Name, inner.Name]).Group() - - # Add two rectangles and a connector that explicitly connects them - # so that ConnectorFormat.BeginConnectedShape / EndConnectedShape - # are populated by Excel. - src_shape = sht.api.Shapes.AddShape(1, 50, 150, 80, 40) - src_shape.TextFrame2.TextRange.Text = "src" - dst_shape = sht.api.Shapes.AddShape(1, 200, 150, 80, 40) - dst_shape.TextFrame2.TextRange.Text = "dst" - connector = sht.api.Shapes.AddConnector(1, 90, 170, 200, 170) - connector.Line.EndArrowheadStyle = 3 - try: - connector.ConnectorFormat.BeginConnect(src_shape, 1) - connector.ConnectorFormat.EndConnect(dst_shape, 1) - except Exception: - # In some environments connector wiring may fail; tests will - # simply not find connected shapes in that case. - pass - - wb.save(str(path)) - wb.close() + yield app finally: try: app.quit() except Exception: - pass + try: + app.kill() + except Exception: + pass -def test_図形の種別とテキストが抽出される(tmp_path: Path) -> None: +def _make_workbook_with_shapes(path: Path) -> None: + with _excel_app() as app: + wb = app.books.add() + try: + sht = wb.sheets[0] + sht.name = "Sheet1" + + rect = sht.api.Shapes.AddShape(1, 50, 50, 120, 60) # msoShapeRectangle + rect.TextFrame2.TextRange.Text = "rect" + + _ = sht.api.Shapes.AddShape(5, 300, 50, 80, 40) # msoShapeOval (no text) + + line = sht.api.Shapes.AddLine(10, 10, 110, 10) + line.Line.EndArrowheadStyle = 3 # msoArrowheadTriangle + + outer = sht.api.Shapes.AddShape(1, 200, 200, 150, 100) + inner = sht.api.Shapes.AddShape(1, 230, 230, 80, 40) + inner.TextFrame2.TextRange.Text = "inner" + sht.api.Shapes.Range([outer.Name, inner.Name]).Group() + + # Add two rectangles and a connector that explicitly connects them + # so that ConnectorFormat.BeginConnectedShape / EndConnectedShape + # are populated by Excel. + src_shape = sht.api.Shapes.AddShape(1, 50, 150, 80, 40) + src_shape.TextFrame2.TextRange.Text = "src" + dst_shape = sht.api.Shapes.AddShape(1, 200, 150, 80, 40) + dst_shape.TextFrame2.TextRange.Text = "dst" + connector = sht.api.Shapes.AddConnector(1, 90, 170, 200, 170) + connector.Line.EndArrowheadStyle = 3 + try: + connector.ConnectorFormat.BeginConnect(src_shape, 1) + connector.ConnectorFormat.EndConnect(dst_shape, 1) + except Exception: + # In some environments connector wiring may fail; tests will + # simply not find connected shapes in that case. + pass + + wb.save(str(path)) + finally: + wb.close() + + +def test_shapes_basic(tmp_path: Path) -> None: """ Verifies extraction of shape types, texts, IDs, and uniqueness from a workbook containing various shapes. @@ -73,7 +79,6 @@ def test_図形の種別とテキストが抽出される(tmp_path: Path) -> Non - all emitted shape ids are unique; - no AutoShape without text is emitted in standard mode. """ - _ensure_excel() path = tmp_path / "shapes.xlsx" _make_workbook_with_shapes(path) @@ -100,13 +105,12 @@ def test_図形の種別とテキストが抽出される(tmp_path: Path) -> Non ) -def test_線図形の方向と矢印情報が抽出される(tmp_path: Path) -> None: +def test_line_direction(tmp_path: Path) -> None: """ Verifies that a line shape's direction and arrow style information is extracted correctly from a workbook. Creates a workbook containing shapes, extracts shapes from "Sheet1", finds an Arrow with a begin or end arrow style, and asserts its direction is "E". """ - _ensure_excel() path = tmp_path / "lines.xlsx" _make_workbook_with_shapes(path) @@ -122,8 +126,7 @@ def test_線図形の方向と矢印情報が抽出される(tmp_path: Path) -> assert line.direction == "E" -def test_コネクターの接続元と接続先が抽出される(tmp_path: Path) -> None: - _ensure_excel() +def test_connector_connections(tmp_path: Path) -> None: path = tmp_path / "connectors.xlsx" _make_workbook_with_shapes(path) diff --git a/tests/conftest.py b/tests/conftest.py index eab10f2..29976fa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,6 +15,9 @@ def pytest_configure(config: pytest.Config) -> None: """Register custom markers to avoid pytest warnings.""" + markexpr = getattr(config.option, "markexpr", "") or "" + if "com" in markexpr: + os.environ.pop("SKIP_COM_TESTS", None) config.addinivalue_line("markers", "com: requires Excel COM (Windows + Excel).") config.addinivalue_line( "markers", @@ -41,6 +44,12 @@ def _has_pdfium() -> bool: return importlib.util.find_spec("pypdfium2") is not None +@lru_cache(maxsize=1) +def _has_pillow() -> bool: + """Return True if Pillow (PIL) is importable.""" + return importlib.util.find_spec("PIL") is not None + + def _com_skip_reason() -> str | None: """ Return a skip reason for COM-marked tests, or None when they should run. @@ -71,6 +80,8 @@ def _render_skip_reason() -> str | None: return reason if not _has_pdfium(): return "pypdfium2 is unavailable." + if not _has_pillow(): + return "Pillow (PIL) is unavailable." return None @@ -85,3 +96,23 @@ def pytest_runtest_setup(item: pytest.Item) -> None: reason = _com_skip_reason() if reason: pytest.skip(reason) + + +@pytest.fixture(autouse=True) # type: ignore[misc] +def _skip_com_for_non_com_tests( + request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch +) -> None: + """Disable COM usage for tests that are not marked as COM/render.""" + node_path = str(request.node.path) + markexpr = getattr(request.config.option, "markexpr", "") or "" + if "com" in markexpr: + monkeypatch.delenv("SKIP_COM_TESTS", raising=False) + return + if "tests\\com\\" in node_path or "tests/com/" in node_path: + monkeypatch.delenv("SKIP_COM_TESTS", raising=False) + return + if request.node.get_closest_marker("render") is not None: + return + if request.node.get_closest_marker("com") is not None: + return + monkeypatch.setenv("SKIP_COM_TESTS", "1") diff --git a/tests/mcp/test_patch_runner.py b/tests/mcp/test_patch_runner.py index 13fc5b1..1714f80 100644 --- a/tests/mcp/test_patch_runner.py +++ b/tests/mcp/test_patch_runner.py @@ -85,13 +85,14 @@ def test_run_patch_add_sheet_rejects_duplicate( _create_workbook(input_path) ops = [PatchOp(op="add_sheet", sheet="Sheet1")] request = PatchRequest(xlsx_path=input_path, ops=ops, on_conflict="rename") - with pytest.raises(ValueError): - run_patch(request, policy=PathPolicy(root=tmp_path)) + result = run_patch(request, policy=PathPolicy(root=tmp_path)) + assert result.error is not None + assert result.error.op_index == 0 -def test_run_patch_rejects_value_starting_with_equal() -> None: - with pytest.raises(ValidationError): - PatchOp(op="set_value", sheet="Sheet1", cell="A1", value="=SUM(1,1)") +def test_patch_op_allows_value_starting_with_equal() -> None: + op = PatchOp(op="set_value", sheet="Sheet1", cell="A1", value="=SUM(1,1)") + assert op.value == "=SUM(1,1)" def test_run_patch_rejects_formula_without_equal() -> None: @@ -99,6 +100,40 @@ def test_run_patch_rejects_formula_without_equal() -> None: PatchOp(op="set_formula", sheet="Sheet1", cell="A1", formula="SUM(1,1)") +def test_run_patch_set_value_with_equal_requires_auto_formula( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _disable_com(monkeypatch) + input_path = tmp_path / "book.xlsx" + _create_workbook(input_path) + ops = [PatchOp(op="set_value", sheet="Sheet1", cell="A1", value="=SUM(1,1)")] + request = PatchRequest(xlsx_path=input_path, ops=ops, on_conflict="rename") + result = run_patch(request, policy=PathPolicy(root=tmp_path)) + assert result.error is not None + assert "rejects values starting with" in result.error.message + + +def test_run_patch_set_value_with_equal_auto_formula( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _disable_com(monkeypatch) + input_path = tmp_path / "book.xlsx" + _create_workbook(input_path) + ops = [PatchOp(op="set_value", sheet="Sheet1", cell="A1", value="=SUM(1,1)")] + request = PatchRequest( + xlsx_path=input_path, ops=ops, on_conflict="rename", auto_formula=True + ) + result = run_patch(request, policy=PathPolicy(root=tmp_path)) + workbook = load_workbook(result.out_path) + try: + formula_value = workbook["Sheet1"]["A1"].value + if isinstance(formula_value, str) and not formula_value.startswith("="): + formula_value = f"={formula_value}" + assert formula_value == "=SUM(1,1)" + finally: + workbook.close() + + def test_run_patch_rejects_path_outside_root( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -173,6 +208,6 @@ def test_run_patch_atomicity(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> ] request = PatchRequest(xlsx_path=input_path, ops=ops, on_conflict="rename") output_path = tmp_path / "book_patched.xlsx" - with pytest.raises(ValueError): - run_patch(request, policy=PathPolicy(root=tmp_path)) + result = run_patch(request, policy=PathPolicy(root=tmp_path)) + assert result.error is not None assert not output_path.exists() From 18ed53fa3c1c4f8f9f1cfd83eefb156f6484f2c3 Mon Sep 17 00:00:00 2001 From: harumiWeb Date: Fri, 6 Feb 2026 21:51:16 +0900 Subject: [PATCH 04/25] feat: Update FEATURE_SPEC and TASKS for review requirements and clarify output handling --- docs/agents/FEATURE_SPEC.md | 38 +++++++++++++++++++++++++++++++++++++ docs/agents/TASKS.md | 11 +++++++++++ pyproject.toml | 2 +- 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/docs/agents/FEATURE_SPEC.md b/docs/agents/FEATURE_SPEC.md index c82da98..314e258 100644 --- a/docs/agents/FEATURE_SPEC.md +++ b/docs/agents/FEATURE_SPEC.md @@ -170,3 +170,41 @@ ] } ``` + +--- + +## 11. レビュー対応仕様(MVP修正) + +### 11.1 `on_conflict` 既定値の統一 + +- サーバーCLI引数 `--on-conflict` の既定値・指定値は、`exstruct_extract` と `exstruct_patch` の双方に同一に適用する +- `exstruct_patch` で固定値 `rename` を持たない +- ツール呼び出しで `on_conflict` が未指定の場合: + - サーバー起動時の `--on-conflict` を適用 +- ツール呼び出しで `on_conflict` が指定された場合: + - ツール引数を最優先する + +### 11.2 出力ディレクトリ生成の明確化 + +- `exstruct_patch` は保存前に `out_path.parent.mkdir(parents=True, exist_ok=True)` 相当を実行する +- `out_dir` が存在しない場合でも保存可能にする + +### 11.3 `.xls` サポート条件の明確化 + +- `.xls` は **COM利用可能環境のみ編集対象** とする +- COM利用不可時の `.xls` は `ValueError` を返す(メッセージで「COM必須」を明示) +- バリデーション規則の拡張子記述を次に更新する: + - `.xlsx` / `.xlsm`: 常時対象 + - `.xls`: 条件付き対象(Windows + COM) + +### 11.4 後方互換性 + +- 既存の `PatchOp` / `PatchResult` スキーマは変更しない +- 変更はデフォルト解決順序・保存前処理・エラーメッセージの明確化に限定する + +### 11.5 テスト要件(レビュー対応分) + +- `exstruct_patch` で `on_conflict` 未指定時にサーバー既定値が反映されること +- `exstruct_patch` で `on_conflict` 指定時にツール引数が優先されること +- 未作成 `out_dir` 指定で保存成功すること +- `.xls` + COM不可時に期待どおり `ValueError` となること(メッセージ確認を含む) diff --git a/docs/agents/TASKS.md b/docs/agents/TASKS.md index 9fb7b6e..fa2b3d4 100644 --- a/docs/agents/TASKS.md +++ b/docs/agents/TASKS.md @@ -10,3 +10,14 @@ - [x] openpyxlベースの適用処理を実装 - [x] COM利用時の分岐(可能なら)とwarning出力 - [x] ユニットテスト(非COM)を追加 + +## レビュー対応タスク(MVP修正) + +- [ ] `mcp/server.py`: `exstruct_patch` の `on_conflict` 解決で固定 `rename` を廃止し、サーバー既定値(`default_on_conflict`)を利用する +- [ ] `mcp/patch_runner.py`: 保存前に出力先ディレクトリ作成処理を追加する(未作成 `out_dir` 対応) +- [ ] `mcp/patch_runner.py`: `.xls` + COM不可時のエラーメッセージを「COM必須」が明確な文言に調整する +- [ ] `docs/agents/FEATURE_SPEC.md`: 拡張子要件を「`.xls` はCOM利用可能環境のみ」に更新する(本文整合) +- [ ] `tests/mcp/test_server.py`: patch ツールの `on_conflict` 既定値伝播を検証するテストを追加/更新する +- [ ] `tests/mcp/test_patch_runner.py`: 未作成 `out_dir` 保存成功ケースを追加する +- [ ] `tests/mcp/test_patch_runner.py`: `.xls` + COM不可エラーの期待値(型・文言)を追加する +- [ ] `ruff check .` / `mypy src/exstruct --strict` / 関連pytest を実行し、結果を記録する diff --git a/pyproject.toml b/pyproject.toml index 128e8ed..5dbe8f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "exstruct" -version = "0.4.2" +version = "0.4.3" description = "Excel to structured JSON (tables, shapes, charts) for LLM/RAG pipelines" readme = "README.md" license = { file = "LICENSE" } From 04c9699f1565090d33eb69b72ace48708d165797 Mon Sep 17 00:00:00 2001 From: harumiWeb Date: Fri, 6 Feb 2026 21:55:31 +0900 Subject: [PATCH 05/25] feat: Update FEATURE_SPEC and TASKS for extension requirements; enhance patch functionality with output directory creation and error handling --- docs/agents/FEATURE_SPEC.md | 4 +-- docs/agents/TASKS.md | 16 ++++----- src/exstruct/mcp/patch_runner.py | 11 ++++++ src/exstruct/mcp/server.py | 3 +- tests/mcp/test_patch_runner.py | 32 +++++++++++++++++ tests/mcp/test_server.py | 60 ++++++++++++++++++++++++++++++++ 6 files changed, 114 insertions(+), 12 deletions(-) diff --git a/docs/agents/FEATURE_SPEC.md b/docs/agents/FEATURE_SPEC.md index 314e258..dcdbddc 100644 --- a/docs/agents/FEATURE_SPEC.md +++ b/docs/agents/FEATURE_SPEC.md @@ -58,7 +58,7 @@ - `xlsx_path` - 存在するファイルであること - 許可パス内であること(PathPolicy) - - 拡張子は `.xlsx` / `.xlsm` / `.xls` + - 拡張子は `.xlsx` / `.xlsm` / `.xls`(`.xls` は COM 利用可能環境のみ) - `sheet` - 空文字禁止 - `add_sheet` は既存名と重複禁止 @@ -119,7 +119,7 @@ ## 8. 競合ポリシー -- `on_conflict` 未指定時は **rename** を既定値とする +- `on_conflict` 未指定時は **サーバー起動時の `--on-conflict` 設定値** を既定値とする - `skip` の場合: - `patch_diff` は空配列 - `warnings` にスキップ理由を記載 diff --git a/docs/agents/TASKS.md b/docs/agents/TASKS.md index fa2b3d4..12a0cb3 100644 --- a/docs/agents/TASKS.md +++ b/docs/agents/TASKS.md @@ -13,11 +13,11 @@ ## レビュー対応タスク(MVP修正) -- [ ] `mcp/server.py`: `exstruct_patch` の `on_conflict` 解決で固定 `rename` を廃止し、サーバー既定値(`default_on_conflict`)を利用する -- [ ] `mcp/patch_runner.py`: 保存前に出力先ディレクトリ作成処理を追加する(未作成 `out_dir` 対応) -- [ ] `mcp/patch_runner.py`: `.xls` + COM不可時のエラーメッセージを「COM必須」が明確な文言に調整する -- [ ] `docs/agents/FEATURE_SPEC.md`: 拡張子要件を「`.xls` はCOM利用可能環境のみ」に更新する(本文整合) -- [ ] `tests/mcp/test_server.py`: patch ツールの `on_conflict` 既定値伝播を検証するテストを追加/更新する -- [ ] `tests/mcp/test_patch_runner.py`: 未作成 `out_dir` 保存成功ケースを追加する -- [ ] `tests/mcp/test_patch_runner.py`: `.xls` + COM不可エラーの期待値(型・文言)を追加する -- [ ] `ruff check .` / `mypy src/exstruct --strict` / 関連pytest を実行し、結果を記録する +- [x] `mcp/server.py`: `exstruct_patch` の `on_conflict` 解決で固定 `rename` を廃止し、サーバー既定値(`default_on_conflict`)を利用する +- [x] `mcp/patch_runner.py`: 保存前に出力先ディレクトリ作成処理を追加する(未作成 `out_dir` 対応) +- [x] `mcp/patch_runner.py`: `.xls` + COM不可時のエラーメッセージを「COM必須」が明確な文言に調整する +- [x] `docs/agents/FEATURE_SPEC.md`: 拡張子要件を「`.xls` はCOM利用可能環境のみ」に更新する(本文整合) +- [x] `tests/mcp/test_server.py`: patch ツールの `on_conflict` 既定値伝播を検証するテストを追加/更新する +- [x] `tests/mcp/test_patch_runner.py`: 未作成 `out_dir` 保存成功ケースを追加する +- [x] `tests/mcp/test_patch_runner.py`: `.xls` + COM不可エラーの期待値(型・文言)を追加する +- [x] `ruff check .` / `mypy src/exstruct --strict` / 関連pytest を実行し、結果を記録する diff --git a/src/exstruct/mcp/patch_runner.py b/src/exstruct/mcp/patch_runner.py index fec9474..608fd0f 100644 --- a/src/exstruct/mcp/patch_runner.py +++ b/src/exstruct/mcp/patch_runner.py @@ -252,6 +252,12 @@ def run_patch( return PatchResult(out_path=str(output_path), patch_diff=[], warnings=warnings) com = get_com_availability() + if resolved_input.suffix.lower() == ".xls" and not com.available: + raise ValueError( + ".xls editing requires Windows Excel COM (xlwings) in this environment." + ) + + _ensure_output_dir(output_path) if com.available: try: diff = _apply_ops_xlwings( @@ -393,6 +399,11 @@ def _normalize_output_name(input_path: Path, out_name: str | None) -> str: return f"{input_path.stem}_patched{input_path.suffix}" +def _ensure_output_dir(path: Path) -> None: + """Ensure the output directory exists before writing.""" + path.parent.mkdir(parents=True, exist_ok=True) + + def _apply_conflict_policy( output_path: Path, on_conflict: OnConflictPolicy ) -> tuple[Path, str | None, bool]: diff --git a/src/exstruct/mcp/server.py b/src/exstruct/mcp/server.py index 46d7a90..971434c 100644 --- a/src/exstruct/mcp/server.py +++ b/src/exstruct/mcp/server.py @@ -197,7 +197,6 @@ def _register_tools( app: FastMCP application instance. policy: Path policy for filesystem access. """ - patch_default_on_conflict: OnConflictPolicy = "rename" async def _extract_tool( # pylint: disable=redefined-builtin xlsx_path: str, @@ -329,7 +328,7 @@ async def _patch_tool( on_conflict=on_conflict, auto_formula=auto_formula, ) - effective_on_conflict = on_conflict or patch_default_on_conflict + effective_on_conflict = on_conflict or default_on_conflict work = functools.partial( run_patch_tool, payload, diff --git a/tests/mcp/test_patch_runner.py b/tests/mcp/test_patch_runner.py index 1714f80..4563326 100644 --- a/tests/mcp/test_patch_runner.py +++ b/tests/mcp/test_patch_runner.py @@ -211,3 +211,35 @@ def test_run_patch_atomicity(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> result = run_patch(request, policy=PathPolicy(root=tmp_path)) assert result.error is not None assert not output_path.exists() + + +def test_run_patch_creates_out_dir( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _disable_com(monkeypatch) + input_path = tmp_path / "book.xlsx" + _create_workbook(input_path) + out_dir = tmp_path / "nested" / "output" + ops = [PatchOp(op="set_value", sheet="Sheet1", cell="A1", value="x")] + request = PatchRequest( + xlsx_path=input_path, + ops=ops, + out_dir=out_dir, + on_conflict="rename", + ) + result = run_patch(request, policy=PathPolicy(root=tmp_path)) + out_path = Path(result.out_path) + assert out_path.exists() + assert out_path.parent == out_dir + + +def test_run_patch_xls_requires_com( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _disable_com(monkeypatch) + input_path = tmp_path / "book.xls" + input_path.write_text("dummy", encoding="utf-8") + ops = [PatchOp(op="add_sheet", sheet="Sheet2")] + request = PatchRequest(xlsx_path=input_path, ops=ops, on_conflict="rename") + with pytest.raises(ValueError, match=r"requires Windows Excel COM"): + run_patch(request, policy=PathPolicy(root=tmp_path)) diff --git a/tests/mcp/test_server.py b/tests/mcp/test_server.py index 993dcbd..bc85a23 100644 --- a/tests/mcp/test_server.py +++ b/tests/mcp/test_server.py @@ -177,6 +177,66 @@ async def fake_run_sync(func: Callable[[], object]) -> object: assert calls["patch"][2] == "rename" +def test_register_tools_passes_patch_default_on_conflict( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + app = DummyApp() + policy = PathPolicy(root=tmp_path) + calls: dict[str, tuple[object, ...]] = {} + + def fake_run_extract_tool( + payload: ExtractToolInput, + *, + policy: PathPolicy, + on_conflict: OnConflictPolicy, + ) -> ExtractToolOutput: + return ExtractToolOutput(out_path="out.json") + + def fake_run_read_json_chunk_tool( + payload: ReadJsonChunkToolInput, + *, + policy: PathPolicy, + ) -> ReadJsonChunkToolOutput: + return ReadJsonChunkToolOutput(chunk="{}") + + def fake_run_validate_input_tool( + payload: ValidateInputToolInput, + *, + policy: PathPolicy, + ) -> ValidateInputToolOutput: + return ValidateInputToolOutput(is_readable=True) + + def fake_run_patch_tool( + payload: PatchToolInput, + *, + policy: PathPolicy, + on_conflict: OnConflictPolicy, + ) -> PatchToolOutput: + calls["patch"] = (payload, policy, on_conflict) + return PatchToolOutput(out_path="out.xlsx", patch_diff=[]) + + async def fake_run_sync(func: Callable[[], object]) -> object: + return func() + + monkeypatch.setattr(server, "run_extract_tool", fake_run_extract_tool) + monkeypatch.setattr( + server, "run_read_json_chunk_tool", fake_run_read_json_chunk_tool + ) + monkeypatch.setattr(server, "run_validate_input_tool", fake_run_validate_input_tool) + monkeypatch.setattr(server, "run_patch_tool", fake_run_patch_tool) + monkeypatch.setattr(anyio.to_thread, "run_sync", fake_run_sync) + + server._register_tools(app, policy, default_on_conflict="overwrite") + patch_tool = cast(Callable[..., Awaitable[object]], app.tools["exstruct_patch"]) + anyio.run( + _call_async, + patch_tool, + {"xlsx_path": "in.xlsx", "ops": [{"op": "add_sheet", "sheet": "New"}]}, + ) + + assert calls["patch"][2] == "overwrite" + + def test_run_server_sets_env(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: created: dict[str, object] = {} From 5e35dd247504bcdb4fa2d8f706ffa9f98efc8f04 Mon Sep 17 00:00:00 2001 From: harumiWeb Date: Fri, 6 Feb 2026 22:05:46 +0900 Subject: [PATCH 06/25] update --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index 039196c..6fbb501 100644 --- a/uv.lock +++ b/uv.lock @@ -620,7 +620,7 @@ wheels = [ [[package]] name = "exstruct" -version = "0.4.2" +version = "0.4.3" source = { editable = "." } dependencies = [ { name = "numpy" }, From 0ae7df50c4eaabb442c3ddcd74af47c7e9941b39 Mon Sep 17 00:00:00 2001 From: harumiWeb Date: Fri, 6 Feb 2026 22:43:00 +0900 Subject: [PATCH 07/25] feat: Update FEATURE_SPEC and TASKS for next phase features; add specifications for dry_run, inverse_ops, and formula checks --- docs/agents/FEATURE_SPEC.md | 106 ++++++++++++++++++++++++++++++++++++ docs/agents/TASKS.md | 21 +++++++ 2 files changed, 127 insertions(+) diff --git a/docs/agents/FEATURE_SPEC.md b/docs/agents/FEATURE_SPEC.md index dcdbddc..19e7117 100644 --- a/docs/agents/FEATURE_SPEC.md +++ b/docs/agents/FEATURE_SPEC.md @@ -208,3 +208,109 @@ - `exstruct_patch` で `on_conflict` 指定時にツール引数が優先されること - 未作成 `out_dir` 指定で保存成功すること - `.xls` + COM不可時に期待どおり `ValueError` となること(メッセージ確認を含む) + +--- + +## 12. 次フェーズ仕様(便利機能 1〜5) + +### 12.1 対象機能 + +- `dry_run`(非破壊プレビュー) +- `undo` 用逆パッチ生成 +- 範囲操作(値一括設定 / 数式フィル) +- 条件付き更新(CAS) +- 数式ヘルスチェック + +### 12.2 ツールI/F拡張 + +`exstruct_patch` の Input に以下を追加: + +- `dry_run: bool = false` +- `return_inverse_ops: bool = false` +- `preflight_formula_check: bool = false` + +`exstruct_patch` の Output に以下を追加: + +- `inverse_ops: list[PatchOp]`(default: `[]`) +- `formula_issues: list[FormulaIssue]`(default: `[]`) + +`FormulaIssue`: + +- `sheet: str` +- `cell: str` +- `level: "warning" | "error"` +- `code: "invalid_token" | "ref_error" | "name_error" | "div0_error" | "value_error" | "na_error" | "circular_ref_suspected"` +- `message: str` + +### 12.3 操作種別拡張(PatchOp) + +#### set_range_values + +- `op: "set_range_values"` +- `sheet: str` +- `range: str`(A1範囲形式、例: `B2:D4`) +- `values: list[list[str | int | float | None]]` +- `shape(values)` と `range` サイズが一致しない場合は `ValueError` + +#### fill_formula + +- `op: "fill_formula"` +- `sheet: str` +- `range: str`(縦または横の連続範囲) +- `base_cell: str` +- `formula: str`(`=` 始まり) +- `formula` を `base_cell` に置いたときの相対参照を、`range` 内に展開する + +#### set_value_if + +- `op: "set_value_if"` +- `sheet: str` +- `cell: str` +- `expected: str | int | float | None` +- `value: str | int | float | None` +- 現在値が `expected` と一致した場合のみ更新 +- 不一致時は `PatchDiffItem.status="skipped"` とし、`warnings` に理由を追加 + +#### set_formula_if + +- `op: "set_formula_if"` +- `sheet: str` +- `cell: str` +- `expected: str | int | float | None` +- `formula: str`(`=` 始まり) +- 条件判定は `set_value_if` と同様 + +### 12.4 実行セマンティクス(追加) + +- `dry_run=true` の場合: + - 保存しない + - `patch_diff` / `warnings` / `formula_issues` / `inverse_ops` のみ返す + - `out_path` は保存予定パスを返す +- `return_inverse_ops=true` の場合: + - `applied` な変更のみ逆操作を生成する + - 逆操作順は「適用順の逆順」とする +- `preflight_formula_check=true` の場合: + - 適用後ワークブックを走査し `formula_issues` を返す + - `level=error` がある場合の扱い: + - `dry_run=false`: 変更は保存せず `error` を返す + - `dry_run=true`: 保存なしのため `error` は返さず、問題一覧のみ返す + +### 12.5 バリデーション規則(追加) + +- `range` は A1 範囲形式(例: `A1:C3`)必須 +- `fill_formula.range` は単一行または単一列のみ(MVP制約) +- 条件付き更新の `expected` 比較は「内部正規化後の値一致」で判定 +- `set_value_if` で `value` が `=` 始まりかつ `auto_formula=false` の場合は既存 `set_value` と同様に拒否 + +### 12.6 後方互換性 + +- 既存 `set_value` / `set_formula` / `add_sheet` の仕様は変更しない +- 新規フィールドは optional 追加のみで、既存クライアントを破壊しない + +### 12.7 受け入れ基準 + +- `dry_run=true` でファイル更新時刻が変化しない +- `return_inverse_ops=true` で、逆パッチ適用により元状態へ戻せる +- `set_range_values` がサイズ不一致を検知できる +- `set_value_if` / `set_formula_if` が不一致時に `skipped` を返せる +- `preflight_formula_check=true` で `#REF!` 等を検出して `formula_issues` に反映できる diff --git a/docs/agents/TASKS.md b/docs/agents/TASKS.md index 12a0cb3..54ca39b 100644 --- a/docs/agents/TASKS.md +++ b/docs/agents/TASKS.md @@ -21,3 +21,24 @@ - [x] `tests/mcp/test_patch_runner.py`: 未作成 `out_dir` 保存成功ケースを追加する - [x] `tests/mcp/test_patch_runner.py`: `.xls` + COM不可エラーの期待値(型・文言)を追加する - [x] `ruff check .` / `mypy src/exstruct --strict` / 関連pytest を実行し、結果を記録する + +## 次フェーズタスク(便利機能 1〜5) + +- [ ] `mcp/patch_runner.py`: `PatchRequest` に `dry_run` / `return_inverse_ops` / `preflight_formula_check` を追加する +- [ ] `mcp/patch_runner.py`: `PatchResult` に `inverse_ops` / `formula_issues` を追加する +- [ ] `mcp/patch_runner.py`: `PatchOp` に `set_range_values` / `fill_formula` / `set_value_if` / `set_formula_if` を追加する +- [ ] `mcp/patch_runner.py`: A1範囲パース・範囲サイズ検証・値行列検証ユーティリティを実装する +- [ ] `mcp/patch_runner.py`: `set_range_values` の openpyxl 実装を追加する +- [ ] `mcp/patch_runner.py`: `fill_formula` の相対参照展開ロジックを実装する(単一行/列制約) +- [ ] `mcp/patch_runner.py`: `set_value_if` / `set_formula_if` の比較・`skipped` 差分出力を実装する +- [ ] `mcp/patch_runner.py`: 逆パッチ生成ロジック(`applied` のみ、逆順)を実装する +- [ ] `mcp/patch_runner.py`: `dry_run=true` で非保存実行に分岐する +- [ ] `mcp/patch_runner.py`: 数式ヘルスチェック(`#REF!` 等)を実装し、`formula_issues` に反映する +- [ ] `mcp/patch_runner.py`: `preflight_formula_check=true` かつ `level=error` の保存抑止ルールを実装する +- [ ] `mcp/tools.py`: `PatchToolInput/Output` の新規フィールドをツールI/Fへ公開する +- [ ] `mcp/server.py`: `exstruct_patch` ツール引数に新規3フラグを追加し、ハンドラへ連携する +- [ ] `docs/agents/FEATURE_SPEC.md`: 実装差分(命名、エラーコード、制約)を最終反映する +- [ ] `tests/mcp/test_patch_runner.py`: `dry_run` 非保存・逆パッチ・範囲操作・条件付き更新・数式検査の単体テストを追加する +- [ ] `tests/mcp/test_tools_handlers.py`: 入出力モデル拡張の受け渡しテストを追加する +- [ ] `tests/mcp/test_server.py`: 新規ツール引数の受け渡しテストを追加する +- [ ] `ruff check .` / `mypy src/exstruct --strict` / 関連pytest を実行し、結果を記録する From c0cce6e476600830cc02a28b21dd998743e2f1de Mon Sep 17 00:00:00 2001 From: harumiWeb Date: Fri, 6 Feb 2026 22:59:37 +0900 Subject: [PATCH 08/25] feat: enhance patch operations with new features and validations - Added new patch operations: set_range_values, fill_formula, set_value_if, and set_formula_if. - Introduced FormulaIssue model for tracking formula-related issues during patching. - Implemented preflight formula checks to validate formulas before applying patches. - Enhanced PatchOp model to include new fields for range and base_cell. - Updated run_patch function to support dry_run and return_inverse_ops options. - Added tests for new operations and validation scenarios, ensuring correct behavior and error handling. --- docs/agents/TASKS.md | 36 +- src/exstruct/mcp/__init__.py | 2 + src/exstruct/mcp/patch_runner.py | 633 +++++++++++++++++++++++++++---- src/exstruct/mcp/server.py | 6 + src/exstruct/mcp/tools.py | 11 + tests/mcp/test_patch_runner.py | 170 +++++++++ tests/mcp/test_server.py | 76 ++++ tests/mcp/test_tool_models.py | 3 + tests/mcp/test_tools_handlers.py | 6 + 9 files changed, 858 insertions(+), 85 deletions(-) diff --git a/docs/agents/TASKS.md b/docs/agents/TASKS.md index 54ca39b..b5424f9 100644 --- a/docs/agents/TASKS.md +++ b/docs/agents/TASKS.md @@ -24,21 +24,21 @@ ## 次フェーズタスク(便利機能 1〜5) -- [ ] `mcp/patch_runner.py`: `PatchRequest` に `dry_run` / `return_inverse_ops` / `preflight_formula_check` を追加する -- [ ] `mcp/patch_runner.py`: `PatchResult` に `inverse_ops` / `formula_issues` を追加する -- [ ] `mcp/patch_runner.py`: `PatchOp` に `set_range_values` / `fill_formula` / `set_value_if` / `set_formula_if` を追加する -- [ ] `mcp/patch_runner.py`: A1範囲パース・範囲サイズ検証・値行列検証ユーティリティを実装する -- [ ] `mcp/patch_runner.py`: `set_range_values` の openpyxl 実装を追加する -- [ ] `mcp/patch_runner.py`: `fill_formula` の相対参照展開ロジックを実装する(単一行/列制約) -- [ ] `mcp/patch_runner.py`: `set_value_if` / `set_formula_if` の比較・`skipped` 差分出力を実装する -- [ ] `mcp/patch_runner.py`: 逆パッチ生成ロジック(`applied` のみ、逆順)を実装する -- [ ] `mcp/patch_runner.py`: `dry_run=true` で非保存実行に分岐する -- [ ] `mcp/patch_runner.py`: 数式ヘルスチェック(`#REF!` 等)を実装し、`formula_issues` に反映する -- [ ] `mcp/patch_runner.py`: `preflight_formula_check=true` かつ `level=error` の保存抑止ルールを実装する -- [ ] `mcp/tools.py`: `PatchToolInput/Output` の新規フィールドをツールI/Fへ公開する -- [ ] `mcp/server.py`: `exstruct_patch` ツール引数に新規3フラグを追加し、ハンドラへ連携する -- [ ] `docs/agents/FEATURE_SPEC.md`: 実装差分(命名、エラーコード、制約)を最終反映する -- [ ] `tests/mcp/test_patch_runner.py`: `dry_run` 非保存・逆パッチ・範囲操作・条件付き更新・数式検査の単体テストを追加する -- [ ] `tests/mcp/test_tools_handlers.py`: 入出力モデル拡張の受け渡しテストを追加する -- [ ] `tests/mcp/test_server.py`: 新規ツール引数の受け渡しテストを追加する -- [ ] `ruff check .` / `mypy src/exstruct --strict` / 関連pytest を実行し、結果を記録する +- [x] `mcp/patch_runner.py`: `PatchRequest` に `dry_run` / `return_inverse_ops` / `preflight_formula_check` を追加する +- [x] `mcp/patch_runner.py`: `PatchResult` に `inverse_ops` / `formula_issues` を追加する +- [x] `mcp/patch_runner.py`: `PatchOp` に `set_range_values` / `fill_formula` / `set_value_if` / `set_formula_if` を追加する +- [x] `mcp/patch_runner.py`: A1範囲パース・範囲サイズ検証・値行列検証ユーティリティを実装する +- [x] `mcp/patch_runner.py`: `set_range_values` の openpyxl 実装を追加する +- [x] `mcp/patch_runner.py`: `fill_formula` の相対参照展開ロジックを実装する(単一行/列制約) +- [x] `mcp/patch_runner.py`: `set_value_if` / `set_formula_if` の比較・`skipped` 差分出力を実装する +- [x] `mcp/patch_runner.py`: 逆パッチ生成ロジック(`applied` のみ、逆順)を実装する +- [x] `mcp/patch_runner.py`: `dry_run=true` で非保存実行に分岐する +- [x] `mcp/patch_runner.py`: 数式ヘルスチェック(`#REF!` 等)を実装し、`formula_issues` に反映する +- [x] `mcp/patch_runner.py`: `preflight_formula_check=true` かつ `level=error` の保存抑止ルールを実装する +- [x] `mcp/tools.py`: `PatchToolInput/Output` の新規フィールドをツールI/Fへ公開する +- [x] `mcp/server.py`: `exstruct_patch` ツール引数に新規3フラグを追加し、ハンドラへ連携する +- [x] `docs/agents/FEATURE_SPEC.md`: 実装差分(命名、エラーコード、制約)を最終反映する +- [x] `tests/mcp/test_patch_runner.py`: `dry_run` 非保存・逆パッチ・範囲操作・条件付き更新・数式検査の単体テストを追加する +- [x] `tests/mcp/test_tools_handlers.py`: 入出力モデル拡張の受け渡しテストを追加する +- [x] `tests/mcp/test_server.py`: 新規ツール引数の受け渡しテストを追加する +- [x] `ruff check .` / `mypy src/exstruct --strict` / 関連pytest を実行し、結果を記録する diff --git a/src/exstruct/mcp/__init__.py b/src/exstruct/mcp/__init__.py index 68fd69d..cd37198 100644 --- a/src/exstruct/mcp/__init__.py +++ b/src/exstruct/mcp/__init__.py @@ -17,6 +17,7 @@ ) from .io import PathPolicy from .patch_runner import ( + FormulaIssue, PatchDiffItem, PatchErrorDetail, PatchOp, @@ -51,6 +52,7 @@ "ExtractOptions", "ExtractToolInput", "ExtractToolOutput", + "FormulaIssue", "PatchDiffItem", "PatchErrorDetail", "PatchOp", diff --git a/src/exstruct/mcp/patch_runner.py b/src/exstruct/mcp/patch_runner.py index 608fd0f..dcbdea3 100644 --- a/src/exstruct/mcp/patch_runner.py +++ b/src/exstruct/mcp/patch_runner.py @@ -14,12 +14,31 @@ from .extract_runner import OnConflictPolicy from .io import PathPolicy -PatchOpType = Literal["set_value", "set_formula", "add_sheet"] +PatchOpType = Literal[ + "set_value", + "set_formula", + "add_sheet", + "set_range_values", + "fill_formula", + "set_value_if", + "set_formula_if", +] PatchStatus = Literal["applied", "skipped"] PatchValueKind = Literal["value", "formula", "sheet"] +FormulaIssueLevel = Literal["warning", "error"] +FormulaIssueCode = Literal[ + "invalid_token", + "ref_error", + "name_error", + "div0_error", + "value_error", + "na_error", + "circular_ref_suspected", +] _ALLOWED_EXTENSIONS = {".xlsx", ".xlsm", ".xls"} _A1_PATTERN = re.compile(r"^[A-Za-z]{1,3}[1-9][0-9]*$") +_A1_RANGE_PATTERN = re.compile(r"^[A-Za-z]{1,3}[1-9][0-9]*:[A-Za-z]{1,3}[1-9][0-9]*$") @runtime_checkable @@ -101,7 +120,11 @@ class PatchOp(BaseModel): op: PatchOpType sheet: str cell: str | None = None + range: str | None = None + base_cell: str | None = None + expected: str | int | float | None = None value: str | int | float | None = None + values: list[list[str | int | float | None]] | None = None formula: str | None = None @field_validator("sheet") @@ -121,18 +144,54 @@ def _validate_cell(cls, value: str | None) -> str | None: raise ValueError(f"Invalid cell reference: {value}") return candidate.upper() + @field_validator("base_cell") + @classmethod + def _validate_base_cell(cls, value: str | None) -> str | None: + if value is None: + return None + candidate = value.strip() + if not _A1_PATTERN.match(candidate): + raise ValueError(f"Invalid base_cell reference: {value}") + return candidate.upper() + + @field_validator("range") + @classmethod + def _validate_range(cls, value: str | None) -> str | None: + if value is None: + return None + candidate = value.strip() + if not _A1_RANGE_PATTERN.match(candidate): + raise ValueError(f"Invalid range reference: {value}") + start, end = candidate.split(":", maxsplit=1) + return f"{start.upper()}:{end.upper()}" + @model_validator(mode="after") def _validate_op(self) -> PatchOp: if self.op == "add_sheet": _validate_add_sheet(self) return self - _validate_cell_required(self) if self.op == "set_value": + _validate_cell_required(self) _validate_set_value(self) return self if self.op == "set_formula": + _validate_cell_required(self) _validate_set_formula(self) return self + if self.op == "set_range_values": + _validate_set_range_values(self) + return self + if self.op == "fill_formula": + _validate_fill_formula(self) + return self + if self.op == "set_value_if": + _validate_cell_required(self) + _validate_set_value_if(self) + return self + if self.op == "set_formula_if": + _validate_cell_required(self) + _validate_set_formula_if(self) + return self return self @@ -168,6 +227,77 @@ def _validate_set_formula(op: PatchOp) -> None: raise ValueError("set_formula requires formula starting with '='.") +def _validate_set_range_values(op: PatchOp) -> None: + """Validate set_range_values operation.""" + if op.cell is not None: + raise ValueError("set_range_values does not accept cell.") + if op.base_cell is not None: + raise ValueError("set_range_values does not accept base_cell.") + if op.expected is not None: + raise ValueError("set_range_values does not accept expected.") + if op.formula is not None: + raise ValueError("set_range_values does not accept formula.") + if op.range is None: + raise ValueError("set_range_values requires range.") + if op.values is None: + raise ValueError("set_range_values requires values.") + if not op.values: + raise ValueError("set_range_values requires non-empty values.") + if not all(op.values): + raise ValueError("set_range_values values rows must not be empty.") + expected_width = len(op.values[0]) + if any(len(row) != expected_width for row in op.values): + raise ValueError("set_range_values requires rectangular values.") + + +def _validate_fill_formula(op: PatchOp) -> None: + """Validate fill_formula operation.""" + if op.cell is not None: + raise ValueError("fill_formula does not accept cell.") + if op.expected is not None: + raise ValueError("fill_formula does not accept expected.") + if op.value is not None: + raise ValueError("fill_formula does not accept value.") + if op.values is not None: + raise ValueError("fill_formula does not accept values.") + if op.range is None: + raise ValueError("fill_formula requires range.") + if op.base_cell is None: + raise ValueError("fill_formula requires base_cell.") + if op.formula is None: + raise ValueError("fill_formula requires formula.") + if not op.formula.startswith("="): + raise ValueError("fill_formula requires formula starting with '='.") + + +def _validate_set_value_if(op: PatchOp) -> None: + """Validate set_value_if operation.""" + if op.formula is not None: + raise ValueError("set_value_if does not accept formula.") + if op.range is not None: + raise ValueError("set_value_if does not accept range.") + if op.values is not None: + raise ValueError("set_value_if does not accept values.") + if op.base_cell is not None: + raise ValueError("set_value_if does not accept base_cell.") + + +def _validate_set_formula_if(op: PatchOp) -> None: + """Validate set_formula_if operation.""" + if op.value is not None: + raise ValueError("set_formula_if does not accept value.") + if op.range is not None: + raise ValueError("set_formula_if does not accept range.") + if op.values is not None: + raise ValueError("set_formula_if does not accept values.") + if op.base_cell is not None: + raise ValueError("set_formula_if does not accept base_cell.") + if op.formula is None: + raise ValueError("set_formula_if requires formula.") + if not op.formula.startswith("="): + raise ValueError("set_formula_if requires formula starting with '='.") + + class PatchValue(BaseModel): """Normalized before/after value in patch diff.""" @@ -197,6 +327,16 @@ class PatchErrorDetail(BaseModel): message: str +class FormulaIssue(BaseModel): + """Formula health-check finding.""" + + sheet: str + cell: str + level: FormulaIssueLevel + code: FormulaIssueCode + message: str + + class PatchRequest(BaseModel): """Input model for ExStruct MCP patch.""" @@ -206,6 +346,9 @@ class PatchRequest(BaseModel): out_name: str | None = None on_conflict: OnConflictPolicy = "rename" auto_formula: bool = False + dry_run: bool = False + return_inverse_ops: bool = False + preflight_formula_check: bool = False class PatchResult(BaseModel): @@ -213,6 +356,8 @@ class PatchResult(BaseModel): out_path: str patch_diff: list[PatchDiffItem] = Field(default_factory=list) + inverse_ops: list[PatchOp] = Field(default_factory=list) + formula_issues: list[FormulaIssue] = Field(default_factory=list) warnings: list[str] = Field(default_factory=list) error: PatchErrorDetail | None = None @@ -249,7 +394,13 @@ def run_patch( if warning: warnings.append(warning) if skipped: - return PatchResult(out_path=str(output_path), patch_diff=[], warnings=warnings) + return PatchResult( + out_path=str(output_path), + patch_diff=[], + inverse_ops=[], + formula_issues=[], + warnings=warnings, + ) com = get_com_availability() if resolved_input.suffix.lower() == ".xls" and not com.available: @@ -257,8 +408,12 @@ def run_patch( ".xls editing requires Windows Excel COM (xlwings) in this environment." ) + use_openpyxl = _requires_openpyxl_backend(request) + if use_openpyxl and com.available: + warnings.append("Using openpyxl backend for extended patch features.") + _ensure_output_dir(output_path) - if com.available: + if com.available and not use_openpyxl: try: diff = _apply_ops_xlwings( resolved_input, @@ -267,23 +422,28 @@ def run_patch( request.auto_formula, ) return PatchResult( - out_path=str(output_path), patch_diff=diff, warnings=warnings + out_path=str(output_path), + patch_diff=diff, + inverse_ops=[], + formula_issues=[], + warnings=warnings, ) except PatchOpError as exc: return PatchResult( out_path=str(output_path), patch_diff=[], + inverse_ops=[], + formula_issues=[], warnings=warnings, error=exc.detail, ) except Exception as exc: fallback = _maybe_fallback_openpyxl( + request, resolved_input, output_path, - request.ops, warnings, reason=f"COM patch failed; falling back to openpyxl. ({exc!r})", - auto_formula=request.auto_formula, ) if fallback is not None: return fallback @@ -292,29 +452,32 @@ def run_patch( if com.reason: warnings.append(f"COM unavailable: {com.reason}") return _apply_with_openpyxl( + request, resolved_input, output_path, - request.ops, warnings, - auto_formula=request.auto_formula, ) def _apply_with_openpyxl( + request: PatchRequest, input_path: Path, output_path: Path, - ops: list[PatchOp], warnings: list[str], - *, - auto_formula: bool, ) -> PatchResult: """Apply patch operations using openpyxl.""" try: - diff = _apply_ops_openpyxl(input_path, output_path, ops, auto_formula) + diff, inverse_ops, formula_issues = _apply_ops_openpyxl( + request, + input_path, + output_path, + ) except PatchOpError as exc: return PatchResult( out_path=str(output_path), patch_diff=[], + inverse_ops=[], + formula_issues=[], warnings=warnings, error=exc.detail, ) @@ -328,17 +491,54 @@ def _apply_with_openpyxl( raise RuntimeError(f"openpyxl patch failed: {exc}") from exc warnings.append("openpyxl editing may drop shapes/charts or unsupported elements.") - return PatchResult(out_path=str(output_path), patch_diff=diff, warnings=warnings) + _append_skip_warnings(warnings, diff) + if ( + not request.dry_run + and request.preflight_formula_check + and any(issue.level == "error" for issue in formula_issues) + ): + issue = formula_issues[0] + error = PatchErrorDetail( + op_index=0, + op=request.ops[0].op if request.ops else "set_value", + sheet=issue.sheet, + cell=issue.cell, + message=f"Formula health check failed: {issue.message}", + ) + return PatchResult( + out_path=str(output_path), + patch_diff=[], + inverse_ops=[], + formula_issues=formula_issues, + warnings=warnings, + error=error, + ) + return PatchResult( + out_path=str(output_path), + patch_diff=diff, + inverse_ops=inverse_ops, + formula_issues=formula_issues, + warnings=warnings, + ) + + +def _append_skip_warnings(warnings: list[str], diff: list[PatchDiffItem]) -> None: + """Append warning messages for skipped conditional operations.""" + for item in diff: + if item.status != "skipped": + continue + warnings.append( + f"Skipped op[{item.op_index}] {item.op} at {item.sheet}!{item.cell} due to condition mismatch." + ) def _maybe_fallback_openpyxl( + request: PatchRequest, input_path: Path, output_path: Path, - ops: list[PatchOp], warnings: list[str], *, reason: str, - auto_formula: bool, ) -> PatchResult | None: """Attempt openpyxl fallback after COM failure.""" if input_path.suffix.lower() == ".xls": @@ -346,11 +546,20 @@ def _maybe_fallback_openpyxl( return None warnings.append(reason) return _apply_with_openpyxl( + request, input_path, output_path, - ops, warnings, - auto_formula=auto_formula, + ) + + +def _requires_openpyxl_backend(request: PatchRequest) -> bool: + """Return True if request requires openpyxl backend for extended features.""" + if request.dry_run or request.return_inverse_ops or request.preflight_formula_check: + return True + return any( + op.op in {"set_range_values", "fill_formula", "set_value_if", "set_formula_if"} + for op in request.ops ) @@ -440,11 +649,10 @@ def _next_available_path(path: Path) -> Path: def _apply_ops_openpyxl( + request: PatchRequest, input_path: Path, output_path: Path, - ops: list[PatchOp], - auto_formula: bool, -) -> list[PatchDiffItem]: +) -> tuple[list[PatchDiffItem], list[PatchOp], list[FormulaIssue]]: """Apply operations using openpyxl.""" try: from openpyxl import load_workbook @@ -456,25 +664,51 @@ def _apply_ops_openpyxl( workbook = load_workbook(input_path) try: - diff = _apply_ops_to_openpyxl_workbook(workbook, ops, auto_formula) - workbook.save(output_path) + diff, inverse_ops = _apply_ops_to_openpyxl_workbook( + workbook, + request.ops, + request.auto_formula, + return_inverse_ops=request.return_inverse_ops, + ) + formula_issues = ( + _collect_formula_issues_openpyxl(workbook) + if request.preflight_formula_check + else [] + ) + if not request.dry_run and not ( + request.preflight_formula_check + and any(issue.level == "error" for issue in formula_issues) + ): + workbook.save(output_path) finally: workbook.close() - return diff + return diff, inverse_ops, formula_issues def _apply_ops_to_openpyxl_workbook( - workbook: OpenpyxlWorkbookProtocol, ops: list[PatchOp], auto_formula: bool -) -> list[PatchDiffItem]: + workbook: OpenpyxlWorkbookProtocol, + ops: list[PatchOp], + auto_formula: bool, + *, + return_inverse_ops: bool, +) -> tuple[list[PatchDiffItem], list[PatchOp]]: """Apply ops to an openpyxl workbook instance.""" sheets = _openpyxl_sheet_map(workbook) diff: list[PatchDiffItem] = [] + inverse_ops: list[PatchOp] = [] for index, op in enumerate(ops): try: - diff.append(_apply_openpyxl_op(workbook, sheets, op, index, auto_formula)) + item, inverse = _apply_openpyxl_op( + workbook, sheets, op, index, auto_formula + ) + diff.append(item) + if return_inverse_ops and item.status == "applied" and inverse is not None: + inverse_ops.append(inverse) except ValueError as exc: raise PatchOpError.from_op(index, op, exc) from exc - return diff + if return_inverse_ops: + inverse_ops.reverse() + return diff, inverse_ops def _openpyxl_sheet_map( @@ -493,62 +727,210 @@ def _apply_openpyxl_op( op: PatchOp, index: int, auto_formula: bool, -) -> PatchDiffItem: +) -> tuple[PatchDiffItem, PatchOp | None]: """Apply a single op to openpyxl workbook.""" if op.op == "add_sheet": - if op.sheet in sheets: - raise ValueError(f"Sheet already exists: {op.sheet}") - sheet = workbook.create_sheet(title=op.sheet) - sheets[op.sheet] = sheet - return PatchDiffItem( + return _apply_openpyxl_add_sheet(workbook, sheets, op, index) + + existing_sheet = sheets.get(op.sheet) + if existing_sheet is None: + raise ValueError(f"Sheet not found: {op.sheet}") + + if op.op == "set_range_values": + return _apply_openpyxl_set_range_values(existing_sheet, op, index) + + if op.op == "fill_formula": + return _apply_openpyxl_fill_formula(existing_sheet, op, index) + + if op.op in {"set_value", "set_formula", "set_value_if", "set_formula_if"}: + return _apply_openpyxl_cell_op(existing_sheet, op, index, auto_formula) + raise ValueError(f"Unsupported op: {op.op}") + + +def _apply_openpyxl_add_sheet( + workbook: OpenpyxlWorkbookProtocol, + sheets: dict[str, OpenpyxlWorksheetProtocol], + op: PatchOp, + index: int, +) -> tuple[PatchDiffItem, PatchOp | None]: + """Apply add_sheet op.""" + if op.sheet in sheets: + raise ValueError(f"Sheet already exists: {op.sheet}") + sheet = workbook.create_sheet(title=op.sheet) + sheets[op.sheet] = sheet + return ( + PatchDiffItem( op_index=index, op=op.op, sheet=op.sheet, cell=None, before=None, after=PatchValue(kind="sheet", value=op.sheet), - ) + ), + None, + ) - existing_sheet = sheets.get(op.sheet) - if existing_sheet is None: - raise ValueError(f"Sheet not found: {op.sheet}") + +def _apply_openpyxl_set_range_values( + sheet: OpenpyxlWorksheetProtocol, + op: PatchOp, + index: int, +) -> tuple[PatchDiffItem, PatchOp | None]: + """Apply set_range_values op.""" + if op.range is None or op.values is None: + raise ValueError("set_range_values requires range and values.") + coordinates = _expand_range_coordinates(op.range) + rows, cols = _shape_of_coordinates(coordinates) + if len(op.values) != rows: + raise ValueError("set_range_values values height does not match range.") + if any(len(row) != cols for row in op.values): + raise ValueError("set_range_values values width does not match range.") + for r_idx, row in enumerate(coordinates): + for c_idx, coord in enumerate(row): + sheet[coord].value = op.values[r_idx][c_idx] + return ( + PatchDiffItem( + op_index=index, + op=op.op, + sheet=op.sheet, + cell=op.range, + before=None, + after=PatchValue(kind="value", value=f"{rows}x{cols}"), + ), + None, + ) + + +def _apply_openpyxl_fill_formula( + sheet: OpenpyxlWorksheetProtocol, + op: PatchOp, + index: int, +) -> tuple[PatchDiffItem, PatchOp | None]: + """Apply fill_formula op.""" + if op.range is None or op.formula is None or op.base_cell is None: + raise ValueError("fill_formula requires range, base_cell and formula.") + coordinates = _expand_range_coordinates(op.range) + rows, cols = _shape_of_coordinates(coordinates) + if rows != 1 and cols != 1: + raise ValueError("fill_formula range must be a single row or a single column.") + for row in coordinates: + for coord in row: + sheet[coord].value = _translate_formula(op.formula, op.base_cell, coord) + return ( + PatchDiffItem( + op_index=index, + op=op.op, + sheet=op.sheet, + cell=op.range, + before=None, + after=PatchValue(kind="formula", value=op.formula), + ), + None, + ) + + +def _apply_openpyxl_cell_op( + sheet: OpenpyxlWorksheetProtocol, + op: PatchOp, + index: int, + auto_formula: bool, +) -> tuple[PatchDiffItem, PatchOp | None]: + """Apply single-cell operations.""" cell_ref = op.cell if cell_ref is None: raise ValueError(f"{op.op} requires cell.") - cell = existing_sheet[cell_ref] + cell = sheet[cell_ref] before = _openpyxl_cell_value(cell) + if op.op == "set_value": - if isinstance(op.value, str) and op.value.startswith("="): - if not auto_formula: - raise ValueError("set_value rejects values starting with '='.") - cell.value = op.value - after = PatchValue(kind="formula", value=op.value) - else: - cell.value = op.value - after = PatchValue(kind="value", value=op.value) - return PatchDiffItem( - op_index=index, - op=op.op, - sheet=op.sheet, - cell=cell_ref, - before=before, - after=after, - ) + after = _set_cell_value(cell, op.value, auto_formula, op_name="set_value") + return _build_cell_result( + op, index, cell_ref, before, after + ), _build_inverse_cell_op(op, cell_ref, before) if op.op == "set_formula": - formula = op.formula - if formula is None: - raise ValueError("set_formula requires formula.") + formula = _require_formula(op.formula, "set_formula") cell.value = formula after = PatchValue(kind="formula", value=formula) - return PatchDiffItem( - op_index=index, - op=op.op, - sheet=op.sheet, - cell=cell_ref, - before=before, - after=after, - ) - raise ValueError(f"Unsupported op: {op.op}") + return _build_cell_result( + op, index, cell_ref, before, after + ), _build_inverse_cell_op(op, cell_ref, before) + if op.op == "set_value_if": + if not _values_equal_for_condition( + _patch_value_to_primitive(before), op.expected + ): + return _build_skipped_result(op, index, cell_ref, before), None + after = _set_cell_value(cell, op.value, auto_formula, op_name="set_value_if") + return _build_cell_result( + op, index, cell_ref, before, after + ), _build_inverse_cell_op(op, cell_ref, before) + formula_if = _require_formula(op.formula, "set_formula_if") + if not _values_equal_for_condition(_patch_value_to_primitive(before), op.expected): + return _build_skipped_result(op, index, cell_ref, before), None + cell.value = formula_if + after = PatchValue(kind="formula", value=formula_if) + return _build_cell_result( + op, index, cell_ref, before, after + ), _build_inverse_cell_op(op, cell_ref, before) + + +def _set_cell_value( + cell: OpenpyxlCellProtocol, + value: str | int | float | None, + auto_formula: bool, + *, + op_name: str, +) -> PatchValue: + """Set cell value with auto_formula handling.""" + if isinstance(value, str) and value.startswith("="): + if not auto_formula: + raise ValueError(f"{op_name} rejects values starting with '='.") + cell.value = value + return PatchValue(kind="formula", value=value) + cell.value = value + return PatchValue(kind="value", value=value) + + +def _build_cell_result( + op: PatchOp, + index: int, + cell_ref: str, + before: PatchValue | None, + after: PatchValue | None, +) -> PatchDiffItem: + """Build applied diff item for single-cell op.""" + return PatchDiffItem( + op_index=index, + op=op.op, + sheet=op.sheet, + cell=cell_ref, + before=before, + after=after, + ) + + +def _build_skipped_result( + op: PatchOp, + index: int, + cell_ref: str, + before: PatchValue | None, +) -> PatchDiffItem: + """Build skipped diff item.""" + return PatchDiffItem( + op_index=index, + op=op.op, + sheet=op.sheet, + cell=cell_ref, + before=before, + after=before, + status="skipped", + ) + + +def _require_formula(formula: str | None, op_name: str) -> str: + """Require a non-null formula string.""" + if formula is None: + raise ValueError(f"{op_name} requires formula.") + return formula def _openpyxl_cell_value(cell: OpenpyxlCellProtocol) -> PatchValue | None: @@ -569,6 +951,123 @@ def _normalize_formula(value: object) -> str: return text if text.startswith("=") else f"={text}" +def _expand_range_coordinates(range_ref: str) -> list[list[str]]: + """Expand A1 range string into a 2D list of coordinates.""" + try: + from openpyxl.utils.cell import get_column_letter, range_boundaries + except ImportError as exc: + raise RuntimeError(f"openpyxl is not available: {exc}") from exc + min_col, min_row, max_col, max_row = range_boundaries(range_ref) + if min_col > max_col or min_row > max_row: + raise ValueError(f"Invalid range reference: {range_ref}") + rows: list[list[str]] = [] + for row_idx in range(min_row, max_row + 1): + row: list[str] = [] + for col_idx in range(min_col, max_col + 1): + row.append(f"{get_column_letter(col_idx)}{row_idx}") + rows.append(row) + return rows + + +def _shape_of_coordinates(coordinates: list[list[str]]) -> tuple[int, int]: + """Return rows/cols for expanded coordinates.""" + if not coordinates or not coordinates[0]: + raise ValueError("Range expansion resulted in an empty coordinate set.") + return len(coordinates), len(coordinates[0]) + + +def _translate_formula(formula: str, origin: str, target: str) -> str: + """Translate formula with relative references from origin to target.""" + try: + from openpyxl.formula.translate import Translator + except ImportError as exc: + raise RuntimeError(f"openpyxl is not available: {exc}") from exc + translated = Translator(formula, origin=origin).translate_formula(target) + return str(translated) + + +def _patch_value_to_primitive(value: PatchValue | None) -> str | int | float | None: + """Convert PatchValue into primitive value for condition checks.""" + if value is None: + return None + return value.value + + +def _values_equal_for_condition( + current: str | int | float | None, + expected: str | int | float | None, +) -> bool: + """Compare values for conditional update checks.""" + return current == expected + + +def _build_inverse_cell_op( + op: PatchOp, + cell_ref: str, + before: PatchValue | None, +) -> PatchOp | None: + """Build inverse operation for single-cell updates.""" + if op.op not in {"set_value", "set_formula", "set_value_if", "set_formula_if"}: + return None + if before is None: + return PatchOp(op="set_value", sheet=op.sheet, cell=cell_ref, value=None) + if before.kind == "formula": + return PatchOp( + op="set_formula", + sheet=op.sheet, + cell=cell_ref, + formula=str(before.value), + ) + return PatchOp(op="set_value", sheet=op.sheet, cell=cell_ref, value=before.value) + + +def _collect_formula_issues_openpyxl( + workbook: OpenpyxlWorkbookProtocol, +) -> list[FormulaIssue]: + """Collect simple formula issues by scanning formula text.""" + token_map: dict[str, tuple[FormulaIssueCode, FormulaIssueLevel]] = { + "#REF!": ("ref_error", "error"), + "#NAME?": ("name_error", "error"), + "#DIV/0!": ("div0_error", "error"), + "#VALUE!": ("value_error", "error"), + "#N/A": ("na_error", "warning"), + } + issues: list[FormulaIssue] = [] + for sheet_name in workbook.sheetnames: + sheet = workbook[sheet_name] + iter_rows = getattr(sheet, "iter_rows", None) + if iter_rows is None: + continue + for row in iter_rows(): + for cell in row: + raw = getattr(cell, "value", None) + if not isinstance(raw, str) or not raw.startswith("="): + continue + normalized = raw.upper() + if "==" in normalized: + issues.append( + FormulaIssue( + sheet=sheet_name, + cell=str(getattr(cell, "coordinate", "")), + level="warning", + code="invalid_token", + message="Formula contains duplicated '=' token.", + ) + ) + for token, (code, level) in token_map.items(): + if token in normalized: + issues.append( + FormulaIssue( + sheet=sheet_name, + cell=str(getattr(cell, "coordinate", "")), + level=level, + code=code, + message=f"Formula contains error token {token}.", + ) + ) + return issues + + def _apply_ops_xlwings( input_path: Path, output_path: Path, diff --git a/src/exstruct/mcp/server.py b/src/exstruct/mcp/server.py index 971434c..164cbaf 100644 --- a/src/exstruct/mcp/server.py +++ b/src/exstruct/mcp/server.py @@ -307,6 +307,9 @@ async def _patch_tool( out_name: str | None = None, on_conflict: OnConflictPolicy | None = None, auto_formula: bool = False, + dry_run: bool = False, + return_inverse_ops: bool = False, + preflight_formula_check: bool = False, ) -> PatchToolOutput: """Handle the ExStruct patch tool call. @@ -327,6 +330,9 @@ async def _patch_tool( out_name=out_name, on_conflict=on_conflict, auto_formula=auto_formula, + dry_run=dry_run, + return_inverse_ops=return_inverse_ops, + preflight_formula_check=preflight_formula_check, ) effective_on_conflict = on_conflict or default_on_conflict work = functools.partial( diff --git a/src/exstruct/mcp/tools.py b/src/exstruct/mcp/tools.py index 6e864f5..74fbd8f 100644 --- a/src/exstruct/mcp/tools.py +++ b/src/exstruct/mcp/tools.py @@ -23,6 +23,7 @@ ) from .io import PathPolicy from .patch_runner import ( + FormulaIssue, PatchDiffItem, PatchErrorDetail, PatchOp, @@ -99,6 +100,9 @@ class PatchToolInput(BaseModel): out_name: str | None = None on_conflict: OnConflictPolicy | None = None auto_formula: bool = False + dry_run: bool = False + return_inverse_ops: bool = False + preflight_formula_check: bool = False class PatchToolOutput(BaseModel): @@ -106,6 +110,8 @@ class PatchToolOutput(BaseModel): out_path: str patch_diff: list[PatchDiffItem] = Field(default_factory=list) + inverse_ops: list[PatchOp] = Field(default_factory=list) + formula_issues: list[FormulaIssue] = Field(default_factory=list) warnings: list[str] = Field(default_factory=list) error: PatchErrorDetail | None = None @@ -201,6 +207,9 @@ def run_patch_tool( out_name=payload.out_name, on_conflict=payload.on_conflict or on_conflict or "rename", auto_formula=payload.auto_formula, + dry_run=payload.dry_run, + return_inverse_ops=payload.return_inverse_ops, + preflight_formula_check=payload.preflight_formula_check, ) result = run_patch(request, policy=policy) return _to_patch_tool_output(result) @@ -271,6 +280,8 @@ def _to_patch_tool_output(result: PatchResult) -> PatchToolOutput: return PatchToolOutput( out_path=result.out_path, patch_diff=result.patch_diff, + inverse_ops=result.inverse_ops, + formula_issues=result.formula_issues, warnings=result.warnings, error=result.error, ) diff --git a/tests/mcp/test_patch_runner.py b/tests/mcp/test_patch_runner.py index 4563326..57de2c6 100644 --- a/tests/mcp/test_patch_runner.py +++ b/tests/mcp/test_patch_runner.py @@ -243,3 +243,173 @@ def test_run_patch_xls_requires_com( request = PatchRequest(xlsx_path=input_path, ops=ops, on_conflict="rename") with pytest.raises(ValueError, match=r"requires Windows Excel COM"): run_patch(request, policy=PathPolicy(root=tmp_path)) + + +def test_run_patch_dry_run_does_not_write( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _disable_com(monkeypatch) + input_path = tmp_path / "book.xlsx" + _create_workbook(input_path) + output_path = tmp_path / "book_patched.xlsx" + assert not output_path.exists() + ops = [PatchOp(op="set_value", sheet="Sheet1", cell="A1", value="x")] + request = PatchRequest( + xlsx_path=input_path, + ops=ops, + on_conflict="rename", + dry_run=True, + ) + result = run_patch(request, policy=PathPolicy(root=tmp_path)) + assert result.error is None + assert not output_path.exists() + assert len(result.patch_diff) == 1 + + +def test_run_patch_return_inverse_ops( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _disable_com(monkeypatch) + input_path = tmp_path / "book.xlsx" + _create_workbook(input_path) + ops = [PatchOp(op="set_value", sheet="Sheet1", cell="A1", value="new")] + request = PatchRequest( + xlsx_path=input_path, + ops=ops, + on_conflict="rename", + return_inverse_ops=True, + ) + result = run_patch(request, policy=PathPolicy(root=tmp_path)) + assert result.error is None + assert len(result.inverse_ops) == 1 + inverse = result.inverse_ops[0] + assert inverse.op == "set_value" + assert inverse.cell == "A1" + assert inverse.value == "old" + + +def test_run_patch_set_range_values( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _disable_com(monkeypatch) + input_path = tmp_path / "book.xlsx" + _create_workbook(input_path) + ops = [ + PatchOp( + op="set_range_values", + sheet="Sheet1", + range="A2:B3", + values=[["r1c1", "r1c2"], ["r2c1", "r2c2"]], + ) + ] + request = PatchRequest(xlsx_path=input_path, ops=ops, on_conflict="rename") + result = run_patch(request, policy=PathPolicy(root=tmp_path)) + assert result.error is None + workbook = load_workbook(result.out_path) + try: + sheet = workbook["Sheet1"] + assert sheet["A2"].value == "r1c1" + assert sheet["B3"].value == "r2c2" + finally: + workbook.close() + + +def test_run_patch_set_range_values_size_mismatch( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _disable_com(monkeypatch) + input_path = tmp_path / "book.xlsx" + _create_workbook(input_path) + ops = [ + PatchOp( + op="set_range_values", + sheet="Sheet1", + range="A2:B3", + values=[["only_one_column"], ["still_one_column"]], + ) + ] + request = PatchRequest(xlsx_path=input_path, ops=ops, on_conflict="rename") + result = run_patch(request, policy=PathPolicy(root=tmp_path)) + assert result.error is not None + assert "width does not match range" in result.error.message + + +def test_run_patch_set_value_if_skipped( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _disable_com(monkeypatch) + input_path = tmp_path / "book.xlsx" + _create_workbook(input_path) + ops = [ + PatchOp( + op="set_value_if", + sheet="Sheet1", + cell="A1", + expected="not_old", + value="new", + ) + ] + request = PatchRequest(xlsx_path=input_path, ops=ops, on_conflict="rename") + result = run_patch(request, policy=PathPolicy(root=tmp_path)) + assert result.error is None + assert len(result.patch_diff) == 1 + assert result.patch_diff[0].status == "skipped" + + +def test_run_patch_fill_formula( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _disable_com(monkeypatch) + input_path = tmp_path / "book.xlsx" + _create_workbook(input_path) + workbook = load_workbook(input_path) + try: + sheet = workbook["Sheet1"] + sheet["A2"] = 1 + sheet["A3"] = 2 + sheet["A4"] = 3 + sheet["B2"] = 10 + sheet["B3"] = 20 + sheet["B4"] = 30 + workbook.save(input_path) + finally: + workbook.close() + ops = [ + PatchOp( + op="fill_formula", + sheet="Sheet1", + range="C2:C4", + base_cell="C2", + formula="=A2+B2", + ) + ] + request = PatchRequest(xlsx_path=input_path, ops=ops, on_conflict="rename") + result = run_patch(request, policy=PathPolicy(root=tmp_path)) + assert result.error is None + workbook = load_workbook(result.out_path) + try: + sheet = workbook["Sheet1"] + assert sheet["C2"].value == "=A2+B2" + assert sheet["C3"].value == "=A3+B3" + assert sheet["C4"].value == "=A4+B4" + finally: + workbook.close() + + +def test_run_patch_formula_health_check( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _disable_com(monkeypatch) + input_path = tmp_path / "book.xlsx" + _create_workbook(input_path) + ops = [PatchOp(op="set_formula", sheet="Sheet1", cell="A1", formula="=#REF!+1")] + request = PatchRequest( + xlsx_path=input_path, + ops=ops, + on_conflict="rename", + preflight_formula_check=True, + ) + result = run_patch(request, policy=PathPolicy(root=tmp_path)) + assert result.error is not None + assert result.formula_issues + assert result.formula_issues[0].code == "ref_error" diff --git a/tests/mcp/test_server.py b/tests/mcp/test_server.py index bc85a23..3223a12 100644 --- a/tests/mcp/test_server.py +++ b/tests/mcp/test_server.py @@ -175,6 +175,12 @@ async def fake_run_sync(func: Callable[[], object]) -> object: chunk_call = cast(tuple[ReadJsonChunkToolInput, PathPolicy], calls["chunk"]) assert chunk_call[0].filter is not None assert calls["patch"][2] == "rename" + patch_call = cast( + tuple[PatchToolInput, PathPolicy, OnConflictPolicy], calls["patch"] + ) + assert patch_call[0].dry_run is False + assert patch_call[0].return_inverse_ops is False + assert patch_call[0].preflight_formula_check is False def test_register_tools_passes_patch_default_on_conflict( @@ -237,6 +243,76 @@ async def fake_run_sync(func: Callable[[], object]) -> object: assert calls["patch"][2] == "overwrite" +def test_register_tools_passes_patch_extended_flags( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + app = DummyApp() + policy = PathPolicy(root=tmp_path) + calls: dict[str, tuple[object, ...]] = {} + + def fake_run_extract_tool( + payload: ExtractToolInput, + *, + policy: PathPolicy, + on_conflict: OnConflictPolicy, + ) -> ExtractToolOutput: + return ExtractToolOutput(out_path="out.json") + + def fake_run_read_json_chunk_tool( + payload: ReadJsonChunkToolInput, + *, + policy: PathPolicy, + ) -> ReadJsonChunkToolOutput: + return ReadJsonChunkToolOutput(chunk="{}") + + def fake_run_validate_input_tool( + payload: ValidateInputToolInput, + *, + policy: PathPolicy, + ) -> ValidateInputToolOutput: + return ValidateInputToolOutput(is_readable=True) + + def fake_run_patch_tool( + payload: PatchToolInput, + *, + policy: PathPolicy, + on_conflict: OnConflictPolicy, + ) -> PatchToolOutput: + calls["patch"] = (payload, policy, on_conflict) + return PatchToolOutput(out_path="out.xlsx", patch_diff=[]) + + async def fake_run_sync(func: Callable[[], object]) -> object: + return func() + + monkeypatch.setattr(server, "run_extract_tool", fake_run_extract_tool) + monkeypatch.setattr( + server, "run_read_json_chunk_tool", fake_run_read_json_chunk_tool + ) + monkeypatch.setattr(server, "run_validate_input_tool", fake_run_validate_input_tool) + monkeypatch.setattr(server, "run_patch_tool", fake_run_patch_tool) + monkeypatch.setattr(anyio.to_thread, "run_sync", fake_run_sync) + + server._register_tools(app, policy, default_on_conflict="overwrite") + patch_tool = cast(Callable[..., Awaitable[object]], app.tools["exstruct_patch"]) + anyio.run( + _call_async, + patch_tool, + { + "xlsx_path": "in.xlsx", + "ops": [{"op": "add_sheet", "sheet": "New"}], + "dry_run": True, + "return_inverse_ops": True, + "preflight_formula_check": True, + }, + ) + patch_call = cast( + tuple[PatchToolInput, PathPolicy, OnConflictPolicy], calls["patch"] + ) + assert patch_call[0].dry_run is True + assert patch_call[0].return_inverse_ops is True + assert patch_call[0].preflight_formula_check is True + + def test_run_server_sets_env(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: created: dict[str, object] = {} diff --git a/tests/mcp/test_tool_models.py b/tests/mcp/test_tool_models.py index 4955b15..7a4f920 100644 --- a/tests/mcp/test_tool_models.py +++ b/tests/mcp/test_tool_models.py @@ -27,3 +27,6 @@ def test_patch_tool_input_defaults() -> None: assert payload.out_dir is None assert payload.out_name is None assert payload.on_conflict is None + assert payload.dry_run is False + assert payload.return_inverse_ops is False + assert payload.preflight_formula_check is False diff --git a/tests/mcp/test_tools_handlers.py b/tests/mcp/test_tools_handlers.py index e723622..1e790d8 100644 --- a/tests/mcp/test_tools_handlers.py +++ b/tests/mcp/test_tools_handlers.py @@ -109,9 +109,15 @@ def _fake_run_patch( payload = tools.PatchToolInput( xlsx_path="input.xlsx", ops=[{"op": "add_sheet", "sheet": "New"}], + dry_run=True, + return_inverse_ops=True, + preflight_formula_check=True, ) tools.run_patch_tool(payload, on_conflict="rename") request = captured["request"] assert isinstance(request, PatchRequest) assert request.xlsx_path == Path("input.xlsx") assert request.on_conflict == "rename" + assert request.dry_run is True + assert request.return_inverse_ops is True + assert request.preflight_formula_check is True From 92d27ed36e4e2baf653eff3aca5e71325dc7479c Mon Sep 17 00:00:00 2001 From: harumiWeb Date: Sun, 8 Feb 2026 16:20:53 +0900 Subject: [PATCH 09/25] feat: enhance PatchOp model with detailed operation descriptions and update conflict policy to overwrite --- src/exstruct/mcp/patch_runner.py | 61 ++++++++++++++++++++++++++------ src/exstruct/mcp/server.py | 33 ++++++++++++----- src/exstruct/mcp/tools.py | 2 +- 3 files changed, 76 insertions(+), 20 deletions(-) diff --git a/src/exstruct/mcp/patch_runner.py b/src/exstruct/mcp/patch_runner.py index dcbdea3..8baaf0a 100644 --- a/src/exstruct/mcp/patch_runner.py +++ b/src/exstruct/mcp/patch_runner.py @@ -115,17 +115,56 @@ def close(self) -> None: ... class PatchOp(BaseModel): - """Single patch operation for an Excel workbook.""" + """Single patch operation for an Excel workbook. - op: PatchOpType - sheet: str - cell: str | None = None - range: str | None = None - base_cell: str | None = None - expected: str | int | float | None = None - value: str | int | float | None = None - values: list[list[str | int | float | None]] | None = None - formula: str | None = None + Operation types and their required fields: + + - ``set_value``: Set a cell value. Requires ``sheet``, ``cell``, ``value``. + - ``set_formula``: Set a cell formula. Requires ``sheet``, ``cell``, ``formula`` (must start with ``=``). + - ``add_sheet``: Add a new worksheet. Requires ``sheet`` (new sheet name). No ``cell``/``value``/``formula``. + - ``set_range_values``: Set values for a rectangular range. Requires ``sheet``, ``range`` (e.g. ``A1:C3``), ``values`` (2D list matching range shape). + - ``fill_formula``: Fill a formula across a single row or column. Requires ``sheet``, ``range``, ``base_cell``, ``formula``. + - ``set_value_if``: Conditionally set value. Requires ``sheet``, ``cell``, ``expected``, ``value``. Skips if current value != expected. + - ``set_formula_if``: Conditionally set formula. Requires ``sheet``, ``cell``, ``expected``, ``formula``. Skips if current value != expected. + """ + + op: PatchOpType = Field( + description=( + "Operation type: 'set_value', 'set_formula', 'add_sheet', " + "'set_range_values', 'fill_formula', 'set_value_if', or 'set_formula_if'." + ) + ) + sheet: str = Field( + description="Target sheet name. For add_sheet, this is the new sheet name." + ) + cell: str | None = Field( + default=None, + description="Cell reference in A1 notation (e.g. 'B2'). Required for set_value, set_formula, set_value_if, set_formula_if.", + ) + range: str | None = Field( + default=None, + description="Range reference in A1 notation (e.g. 'A1:C3'). Required for set_range_values and fill_formula.", + ) + base_cell: str | None = Field( + default=None, + description="Base cell for formula translation in fill_formula (e.g. 'C2').", + ) + expected: str | int | float | None = Field( + default=None, + description="Expected current value for conditional ops (set_value_if, set_formula_if). Operation is skipped if mismatch.", + ) + value: str | int | float | None = Field( + default=None, + description="Value to set. Use null to clear a cell. For set_value and set_value_if.", + ) + values: list[list[str | int | float | None]] | None = Field( + default=None, + description="2D list of values for set_range_values. Shape must match the range dimensions.", + ) + formula: str | None = Field( + default=None, + description="Formula string starting with '=' (e.g. '=SUM(A1:A10)'). For set_formula, set_formula_if, fill_formula.", + ) @field_validator("sheet") @classmethod @@ -344,7 +383,7 @@ class PatchRequest(BaseModel): ops: list[PatchOp] out_dir: Path | None = None out_name: str | None = None - on_conflict: OnConflictPolicy = "rename" + on_conflict: OnConflictPolicy = "overwrite" auto_formula: bool = False dry_run: bool = False return_inverse_ops: bool = False diff --git a/src/exstruct/mcp/server.py b/src/exstruct/mcp/server.py index 164cbaf..034626e 100644 --- a/src/exstruct/mcp/server.py +++ b/src/exstruct/mcp/server.py @@ -16,6 +16,7 @@ from .extract_runner import OnConflictPolicy from .io import PathPolicy +from .patch_runner import PatchOp from .tools import ( ExtractToolInput, ExtractToolOutput, @@ -302,7 +303,7 @@ async def _validate_input_tool(xlsx_path: str) -> ValidateInputToolOutput: async def _patch_tool( xlsx_path: str, - ops: list[dict[str, Any]], + ops: list[PatchOp], out_dir: str | None = None, out_name: str | None = None, on_conflict: OnConflictPolicy | None = None, @@ -311,17 +312,33 @@ async def _patch_tool( return_inverse_ops: bool = False, preflight_formula_check: bool = False, ) -> PatchToolOutput: - """Handle the ExStruct patch tool call. + """Edit an Excel workbook by applying patch operations. + + Supports cell value updates, formula updates, and adding new sheets. + Operations are applied atomically: all succeed or none are saved. Args: - xlsx_path: Path to the Excel workbook. - ops: Patch operations to apply. - out_dir: Optional output directory. - out_name: Optional output filename. - on_conflict: Optional conflict policy override. + xlsx_path: Path to the Excel workbook to edit. + ops: Patch operations to apply in order. Each op has an 'op' field + specifying the type: 'set_value' (set cell value), 'set_formula' + (set cell formula starting with '='), 'add_sheet' (create new sheet), + 'set_range_values' (bulk set rectangular range), 'fill_formula' + (fill formula across a row/column), 'set_value_if' (conditional + value update), 'set_formula_if' (conditional formula update). + out_dir: Output directory. Defaults to same directory as input. + out_name: Output filename. Defaults to '{stem}_patched{ext}'. + on_conflict: Conflict policy when output file exists: + 'overwrite' (replace), 'skip' (do nothing), 'rename' (auto-rename). + Defaults to server --on-conflict setting. + auto_formula: When true, values starting with '=' in set_value ops + are treated as formulas instead of being rejected. + dry_run: When true, compute diff without saving changes. + return_inverse_ops: When true, return inverse (undo) operations. + preflight_formula_check: When true, scan formulas for errors + like #REF!, #NAME?, #DIV/0! before saving. Returns: - Patch result payload. + Patch result with output path, applied diffs, and any warnings. """ payload = PatchToolInput( xlsx_path=xlsx_path, diff --git a/src/exstruct/mcp/tools.py b/src/exstruct/mcp/tools.py index 74fbd8f..0bd5a6a 100644 --- a/src/exstruct/mcp/tools.py +++ b/src/exstruct/mcp/tools.py @@ -205,7 +205,7 @@ def run_patch_tool( ops=payload.ops, out_dir=Path(payload.out_dir) if payload.out_dir else None, out_name=payload.out_name, - on_conflict=payload.on_conflict or on_conflict or "rename", + on_conflict=payload.on_conflict or on_conflict or "overwrite", auto_formula=payload.auto_formula, dry_run=payload.dry_run, return_inverse_ops=payload.return_inverse_ops, From 5100866a377b9ae105dc4c3f88efb90932ed0486 Mon Sep 17 00:00:00 2001 From: harumiWeb Date: Sun, 8 Feb 2026 16:22:06 +0900 Subject: [PATCH 10/25] update --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5dbe8f0..d09640e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "exstruct" -version = "0.4.3" +version = "0.4.4" description = "Excel to structured JSON (tables, shapes, charts) for LLM/RAG pipelines" readme = "README.md" license = { file = "LICENSE" } From eb869ce258a3b2d8c590ac98d6241c060dc5fbae Mon Sep 17 00:00:00 2001 From: harumiWeb Date: Sun, 8 Feb 2026 17:06:40 +0900 Subject: [PATCH 11/25] feat: add alpha_col option to convert column keys to Excel-style names and implement related utilities and tests --- docs/agents/FEATURE_SPEC.md | 108 ++++++++++++++++++++- docs/agents/TASKS.md | 17 ++++ src/exstruct/__init__.py | 20 +++- src/exstruct/cli/main.py | 6 ++ src/exstruct/engine.py | 10 +- src/exstruct/mcp/extract_runner.py | 5 + src/exstruct/mcp/server.py | 4 +- src/exstruct/models/__init__.py | 132 ++++++++++++++++++++++++- tests/cli/test_cli_alpha_col.py | 78 +++++++++++++++ tests/engine/test_engine_alpha_col.py | 51 ++++++++++ tests/mcp/test_extract_alpha_col.py | 24 +++++ tests/models/test_alpha_col.py | 134 ++++++++++++++++++++++++++ uv.lock | 2 +- 13 files changed, 580 insertions(+), 11 deletions(-) create mode 100644 tests/cli/test_cli_alpha_col.py create mode 100644 tests/engine/test_engine_alpha_col.py create mode 100644 tests/mcp/test_extract_alpha_col.py create mode 100644 tests/models/test_alpha_col.py diff --git a/docs/agents/FEATURE_SPEC.md b/docs/agents/FEATURE_SPEC.md index 19e7117..198bcfc 100644 --- a/docs/agents/FEATURE_SPEC.md +++ b/docs/agents/FEATURE_SPEC.md @@ -15,9 +15,11 @@ ## 2. ツールI/F定義(OpenAPI / Pydantic想定) ### Tool名 + - `exstruct_patch` ### Input + - `xlsx_path: str` - `ops: list[PatchOp]` - `out_dir: str | None` @@ -26,6 +28,7 @@ - `auto_formula: bool`(default: false) ### Output + - `out_path: str` - `patch_diff: list[PatchDiffItem]` - `warnings: list[str]` @@ -36,18 +39,21 @@ ## 3. 操作種別(PatchOp) ### set_value + - `op: "set_value"` - `sheet: str` - `cell: str` (A1形式) - `value: str | int | float | None`(nullでクリア) ### set_formula + - `op: "set_formula"` - `sheet: str` - `cell: str` (A1形式) - `formula: str`(必ず `=` から開始) ### add_sheet + - `op: "add_sheet"` - `sheet: str`(新規シート名) @@ -125,6 +131,7 @@ - `warnings` にスキップ理由を記載 出力名の既定: + - `out_name` 未指定時は `"{stem}_patched{suffix}"` --- @@ -141,32 +148,45 @@ ## 10. 例 ### set_value + ```json { "xlsx_path": "book.xlsx", "ops": [ - {"op": "set_value", "sheet": "フォーム", "cell": "B2", "value": "山田太郎"} + { + "op": "set_value", + "sheet": "フォーム", + "cell": "B2", + "value": "山田太郎" + } ] } ``` ### set_formula + ```json { "xlsx_path": "book.xlsx", "ops": [ - {"op": "set_formula", "sheet": "計算", "cell": "C10", "formula": "=SUM(Sheet1!A:A)"} + { + "op": "set_formula", + "sheet": "計算", + "cell": "C10", + "formula": "=SUM(Sheet1!A:A)" + } ] } ``` ### add_sheet + ```json { "xlsx_path": "book.xlsx", "ops": [ - {"op": "add_sheet", "sheet": "売上集計"}, - {"op": "set_value", "sheet": "売上集計", "cell": "A1", "value": "月"} + { "op": "add_sheet", "sheet": "売上集計" }, + { "op": "set_value", "sheet": "売上集計", "cell": "A1", "value": "月" } ] } ``` @@ -314,3 +334,83 @@ - `set_range_values` がサイズ不一致を検知できる - `set_value_if` / `set_formula_if` が不一致時に `skipped` を返せる - `preflight_formula_check=true` で `#REF!` 等を検出して `formula_issues` に反映できる + +--- + +## 13. ABC列名キー出力オプション(`alpha_col`) + +### 13.1 背景・目的 + +AIエージェントがExStruct MCPで抽出した結果をもとに `exstruct_patch` でセル編集を行う際、 +`CellRow.c` のキーが 0-based 数値文字列(`"0"`, `"12"`, `"18"` 等)のため、 +Excel上の列名(A, M, S 等)との対応が直感的に取れず、**誤ったセル座標への書き込みが頻発する**。 + +本機能は、抽出結果の `CellRow.c` キーを Excel 列名形式(A, B, ..., Z, AA, AB, ...)で +出力するオプションを提供し、AIエージェントの座標理解精度を大幅に改善する。 + +### 13.2 スコープ + +- **対象**: `CellRow.c` のキー、`CellRow.links` のキー +- **非対象**: `MergedCells.items` / `PrintArea` / `formulas_map` / `colors_map` の列インデックス(整数のまま) +- **変換方式**: 0-based 数値 → Excel列名(`0` → `A`, `1` → `B`, `25` → `Z`, `26` → `AA`) + +### 13.3 I/F + +#### 本体 API(`ExStructEngine` / `extract()` / `process_excel()`) + +- `StructOptions` に `alpha_col: bool = False` を追加 +- `True` の場合、抽出結果の `CellRow.c` / `CellRow.links` のキーを ABC 形式に変換する + +#### CLI + +- `--alpha-col` フラグを追加(`action="store_true"`) +- `process_excel()` に `alpha_col` パラメータを追加 + +#### MCP ツール(`exstruct_extract`) + +- `ExtractOptions` に `alpha_col: bool = False` を追加 +- `ExtractToolInput.options.alpha_col` をエンジンに連携 + +### 13.4 変換ユーティリティ + +- `exstruct.models.col_index_to_alpha(index: int) -> str` を新設 + - 0-based 整数 → Excel 列名文字列 + - 例: `0` → `"A"`, `25` → `"Z"`, `26` → `"AA"`, `701` → `"ZZ"` +- `exstruct.models.convert_row_keys_to_alpha(row: CellRow) -> CellRow` を新設 + - `CellRow` の `c` / `links` キーを一括変換した新しい `CellRow` を返す +- `exstruct.models.convert_sheet_keys_to_alpha(sheet: SheetData) -> SheetData` を新設 + - `SheetData.rows` 内の全 `CellRow` を変換した新しい `SheetData` を返す + +### 13.5 変換タイミング + +- `extract_workbook()` でデータ抽出が完了した **後**、シリアライズの **前** に変換する +- パイプライン上の位置: `ExStructEngine.extract()` の返却直前 + +### 13.6 出力例 + +#### `alpha_col=False`(デフォルト: 従来通り) + +```json +{ "r": 6, "c": { "0": "氏 名", "12": "生年月日", "18": "年" } } +``` + +#### `alpha_col=True` + +```json +{ "r": 6, "c": { "A": "氏 名", "M": "生年月日", "S": "年" } } +``` + +### 13.7 後方互換性 + +- デフォルト `False` のため、既存の出力は一切変更されない +- `alpha_col=True` は opt-in のみ +- `_safe_col_index()` 等の下流処理は数値キーを前提としているが、変換はシリアライズ直前のため影響なし + +### 13.8 受け入れ基準 + +- `col_index_to_alpha(0)` → `"A"`, `col_index_to_alpha(25)` → `"Z"`, `col_index_to_alpha(26)` → `"AA"` +- `alpha_col=True` で抽出した `CellRow.c` のキーがすべて A-Z 形式 +- `alpha_col=True` で `CellRow.links` のキーも同様に変換される +- `alpha_col=False`(デフォルト)で従来通り数値キー +- CLI `--alpha-col` フラグが正しく `process_excel()` に伝播する +- MCP `exstruct_extract` の `options.alpha_col` がエンジンに伝播する diff --git a/docs/agents/TASKS.md b/docs/agents/TASKS.md index b5424f9..779911a 100644 --- a/docs/agents/TASKS.md +++ b/docs/agents/TASKS.md @@ -42,3 +42,20 @@ - [x] `tests/mcp/test_tools_handlers.py`: 入出力モデル拡張の受け渡しテストを追加する - [x] `tests/mcp/test_server.py`: 新規ツール引数の受け渡しテストを追加する - [x] `ruff check .` / `mypy src/exstruct --strict` / 関連pytest を実行し、結果を記録する + +## ABC列名キー出力オプション(`alpha_col`) + +- [ ] `models/__init__.py`: `col_index_to_alpha()` 変換ユーティリティを追加する +- [ ] `models/__init__.py`: `convert_row_keys_to_alpha()` / `convert_sheet_keys_to_alpha()` を追加する +- [ ] `engine.py`: `StructOptions` に `alpha_col: bool = False` を追加する +- [ ] `engine.py`: `ExStructEngine.extract()` で `alpha_col=True` 時に変換を適用する +- [ ] `__init__.py`: `extract()` / `process_excel()` に `alpha_col` パラメータを追加する +- [ ] `cli/main.py`: `--alpha-col` CLI フラグを追加する +- [ ] `mcp/extract_runner.py`: `ExtractOptions` に `alpha_col` を追加し、`process_excel()` へ連携する +- [ ] `mcp/server.py`: `exstruct_extract` ツールに `alpha_col` パラメータを追加する +- [ ] `tests/models/`: `col_index_to_alpha()` の単体テストを追加する +- [ ] `tests/models/`: `convert_row_keys_to_alpha()` / `convert_sheet_keys_to_alpha()` のテストを追加する +- [ ] `tests/engine/`: `alpha_col=True` でのエンジン抽出テストを追加する +- [ ] `tests/cli/`: `--alpha-col` CLI フラグのテストを追加する +- [ ] `tests/mcp/`: MCP extract の `alpha_col` 連携テストを追加する +- [ ] `ruff check .` / `mypy src/exstruct --strict` / 関連pytest を実行し、結果を記録する diff --git a/src/exstruct/__init__.py b/src/exstruct/__init__.py index 6bdfd75..844b2e2 100644 --- a/src/exstruct/__init__.py +++ b/src/exstruct/__init__.py @@ -41,6 +41,10 @@ Shape, SheetData, WorkbookData, + col_index_to_alpha, + convert_row_keys_to_alpha, + convert_sheet_keys_to_alpha, + convert_workbook_keys_to_alpha, ) from .render import export_pdf, export_sheet_images @@ -82,19 +86,26 @@ "ColorsOptions", "serialize_workbook", "export_auto_page_breaks", + "col_index_to_alpha", + "convert_row_keys_to_alpha", + "convert_sheet_keys_to_alpha", + "convert_workbook_keys_to_alpha", ] ExtractionMode = Literal["light", "standard", "verbose"] -def extract(file_path: str | Path, mode: ExtractionMode = "standard") -> WorkbookData: +def extract( + file_path: str | Path, mode: ExtractionMode = "standard", *, alpha_col: bool = False +) -> WorkbookData: """ Extracts an Excel workbook into a WorkbookData structure. Parameters: file_path (str | Path): Path to the workbook file (.xlsx, .xlsm, .xls). mode (ExtractionMode): Extraction detail level. "light" includes cells and table detection only (no COM, shapes/charts empty; print areas via openpyxl). "standard" includes texted shapes, arrows, charts (COM if available) and print areas. "verbose" also includes shape/chart sizes, cell link map, colors map, and formulas map. + alpha_col: When True, convert CellRow column keys to Excel-style ABC names (A, B, ..., Z, AA, ...) instead of 0-based numeric strings. Returns: WorkbookData: Parsed workbook representation containing sheets, rows, shapes, charts, and print areas. @@ -108,6 +119,7 @@ def extract(file_path: str | Path, mode: ExtractionMode = "standard") -> Workboo include_cell_links=include_links, include_colors_map=include_colors_map, include_formulas_map=include_formulas_map, + alpha_col=alpha_col, ) ) return engine.extract(file_path, mode=mode) @@ -317,6 +329,8 @@ def process_excel( print_areas_dir: str | Path | None = None, auto_page_breaks_dir: str | Path | None = None, stream: TextIO | None = None, + *, + alpha_col: bool = False, ) -> None: """ Convenience wrapper: extract -> serialize (file or stdout) -> optional PDF/PNG. @@ -335,6 +349,8 @@ def process_excel( print_areas_dir: Directory to write per-print-area files (string or Path). auto_page_breaks_dir: Directory to write per-auto-page-break files (COM only). stream: IO override when output_path is None. + alpha_col: When True, convert CellRow column keys to Excel-style + ABC names (A, B, ...) instead of 0-based numeric strings. Raises: ValueError: If an unsupported format or mode is given. @@ -353,7 +369,7 @@ def process_excel( >>> process_excel(Path("input.xlsx"), output_path=Path("out.json"), pdf=True) # doctest: +SKIP """ engine = ExStructEngine( - options=StructOptions(mode=mode), + options=StructOptions(mode=mode, alpha_col=alpha_col), output=OutputOptions( format=FormatOptions(fmt=out_fmt, pretty=pretty, indent=indent), filters=FilterOptions( diff --git a/src/exstruct/cli/main.py b/src/exstruct/cli/main.py index 4a69923..e53d83f 100644 --- a/src/exstruct/cli/main.py +++ b/src/exstruct/cli/main.py @@ -109,6 +109,11 @@ def build_parser( availability if availability is not None else get_com_availability() ) _add_auto_page_breaks_argument(parser, resolved_availability) + parser.add_argument( + "--alpha-col", + action="store_true", + help="Output column keys as Excel-style ABC names (A, B, ..., Z, AA, ...) instead of 0-based indices.", + ) return parser @@ -143,6 +148,7 @@ def main(argv: list[str] | None = None) -> int: sheets_dir=args.sheets_dir, print_areas_dir=args.print_areas_dir, auto_page_breaks_dir=getattr(args, "auto_page_breaks_dir", None), + alpha_col=args.alpha_col, ) return 0 except Exception as e: diff --git a/src/exstruct/engine.py b/src/exstruct/engine.py index 7a97ff9..000fc7d 100644 --- a/src/exstruct/engine.py +++ b/src/exstruct/engine.py @@ -17,7 +17,7 @@ save_sheets, serialize_workbook, ) -from .models import SheetData, WorkbookData +from .models import SheetData, WorkbookData, convert_workbook_keys_to_alpha from .render import export_pdf, export_sheet_images ExtractionMode = Literal["light", "standard", "verbose"] @@ -76,6 +76,8 @@ class StructOptions: include_merged_cells: Whether to extract merged cell ranges. include_merged_values_in_rows: Whether to keep merged values in rows. colors: Color extraction options. + alpha_col: When True, convert CellRow column keys to Excel-style + ABC names (A, B, ..., Z, AA, ...) instead of 0-based numeric strings. """ mode: ExtractionMode = "standard" @@ -88,6 +90,7 @@ class StructOptions: include_merged_cells: bool | None = None # None -> auto: light=False, others=True include_merged_values_in_rows: bool = True colors: ColorsOptions = field(default_factory=ColorsOptions) + alpha_col: bool = False class FormatOptions(BaseModel): @@ -383,7 +386,7 @@ def extract( ) normalized_file_path = self._ensure_path(file_path) with self._table_params_scope(): - return extract_workbook( + workbook = extract_workbook( normalized_file_path, mode=chosen_mode, include_cell_links=self.options.include_cell_links, @@ -396,6 +399,9 @@ def extract( include_merged_cells=self.options.include_merged_cells, include_merged_values_in_rows=self.options.include_merged_values_in_rows, ) + if self.options.alpha_col: + workbook = convert_workbook_keys_to_alpha(workbook) + return workbook def serialize( self, diff --git a/src/exstruct/mcp/extract_runner.py b/src/exstruct/mcp/extract_runner.py index 2b287ec..aeb9509 100644 --- a/src/exstruct/mcp/extract_runner.py +++ b/src/exstruct/mcp/extract_runner.py @@ -38,6 +38,10 @@ class ExtractOptions(BaseModel): auto_page_breaks_dir: Path | None = Field( default=None, description="Directory for auto page-break outputs." ) + alpha_col: bool = Field( + default=False, + description="When true, convert CellRow column keys to Excel-style ABC names (A, B, ..., Z, AA, ...) instead of 0-based indices.", + ) class ExtractRequest(BaseModel): @@ -118,6 +122,7 @@ def run_extract( sheets_dir=sheets_dir, print_areas_dir=print_areas_dir, auto_page_breaks_dir=auto_page_breaks_dir, + alpha_col=options.alpha_col, ) meta, meta_warnings = _try_read_workbook_meta(resolved_input) warnings.extend(meta_warnings) diff --git a/src/exstruct/mcp/server.py b/src/exstruct/mcp/server.py index 034626e..a478993 100644 --- a/src/exstruct/mcp/server.py +++ b/src/exstruct/mcp/server.py @@ -216,7 +216,9 @@ async def _extract_tool( # pylint: disable=redefined-builtin format: Output format. out_dir: Optional output directory. out_name: Optional output filename. - options: Additional options (reserved for future use). + options: Additional options. Supports: pretty (bool), indent (int), + sheets_dir (str), print_areas_dir (str), auto_page_breaks_dir (str), + alpha_col (bool – convert column keys to Excel-style ABC names). Returns: Extraction result payload. diff --git a/src/exstruct/models/__init__.py b/src/exstruct/models/__init__.py index b25e7a5..6d798cc 100644 --- a/src/exstruct/models/__init__.py +++ b/src/exstruct/models/__init__.py @@ -3,7 +3,7 @@ from collections.abc import Generator import json from pathlib import Path -from typing import Literal +from typing import Literal, TypeVar from pydantic import BaseModel, ConfigDict, Field @@ -408,3 +408,133 @@ def save( case _: raise ValueError(f"Unsupported export format: {fmt}") return dest + + +# --------------------------------------------------------------------------- +# Column index ↔ Excel alpha-style column name conversion +# --------------------------------------------------------------------------- + + +def col_index_to_alpha(index: int) -> str: + """Convert a 0-based column index to an Excel-style column name. + + Args: + index: 0-based column index (0 → A, 25 → Z, 26 → AA, …). + + Returns: + Excel column name string. + + Raises: + ValueError: If index is negative. + + Examples: + >>> col_index_to_alpha(0) + 'A' + >>> col_index_to_alpha(25) + 'Z' + >>> col_index_to_alpha(26) + 'AA' + """ + if index < 0: + raise ValueError(f"Column index must be non-negative, got {index}") + result: list[str] = [] + num = index + while True: + num, remainder = divmod(num, 26) + result.append(chr(65 + remainder)) + if num == 0: + break + num -= 1 + result.reverse() + return "".join(result) + + +def convert_row_keys_to_alpha(row: CellRow) -> CellRow: + """Return a new CellRow with numeric column keys converted to ABC-style. + + Non-numeric keys are preserved unchanged. + + Args: + row: Original CellRow with 0-based numeric string keys. + + Returns: + New CellRow with alpha column keys. + """ + new_c = _convert_mapping_keys_to_alpha(row.c, row_index=row.r, field_name="c") + + new_links: dict[str, str] | None = None + if row.links: + new_links = _convert_mapping_keys_to_alpha( + row.links, row_index=row.r, field_name="links" + ) + + return CellRow(r=row.r, c=new_c, links=new_links) + + +def convert_sheet_keys_to_alpha(sheet: SheetData) -> SheetData: + """Return a new SheetData with all CellRow column keys converted to ABC-style. + + Args: + sheet: Original SheetData. + + Returns: + New SheetData with alpha column keys in every row. + """ + new_rows = [convert_row_keys_to_alpha(row) for row in sheet.rows] + return sheet.model_copy(update={"rows": new_rows}) + + +def convert_workbook_keys_to_alpha(workbook: WorkbookData) -> WorkbookData: + """Return a new WorkbookData with all CellRow column keys converted to ABC-style. + + Args: + workbook: Original WorkbookData. + + Returns: + New WorkbookData with alpha column keys in every sheet. + """ + new_sheets = { + name: convert_sheet_keys_to_alpha(sheet) + for name, sheet in workbook.sheets.items() + } + return workbook.model_copy(update={"sheets": new_sheets}) + + +def _alpha_key(key: str) -> str: + """Convert a numeric string key to alpha, passing non-numeric keys through.""" + try: + return col_index_to_alpha(int(key)) + except ValueError: + return key + + +MapValue = TypeVar("MapValue") + + +def _convert_mapping_keys_to_alpha( + source: dict[str, MapValue], *, row_index: int, field_name: str +) -> dict[str, MapValue]: + """Convert mapping keys to alpha-style and reject collisions. + + Args: + source: Original key-value mapping. + row_index: 1-based row index for error context. + field_name: Field name ("c" or "links") for error context. + + Returns: + Converted mapping with alpha-style keys. + + Raises: + ValueError: If multiple source keys map to the same alpha key. + """ + converted: dict[str, MapValue] = {} + for original_key, value in source.items(): + alpha_key = _alpha_key(original_key) + if alpha_key in converted: + raise ValueError( + "Column key collision after alpha conversion in " + f"row {row_index} ({field_name}): " + f"{original_key!r} -> {alpha_key!r}" + ) + converted[alpha_key] = value + return converted diff --git a/tests/cli/test_cli_alpha_col.py b/tests/cli/test_cli_alpha_col.py new file mode 100644 index 0000000..24f9c40 --- /dev/null +++ b/tests/cli/test_cli_alpha_col.py @@ -0,0 +1,78 @@ +"""Tests for the --alpha-col CLI flag.""" + +from __future__ import annotations + +import json +from pathlib import Path + +from openpyxl import Workbook + +from exstruct.cli.main import main as cli_main + + +def _create_workbook(tmp_path: Path) -> Path: + """Create a minimal Excel workbook for CLI tests. + + Args: + tmp_path: Temporary directory provided by pytest. + + Returns: + Path to the generated workbook. + """ + wb = Workbook() + ws = wb.active + ws.title = "Sheet1" + ws.append(["Hello", "World"]) + ws.append([1, 2]) + dest = tmp_path / "alpha_test.xlsx" + wb.save(dest) + wb.close() + return dest + + +def test_cli_alpha_col_flag(tmp_path: Path) -> None: + """--alpha-col should produce ABC-style column keys in output.""" + xlsx = _create_workbook(tmp_path) + out = tmp_path / "out.json" + code = cli_main([str(xlsx), "-o", str(out), "--alpha-col", "--pretty"]) + assert code == 0 + data = json.loads(out.read_text(encoding="utf-8")) + rows = data["sheets"]["Sheet1"]["rows"] + # With alpha_col, column keys should be A, B, ... not 0, 1, ... + for row in rows: + cell_keys = set(row["c"].keys()) + assert "A" in cell_keys + assert "B" in cell_keys + assert "0" not in cell_keys + + +def test_cli_without_alpha_col_flag(tmp_path: Path) -> None: + """Without --alpha-col, column keys should remain 0-based numeric.""" + xlsx = _create_workbook(tmp_path) + out = tmp_path / "out.json" + code = cli_main([str(xlsx), "-o", str(out), "--pretty"]) + assert code == 0 + data = json.loads(out.read_text(encoding="utf-8")) + rows = data["sheets"]["Sheet1"]["rows"] + for row in rows: + cell_keys = set(row["c"].keys()) + assert "0" in cell_keys + assert "A" not in cell_keys + + +def test_cli_alpha_col_parser_present() -> None: + """The --alpha-col flag should be accepted by the parser.""" + from exstruct.cli.main import build_parser + + parser = build_parser() + args = parser.parse_args(["dummy.xlsx", "--alpha-col"]) + assert args.alpha_col is True + + +def test_cli_alpha_col_default_false() -> None: + """Without --alpha-col, default should be False.""" + from exstruct.cli.main import build_parser + + parser = build_parser() + args = parser.parse_args(["dummy.xlsx"]) + assert args.alpha_col is False diff --git a/tests/engine/test_engine_alpha_col.py b/tests/engine/test_engine_alpha_col.py new file mode 100644 index 0000000..0c1a51a --- /dev/null +++ b/tests/engine/test_engine_alpha_col.py @@ -0,0 +1,51 @@ +"""Tests for alpha_col option through the engine and public API layers.""" + +from __future__ import annotations + +from pathlib import Path + +from _pytest.monkeypatch import MonkeyPatch + +from exstruct.engine import ExStructEngine, StructOptions +from exstruct.models import CellRow, SheetData, WorkbookData + + +def _fake_workbook(path: Path, **kwargs: object) -> WorkbookData: + """Return a minimal WorkbookData with numeric column keys. + + Args: + path: Input path (unused but required by signature). + **kwargs: Extra keyword arguments accepted for forward-compatibility. + + Returns: + A WorkbookData with one sheet and one row. + """ + return WorkbookData( + book_name=path.name, + sheets={ + "Sheet1": SheetData( + rows=[ + CellRow(r=1, c={"0": "A1", "1": "B1", "25": "Z1"}, links=None), + ] + ) + }, + ) + + +def test_engine_alpha_col_false(monkeypatch: MonkeyPatch, tmp_path: Path) -> None: + """With alpha_col=False, column keys remain numeric.""" + monkeypatch.setattr("exstruct.engine.extract_workbook", _fake_workbook) + engine = ExStructEngine(options=StructOptions(mode="light", alpha_col=False)) + result = engine.extract(tmp_path / "book.xlsx", mode="light") + row = result.sheets["Sheet1"].rows[0] + assert "0" in row.c + assert "1" in row.c + + +def test_engine_alpha_col_true(monkeypatch: MonkeyPatch, tmp_path: Path) -> None: + """With alpha_col=True, column keys are converted to ABC-style.""" + monkeypatch.setattr("exstruct.engine.extract_workbook", _fake_workbook) + engine = ExStructEngine(options=StructOptions(mode="light", alpha_col=True)) + result = engine.extract(tmp_path / "book.xlsx", mode="light") + row = result.sheets["Sheet1"].rows[0] + assert row.c == {"A": "A1", "B": "B1", "Z": "Z1"} diff --git a/tests/mcp/test_extract_alpha_col.py b/tests/mcp/test_extract_alpha_col.py new file mode 100644 index 0000000..3c6c9d0 --- /dev/null +++ b/tests/mcp/test_extract_alpha_col.py @@ -0,0 +1,24 @@ +"""Tests for alpha_col support in MCP extract_runner.""" + +from __future__ import annotations + +from exstruct.mcp.extract_runner import ExtractOptions + + +def test_extract_options_alpha_col_default() -> None: + """alpha_col should default to False.""" + opts = ExtractOptions() + assert opts.alpha_col is False + + +def test_extract_options_alpha_col_true() -> None: + """alpha_col should be settable to True.""" + opts = ExtractOptions(alpha_col=True) + assert opts.alpha_col is True + + +def test_extract_options_from_dict() -> None: + """alpha_col should be parseable from a plain dict (MCP JSON payload).""" + opts = ExtractOptions.model_validate({"alpha_col": True, "pretty": True}) + assert opts.alpha_col is True + assert opts.pretty is True diff --git a/tests/models/test_alpha_col.py b/tests/models/test_alpha_col.py new file mode 100644 index 0000000..321d7cd --- /dev/null +++ b/tests/models/test_alpha_col.py @@ -0,0 +1,134 @@ +"""Tests for alpha column key conversion utilities.""" + +from __future__ import annotations + +import pytest + +from exstruct.models import ( + CellRow, + SheetData, + WorkbookData, + col_index_to_alpha, + convert_row_keys_to_alpha, + convert_sheet_keys_to_alpha, + convert_workbook_keys_to_alpha, +) + + +class TestColIndexToAlpha: + """Tests for col_index_to_alpha().""" + + def test_known_values(self) -> None: + cases: list[tuple[int, str]] = [ + (0, "A"), + (1, "B"), + (25, "Z"), + (26, "AA"), + (27, "AB"), + (51, "AZ"), + (52, "BA"), + (701, "ZZ"), + (702, "AAA"), + ] + for index, expected in cases: + assert col_index_to_alpha(index) == expected + + def test_negative_raises(self) -> None: + with pytest.raises(ValueError, match="non-negative"): + col_index_to_alpha(-1) + + +class TestConvertRowKeysToAlpha: + """Tests for convert_row_keys_to_alpha().""" + + def test_basic_numeric_keys(self) -> None: + row = CellRow(r=1, c={"0": "val_A", "1": "val_B", "2": "val_C"}, links=None) + result = convert_row_keys_to_alpha(row) + assert result.c == {"A": "val_A", "B": "val_B", "C": "val_C"} + assert result.r == 1 + + def test_links_converted(self) -> None: + row = CellRow( + r=2, + c={"0": "text"}, + links={"0": "http://example.com"}, + ) + result = convert_row_keys_to_alpha(row) + assert result.links == {"A": "http://example.com"} + + def test_no_links(self) -> None: + row = CellRow(r=1, c={"0": 1}, links=None) + result = convert_row_keys_to_alpha(row) + assert result.links is None + + def test_non_numeric_keys_pass_through(self) -> None: + row = CellRow(r=1, c={"X": "val"}, links=None) + result = convert_row_keys_to_alpha(row) + assert result.c == {"X": "val"} + + def test_high_columns(self) -> None: + row = CellRow(r=1, c={"26": "AA_col"}, links=None) + result = convert_row_keys_to_alpha(row) + assert result.c == {"AA": "AA_col"} + + def test_raises_on_collision_in_cells(self) -> None: + row = CellRow(r=3, c={"0": "num_zero", "A": "alpha_a"}, links=None) + with pytest.raises(ValueError, match="collision"): + convert_row_keys_to_alpha(row) + + def test_raises_on_collision_in_links(self) -> None: + row = CellRow( + r=4, + c={"0": "value"}, + links={"0": "https://numeric.example", "A": "https://alpha.example"}, + ) + with pytest.raises(ValueError, match="collision"): + convert_row_keys_to_alpha(row) + + +class TestConvertSheetKeysToAlpha: + """Tests for convert_sheet_keys_to_alpha().""" + + def test_converts_all_rows(self) -> None: + rows = [ + CellRow(r=1, c={"0": "A1", "1": "B1"}, links=None), + CellRow(r=2, c={"0": "A2", "2": "C2"}, links=None), + ] + sheet = SheetData(rows=rows) + result = convert_sheet_keys_to_alpha(sheet) + assert result.rows[0].c == {"A": "A1", "B": "B1"} + assert result.rows[1].c == {"A": "A2", "C": "C2"} + + def test_preserves_other_fields(self) -> None: + sheet = SheetData( + rows=[CellRow(r=1, c={"0": "v"}, links=None)], + shapes=[], + charts=[], + ) + result = convert_sheet_keys_to_alpha(sheet) + assert result.shapes == [] + assert result.charts == [] + + +class TestConvertWorkbookKeysToAlpha: + """Tests for convert_workbook_keys_to_alpha().""" + + def test_converts_all_sheets(self) -> None: + wb = WorkbookData( + book_name="test.xlsx", + sheets={ + "Sheet1": SheetData( + rows=[CellRow(r=1, c={"0": "a", "1": "b"}, links=None)] + ), + "Sheet2": SheetData(rows=[CellRow(r=1, c={"2": "c"}, links=None)]), + }, + ) + result = convert_workbook_keys_to_alpha(wb) + assert result.sheets["Sheet1"].rows[0].c == {"A": "a", "B": "b"} + assert result.sheets["Sheet2"].rows[0].c == {"C": "c"} + assert result.book_name == "test.xlsx" + + def test_empty_workbook(self) -> None: + wb = WorkbookData(book_name="empty.xlsx", sheets={}) + result = convert_workbook_keys_to_alpha(wb) + assert result.sheets == {} diff --git a/uv.lock b/uv.lock index 6fbb501..c1e7930 100644 --- a/uv.lock +++ b/uv.lock @@ -620,7 +620,7 @@ wheels = [ [[package]] name = "exstruct" -version = "0.4.3" +version = "0.4.4" source = { editable = "." } dependencies = [ { name = "numpy" }, From 9cc0f696dbe34b37008339e347575d5f94fe70e3 Mon Sep 17 00:00:00 2001 From: harumiWeb Date: Sun, 8 Feb 2026 17:46:03 +0900 Subject: [PATCH 12/25] feat: add merged_ranges output for alpha_col option and update related specifications and tests --- docs/agents/FEATURE_SPEC.md | 22 ++++++++++++++++ docs/agents/TASKS.md | 38 +++++++++++++++++---------- src/exstruct/engine.py | 3 +++ src/exstruct/models/__init__.py | 27 ++++++++++++++++++- tests/engine/test_engine_alpha_col.py | 22 ++++++++++++++-- tests/models/test_alpha_col.py | 20 ++++++++++++++ 6 files changed, 115 insertions(+), 17 deletions(-) diff --git a/docs/agents/FEATURE_SPEC.md b/docs/agents/FEATURE_SPEC.md index 198bcfc..1e88833 100644 --- a/docs/agents/FEATURE_SPEC.md +++ b/docs/agents/FEATURE_SPEC.md @@ -414,3 +414,25 @@ Excel上の列名(A, M, S 等)との対応が直感的に取れず、**誤 - `alpha_col=False`(デフォルト)で従来通り数値キー - CLI `--alpha-col` フラグが正しく `process_excel()` に伝播する - MCP `exstruct_extract` の `options.alpha_col` がエンジンに伝播する + +### 13.9 `merged_ranges` 出力(`alpha_col` 時) + +#### 背景 + +`alpha_col=True` 時に `CellRow.c` / `links` は ABC 列名になるため、`merged_cells.items` +(`(r1, c1, r2, c2, v)` の数値列インデックス)との参照体系が混在する。 +AIエージェントの編集座標推論の一貫性を高めるため、A1形式の `merged_ranges` を別フィールドで出力する。 + +#### 仕様 + +- `SheetData` に `merged_ranges: list[str]` を追加する(例: `["A1:C3", "F10:G12"]`) +- `alpha_col=True` のときのみ `merged_ranges` を生成する +- `alpha_col=True` では `merged_cells` は出力しない(可読性優先) +- `alpha_col=False` のときは従来どおり `merged_cells` を出力し、`merged_ranges` は空とする + +#### 受け入れ基準(追加) + +- `alpha_col=True` かつ `merged_cells` が存在するシートで `merged_ranges` が出力される +- `merged_ranges` の各要素は `"A1:C3"` 形式で、`items` と同じ範囲を表す +- `alpha_col=True` では `merged_cells` は出力されない +- `alpha_col=False` では `merged_ranges` は空(実質非出力)となる diff --git a/docs/agents/TASKS.md b/docs/agents/TASKS.md index 779911a..c05c58e 100644 --- a/docs/agents/TASKS.md +++ b/docs/agents/TASKS.md @@ -45,17 +45,27 @@ ## ABC列名キー出力オプション(`alpha_col`) -- [ ] `models/__init__.py`: `col_index_to_alpha()` 変換ユーティリティを追加する -- [ ] `models/__init__.py`: `convert_row_keys_to_alpha()` / `convert_sheet_keys_to_alpha()` を追加する -- [ ] `engine.py`: `StructOptions` に `alpha_col: bool = False` を追加する -- [ ] `engine.py`: `ExStructEngine.extract()` で `alpha_col=True` 時に変換を適用する -- [ ] `__init__.py`: `extract()` / `process_excel()` に `alpha_col` パラメータを追加する -- [ ] `cli/main.py`: `--alpha-col` CLI フラグを追加する -- [ ] `mcp/extract_runner.py`: `ExtractOptions` に `alpha_col` を追加し、`process_excel()` へ連携する -- [ ] `mcp/server.py`: `exstruct_extract` ツールに `alpha_col` パラメータを追加する -- [ ] `tests/models/`: `col_index_to_alpha()` の単体テストを追加する -- [ ] `tests/models/`: `convert_row_keys_to_alpha()` / `convert_sheet_keys_to_alpha()` のテストを追加する -- [ ] `tests/engine/`: `alpha_col=True` でのエンジン抽出テストを追加する -- [ ] `tests/cli/`: `--alpha-col` CLI フラグのテストを追加する -- [ ] `tests/mcp/`: MCP extract の `alpha_col` 連携テストを追加する -- [ ] `ruff check .` / `mypy src/exstruct --strict` / 関連pytest を実行し、結果を記録する +- [x] `models/__init__.py`: `col_index_to_alpha()` 変換ユーティリティを追加する +- [x] `models/__init__.py`: `convert_row_keys_to_alpha()` / `convert_sheet_keys_to_alpha()` を追加する +- [x] `engine.py`: `StructOptions` に `alpha_col: bool = False` を追加する +- [x] `engine.py`: `ExStructEngine.extract()` で `alpha_col=True` 時に変換を適用する +- [x] `__init__.py`: `extract()` / `process_excel()` に `alpha_col` パラメータを追加する +- [x] `cli/main.py`: `--alpha-col` CLI フラグを追加する +- [x] `mcp/extract_runner.py`: `ExtractOptions` に `alpha_col` を追加し、`process_excel()` へ連携する +- [x] `mcp/server.py`: `exstruct_extract` ツールに `alpha_col` パラメータを追加する +- [x] `tests/models/`: `col_index_to_alpha()` の単体テストを追加する +- [x] `tests/models/`: `convert_row_keys_to_alpha()` / `convert_sheet_keys_to_alpha()` のテストを追加する +- [x] `tests/engine/`: `alpha_col=True` でのエンジン抽出テストを追加する +- [x] `tests/cli/`: `--alpha-col` CLI フラグのテストを追加する +- [x] `tests/mcp/`: MCP extract の `alpha_col` 連携テストを追加する +- [x] `ruff check .` / `mypy src/exstruct --strict` / 関連pytest を実行し、結果を記録する + +## `merged_ranges` 出力(`alpha_col`時) + +- [x] `docs/agents/FEATURE_SPEC.md`: `merged_ranges` 仕様に更新する +- [x] `models/__init__.py`: `SheetData` に `merged_ranges` フィールドを追加する +- [x] `models/__init__.py`: `alpha_col=True` 変換時に `merged_cells.items` から `merged_ranges` を生成する +- [x] `models/__init__.py`: `alpha_col=True` 変換時に `merged_cells` を非表示化する +- [x] `tests/models/test_alpha_col.py`: `merged_ranges` 生成と非生成(alpha_col=False相当)を検証する +- [x] `tests/engine/test_engine_alpha_col.py`: エンジン経由で `merged_ranges` が出力されることを検証する +- [x] `uv run task precommit-run` を実行し、静的解析・型検査の通過を確認する diff --git a/src/exstruct/engine.py b/src/exstruct/engine.py index 000fc7d..3469c53 100644 --- a/src/exstruct/engine.py +++ b/src/exstruct/engine.py @@ -317,6 +317,9 @@ def _filter_sheet( merged_cells=sheet.merged_cells if self.output.filters.include_merged_cells else None, + merged_ranges=sheet.merged_ranges + if self.output.filters.include_merged_cells + else [], ) def _filter_workbook( diff --git a/src/exstruct/models/__init__.py b/src/exstruct/models/__init__.py index 6d798cc..e9e8d3f 100644 --- a/src/exstruct/models/__init__.py +++ b/src/exstruct/models/__init__.py @@ -194,6 +194,13 @@ class SheetData(BaseModel): merged_cells: MergedCells | None = Field( default=None, description="Merged cell ranges on the sheet." ) + merged_ranges: list[str] = Field( + default_factory=list, + description=( + "Merged ranges in A1 notation (e.g., 'A1:C3'). " + "Used in alpha_col-oriented output." + ), + ) def _as_payload(self) -> dict[str, object]: from ..io import dict_without_empty_values @@ -481,7 +488,13 @@ def convert_sheet_keys_to_alpha(sheet: SheetData) -> SheetData: New SheetData with alpha column keys in every row. """ new_rows = [convert_row_keys_to_alpha(row) for row in sheet.rows] - return sheet.model_copy(update={"rows": new_rows}) + updated_fields: dict[str, object] = {"rows": new_rows} + if sheet.merged_cells is not None: + updated_fields["merged_ranges"] = _merged_items_to_a1_ranges( + sheet.merged_cells.items + ) + updated_fields["merged_cells"] = None + return sheet.model_copy(update=updated_fields) def convert_workbook_keys_to_alpha(workbook: WorkbookData) -> WorkbookData: @@ -538,3 +551,15 @@ def _convert_mapping_keys_to_alpha( ) converted[alpha_key] = value return converted + + +def _merged_items_to_a1_ranges( + items: list[tuple[int, int, int, int, str]], +) -> list[str]: + """Convert merged cell tuples to A1 range strings.""" + ranges: list[str] = [] + for r1, c1, r2, c2, _value in items: + start = f"{col_index_to_alpha(c1)}{r1}" + end = f"{col_index_to_alpha(c2)}{r2}" + ranges.append(f"{start}:{end}") + return ranges diff --git a/tests/engine/test_engine_alpha_col.py b/tests/engine/test_engine_alpha_col.py index 0c1a51a..f2f1b49 100644 --- a/tests/engine/test_engine_alpha_col.py +++ b/tests/engine/test_engine_alpha_col.py @@ -2,12 +2,13 @@ from __future__ import annotations +import json from pathlib import Path from _pytest.monkeypatch import MonkeyPatch from exstruct.engine import ExStructEngine, StructOptions -from exstruct.models import CellRow, SheetData, WorkbookData +from exstruct.models import CellRow, MergedCells, SheetData, WorkbookData def _fake_workbook(path: Path, **kwargs: object) -> WorkbookData: @@ -26,7 +27,8 @@ def _fake_workbook(path: Path, **kwargs: object) -> WorkbookData: "Sheet1": SheetData( rows=[ CellRow(r=1, c={"0": "A1", "1": "B1", "25": "Z1"}, links=None), - ] + ], + merged_cells=MergedCells(items=[(1, 0, 3, 2, "merged")]), ) }, ) @@ -40,6 +42,7 @@ def test_engine_alpha_col_false(monkeypatch: MonkeyPatch, tmp_path: Path) -> Non row = result.sheets["Sheet1"].rows[0] assert "0" in row.c assert "1" in row.c + assert result.sheets["Sheet1"].merged_ranges == [] def test_engine_alpha_col_true(monkeypatch: MonkeyPatch, tmp_path: Path) -> None: @@ -49,3 +52,18 @@ def test_engine_alpha_col_true(monkeypatch: MonkeyPatch, tmp_path: Path) -> None result = engine.extract(tmp_path / "book.xlsx", mode="light") row = result.sheets["Sheet1"].rows[0] assert row.c == {"A": "A1", "B": "B1", "Z": "Z1"} + assert result.sheets["Sheet1"].merged_ranges == ["A1:C3"] + assert result.sheets["Sheet1"].merged_cells is None + + +def test_engine_serialize_alpha_col_includes_merged_ranges( + monkeypatch: MonkeyPatch, tmp_path: Path +) -> None: + """Serialized output should include merged_ranges when alpha_col=True.""" + monkeypatch.setattr("exstruct.engine.extract_workbook", _fake_workbook) + engine = ExStructEngine(options=StructOptions(mode="light", alpha_col=True)) + result = engine.extract(tmp_path / "book.xlsx", mode="light") + payload = json.loads(engine.serialize(result, fmt="json")) + sheet = payload["sheets"]["Sheet1"] + assert sheet["merged_ranges"] == ["A1:C3"] + assert "merged_cells" not in sheet diff --git a/tests/models/test_alpha_col.py b/tests/models/test_alpha_col.py index 321d7cd..93f1ed9 100644 --- a/tests/models/test_alpha_col.py +++ b/tests/models/test_alpha_col.py @@ -6,6 +6,7 @@ from exstruct.models import ( CellRow, + MergedCells, SheetData, WorkbookData, col_index_to_alpha, @@ -109,6 +110,25 @@ def test_preserves_other_fields(self) -> None: assert result.shapes == [] assert result.charts == [] + def test_moves_merged_cells_to_merged_ranges(self) -> None: + sheet = SheetData( + rows=[CellRow(r=1, c={"0": "v"}, links=None)], + merged_cells=MergedCells( + items=[(1, 0, 3, 2, "merged"), (10, 25, 10, 26, "x")] + ), + ) + result = convert_sheet_keys_to_alpha(sheet) + assert result.merged_ranges == ["A1:C3", "Z10:AA10"] + assert result.merged_cells is None + + def test_no_merged_cells_keeps_none(self) -> None: + sheet = SheetData( + rows=[CellRow(r=1, c={"0": "v"}, links=None)], merged_cells=None + ) + result = convert_sheet_keys_to_alpha(sheet) + assert result.merged_cells is None + assert result.merged_ranges == [] + class TestConvertWorkbookKeysToAlpha: """Tests for convert_workbook_keys_to_alpha().""" From a555b699b914d6a9c9e38b5662b32cbde0e452b8 Mon Sep 17 00:00:00 2001 From: harumiWeb Date: Mon, 16 Feb 2026 21:23:23 +0900 Subject: [PATCH 13/25] feat: update MCP documentation and extract options to reflect alpha_col default as true --- README.md | 2 ++ docs/README.en.md | 2 ++ docs/README.ja.md | 3 +++ docs/mcp.md | 32 +++++++++++++++++++++++++++++ src/exstruct/mcp/extract_runner.py | 4 ++-- tests/mcp/test_extract_alpha_col.py | 4 ++-- 6 files changed, 43 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 20e5283..04eb201 100644 --- a/README.md +++ b/README.md @@ -73,11 +73,13 @@ exstruct-mcp --root C:\data --log-file C:\logs\exstruct-mcp.log --on-conflict re Available tools: - `exstruct_extract` +- `exstruct_patch` - `exstruct_read_json_chunk` - `exstruct_validate_input` Notes: +- In MCP, `exstruct_extract` defaults to `options.alpha_col=true` (column keys: `A`, `B`, ...). Set `options.alpha_col=false` for legacy 0-based numeric string keys. - Logs go to stderr (and optionally `--log-file`) to avoid contaminating stdio responses. - On Windows with Excel, standard/verbose can use COM for richer extraction. On non-Windows, COM is unavailable and extraction uses openpyxl-based fallbacks. diff --git a/docs/README.en.md b/docs/README.en.md index c7a99cb..d47dcc8 100644 --- a/docs/README.en.md +++ b/docs/README.en.md @@ -73,11 +73,13 @@ exstruct-mcp --root C:\data --log-file C:\logs\exstruct-mcp.log --on-conflict re Available tools: - `exstruct_extract` +- `exstruct_patch` - `exstruct_read_json_chunk` - `exstruct_validate_input` Notes: +- In MCP, `exstruct_extract` defaults to `options.alpha_col=true` (column keys: `A`, `B`, ...). Set `options.alpha_col=false` for legacy 0-based numeric string keys. - Logs go to stderr (and optionally `--log-file`) to avoid contaminating stdio responses. - On Windows with Excel, standard/verbose can use COM for richer extraction. On non-Windows, COM is unavailable and extraction uses openpyxl-based fallbacks. diff --git a/docs/README.ja.md b/docs/README.ja.md index f19b011..cc11956 100644 --- a/docs/README.ja.md +++ b/docs/README.ja.md @@ -70,9 +70,12 @@ exstruct-mcp --root C:\data --log-file C:\logs\exstruct-mcp.log --on-conflict re 利用可能なツール: - `exstruct_extract` +- `exstruct_patch` - `exstruct_read_json_chunk` - `exstruct_validate_input` +- MCPでは `exstruct_extract` の `options.alpha_col=true` が既定です(列キーは `A`, `B`, ...)。従来の0始まり数値キーが必要な場合は `options.alpha_col=false` を指定してください。 + 注意点: - 標準入出力の応答を汚染しないよう、ログは標準エラー出力(およびオプションで`--log-file`で指定したファイル)に出力されます。 diff --git a/docs/mcp.md b/docs/mcp.md index 7bd23ab..285a8c2 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -6,6 +6,7 @@ so AI agents can call it safely as a tool. ## What it provides - Convert Excel into structured JSON (file output) +- Edit Excel by applying patch operations (cell/sheet updates) - Read large JSON outputs in chunks - Pre-validate input files @@ -33,14 +34,45 @@ exstruct-mcp --root C:\\data --log-file C:\\logs\\exstruct-mcp.log --on-conflict ## Tools - `exstruct_extract` +- `exstruct_patch` - `exstruct_read_json_chunk` - `exstruct_validate_input` +### `exstruct_extract` defaults + +- `options.alpha_col` defaults to `true` in MCP (column keys become `A`, `B`, ...). +- Set `options.alpha_col=false` if you need legacy 0-based numeric string keys. + ## Basic flow 1. Call `exstruct_extract` to generate the output JSON file 2. Use `exstruct_read_json_chunk` to read only the parts you need +## Edit flow (patch) + +1. Inspect workbook structure with `exstruct_extract` (and `exstruct_read_json_chunk` if needed) +2. Build patch operations (`ops`) for target cells/sheets +3. Call `exstruct_patch` to apply edits +4. Re-run `exstruct_extract` to verify results if needed + +### `exstruct_patch` highlights + +- Atomic apply: all operations succeed, or no changes are saved +- Supports: + - `set_value` + - `set_formula` + - `add_sheet` + - `set_range_values` + - `fill_formula` + - `set_value_if` + - `set_formula_if` +- Useful flags: + - `dry_run`: compute diff only (no file write) + - `return_inverse_ops`: return undo operations + - `preflight_formula_check`: detect formula issues before save + - `auto_formula`: treat `=...` in `set_value` as formula +- Conflict handling follows server `--on-conflict` unless overridden per tool call + ## AI agent configuration examples ### Codex diff --git a/src/exstruct/mcp/extract_runner.py b/src/exstruct/mcp/extract_runner.py index aeb9509..387bc00 100644 --- a/src/exstruct/mcp/extract_runner.py +++ b/src/exstruct/mcp/extract_runner.py @@ -39,8 +39,8 @@ class ExtractOptions(BaseModel): default=None, description="Directory for auto page-break outputs." ) alpha_col: bool = Field( - default=False, - description="When true, convert CellRow column keys to Excel-style ABC names (A, B, ..., Z, AA, ...) instead of 0-based indices.", + default=True, + description="When true, convert CellRow column keys to Excel-style ABC names (A, B, ..., Z, AA, ...) instead of 0-based indices. MCP default is true.", ) diff --git a/tests/mcp/test_extract_alpha_col.py b/tests/mcp/test_extract_alpha_col.py index 3c6c9d0..c02516a 100644 --- a/tests/mcp/test_extract_alpha_col.py +++ b/tests/mcp/test_extract_alpha_col.py @@ -6,9 +6,9 @@ def test_extract_options_alpha_col_default() -> None: - """alpha_col should default to False.""" + """alpha_col should default to True in MCP.""" opts = ExtractOptions() - assert opts.alpha_col is False + assert opts.alpha_col is True def test_extract_options_alpha_col_true() -> None: From 8421741bbf4113f4fd8b24f467e4a62e06c38cc4 Mon Sep 17 00:00:00 2001 From: harumiWeb Date: Mon, 16 Feb 2026 21:42:38 +0900 Subject: [PATCH 14/25] feat: normalize `ops` input in `exstruct_patch` to accept both object lists and JSON strings --- docs/agents/FEATURE_SPEC.md | 48 +++++++++++ docs/agents/TASKS.md | 12 +++ docs/mcp.md | 2 + src/exstruct/mcp/server.py | 88 ++++++++++++++++++--- tests/mcp/test_server.py | 154 ++++++++++++++++++++++++++++++++++++ 5 files changed, 295 insertions(+), 9 deletions(-) diff --git a/docs/agents/FEATURE_SPEC.md b/docs/agents/FEATURE_SPEC.md index 1e88833..2f009c1 100644 --- a/docs/agents/FEATURE_SPEC.md +++ b/docs/agents/FEATURE_SPEC.md @@ -436,3 +436,51 @@ AIエージェントの編集座標推論の一貫性を高めるため、A1形 - `merged_ranges` の各要素は `"A1:C3"` 形式で、`items` と同じ範囲を表す - `alpha_col=True` では `merged_cells` は出力されない - `alpha_col=False` では `merged_ranges` は空(実質非出力)となる + +--- + +## 14. `exstruct_patch.ops` 入力スキーマ整合(AIエージェント運用改善) + +### 14.1 背景 + +一部のMCPクライアントは、`ops` を `PatchOp` オブジェクト配列ではなく、 +JSON文字列配列として送信する場合がある。 +この差異により、AIエージェントがツール定義どおりに呼び出しても +入力バリデーションで失敗するケースがある。 + +### 14.2 目的 + +- `exstruct_patch` の受け口を明確化し、実装とツール入力スキーマの体験差を解消する +- AIエージェントがクライアント差異を意識せず patch を実行できるようにする + +### 14.3 入力仕様 + +- 正式入力: `ops: list[object]`(各要素は `PatchOp` 相当のJSONオブジェクト) +- 互換入力: `ops: list[str]`(各要素は `PatchOp` オブジェクトを表すJSON文字列) +- サーバー内部で次を行う: + - object はそのまま採用 + - string は `json.loads()` で object に変換 + - 変換後に `PatchToolInput`(`ops: list[PatchOp]`)で厳密検証 + +### 14.4 エラー仕様 + +- JSON文字列のパース失敗時: + - `ValueError` + - メッセージに失敗した `ops[index]` を含める + - 「object 形式の推奨入力例」を併記する +- JSONとして有効でもobjectでない場合(配列/数値など): + - `ValueError` + - 「JSON object 必須」を明記する + +### 14.5 後方互換性 + +- 既存の object 配列クライアントは変更不要 +- string 配列クライアントはサーバー側で吸収し、破壊的変更を回避する +- `PatchOp` 自体のスキーマと patch 実行ロジックは変更しない + +### 14.6 受け入れ基準 + +- object 配列入力で従来どおり patch が実行できる +- JSON文字列配列入力でも patch が実行できる +- 不正JSON文字列で `ops[index]` を含む明確なエラーが返る +- object 以外のJSON型入力で「object必須」エラーが返る diff --git a/docs/agents/TASKS.md b/docs/agents/TASKS.md index c05c58e..030de15 100644 --- a/docs/agents/TASKS.md +++ b/docs/agents/TASKS.md @@ -69,3 +69,15 @@ - [x] `tests/models/test_alpha_col.py`: `merged_ranges` 生成と非生成(alpha_col=False相当)を検証する - [x] `tests/engine/test_engine_alpha_col.py`: エンジン経由で `merged_ranges` が出力されることを検証する - [x] `uv run task precommit-run` を実行し、静的解析・型検査の通過を確認する + +## `exstruct_patch.ops` 入力スキーマ整合(AIエージェント運用改善) + +- [x] `docs/agents/FEATURE_SPEC.md`: `ops` の正式入力(object配列)と互換入力(JSON文字列配列)を定義する +- [x] `mcp/server.py`: `ops` 正規化ヘルパー(object/string両対応)を追加する +- [x] `mcp/server.py`: `_patch_tool` で正規化済み `ops` を `PatchToolInput` に渡す +- [x] `mcp/server.py`: 不正JSON時に `ops[index]` と入力例を含むエラー文言を返す +- [x] `tests/mcp/test_server.py`: `ops` 正規化ヘルパーの単体テスト(成功/失敗)を追加する +- [x] `tests/mcp/test_server.py`: `exstruct_patch` が JSON文字列配列 `ops` を受理できることを検証する +- [x] `tests/mcp/test_server.py`: 不正JSON文字列 `ops` が明確な `ValueError` になることを検証する +- [x] `ruff check src/exstruct/mcp/server.py tests/mcp/test_server.py` を実行し、静的解析を確認する +- [x] `pytest tests/mcp/test_server.py` を実行し、回帰がないことを確認する diff --git a/docs/mcp.md b/docs/mcp.md index 285a8c2..a52c16f 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -58,6 +58,8 @@ exstruct-mcp --root C:\\data --log-file C:\\logs\\exstruct-mcp.log --on-conflict ### `exstruct_patch` highlights - Atomic apply: all operations succeed, or no changes are saved +- `ops` accepts an object list as the canonical form. + For compatibility, JSON object strings in `ops` are also accepted and normalized. - Supports: - `set_value` - `set_formula` diff --git a/src/exstruct/mcp/server.py b/src/exstruct/mcp/server.py index a478993..ddc84d9 100644 --- a/src/exstruct/mcp/server.py +++ b/src/exstruct/mcp/server.py @@ -3,6 +3,7 @@ import argparse import functools import importlib +import json import logging import os from pathlib import Path @@ -16,7 +17,6 @@ from .extract_runner import OnConflictPolicy from .io import PathPolicy -from .patch_runner import PatchOp from .tools import ( ExtractToolInput, ExtractToolOutput, @@ -305,7 +305,7 @@ async def _validate_input_tool(xlsx_path: str) -> ValidateInputToolOutput: async def _patch_tool( xlsx_path: str, - ops: list[PatchOp], + ops: list[dict[str, Any] | str], out_dir: str | None = None, out_name: str | None = None, on_conflict: OnConflictPolicy | None = None, @@ -321,12 +321,16 @@ async def _patch_tool( Args: xlsx_path: Path to the Excel workbook to edit. - ops: Patch operations to apply in order. Each op has an 'op' field - specifying the type: 'set_value' (set cell value), 'set_formula' - (set cell formula starting with '='), 'add_sheet' (create new sheet), - 'set_range_values' (bulk set rectangular range), 'fill_formula' - (fill formula across a row/column), 'set_value_if' (conditional - value update), 'set_formula_if' (conditional formula update). + ops: Patch operations to apply in order. Preferred format is an + object list (one object per operation). For compatibility with + clients that cannot send object arrays, JSON object strings are + also accepted and normalized before validation. Each operation + has an 'op' field specifying the type: 'set_value' (set cell + value), 'set_formula' (set cell formula starting with '='), + 'add_sheet' (create new sheet), 'set_range_values' (bulk set + rectangular range), 'fill_formula' (fill formula across a + row/column), 'set_value_if' (conditional value update), + 'set_formula_if' (conditional formula update). out_dir: Output directory. Defaults to same directory as input. out_name: Output filename. Defaults to '{stem}_patched{ext}'. on_conflict: Conflict policy when output file exists: @@ -342,9 +346,10 @@ async def _patch_tool( Returns: Patch result with output path, applied diffs, and any warnings. """ + normalized_ops = _coerce_patch_ops(ops) payload = PatchToolInput( xlsx_path=xlsx_path, - ops=ops, + ops=normalized_ops, out_dir=out_dir, out_name=out_name, on_conflict=on_conflict, @@ -379,3 +384,68 @@ def _coerce_filter(filter_data: dict[str, Any] | None) -> dict[str, Any] | None: if not filter_data: return None return dict(filter_data) + + +def _coerce_patch_ops(ops_data: list[dict[str, Any] | str]) -> list[dict[str, Any]]: + """Normalize patch operations payload for MCP clients. + + Args: + ops_data: Raw operations from MCP tool input. + + Returns: + Patch operations as object list. + + Raises: + ValueError: If a string op is not valid JSON object. + """ + normalized_ops: list[dict[str, Any]] = [] + for index, raw_op in enumerate(ops_data): + if isinstance(raw_op, dict): + normalized_ops.append(dict(raw_op)) + continue + normalized_ops.append(_parse_patch_op_json(raw_op, index)) + return normalized_ops + + +def _parse_patch_op_json(raw_op: str, index: int) -> dict[str, Any]: + """Parse a JSON string patch operation into object form. + + Args: + raw_op: Raw JSON string for one patch operation. + index: Source index in the ops list. + + Returns: + Parsed patch operation object. + + Raises: + ValueError: If the string is not valid JSON object. + """ + text = raw_op.strip() + if not text: + raise ValueError(_build_patch_op_error_message(index, "empty string")) + try: + parsed = json.loads(text) + except json.JSONDecodeError as exc: + raise ValueError(_build_patch_op_error_message(index, "invalid JSON")) from exc + if not isinstance(parsed, dict): + raise ValueError( + _build_patch_op_error_message(index, "JSON value must be an object") + ) + return cast(dict[str, Any], parsed) + + +def _build_patch_op_error_message(index: int, reason: str) -> str: + """Build a consistent validation message for invalid patch ops. + + Args: + index: Source index in the ops list. + reason: Validation failure reason. + + Returns: + Human-readable error message. + """ + example = '{"op":"set_value","sheet":"Sheet1","cell":"A1","value":"sample"}' + return ( + f"Invalid patch operation at ops[{index}]: {reason}. " + f"Use object form like {example}." + ) diff --git a/tests/mcp/test_server.py b/tests/mcp/test_server.py index 3223a12..af7ed5b 100644 --- a/tests/mcp/test_server.py +++ b/tests/mcp/test_server.py @@ -96,6 +96,34 @@ def test_coerce_filter() -> None: assert server._coerce_filter({"a": 1}) == {"a": 1} +def test_coerce_patch_ops_accepts_object_and_json_string() -> None: + result = server._coerce_patch_ops( + [ + {"op": "add_sheet", "sheet": "New"}, + '{"op":"set_value","sheet":"New","cell":"A1","value":"x"}', + ] + ) + assert result == [ + {"op": "add_sheet", "sheet": "New"}, + {"op": "set_value", "sheet": "New", "cell": "A1", "value": "x"}, + ] + + +def test_coerce_patch_ops_rejects_invalid_json_string() -> None: + with pytest.raises( + ValueError, match=r"Invalid patch operation at ops\[0\]: invalid JSON" + ): + server._coerce_patch_ops(["{invalid json}"]) + + +def test_coerce_patch_ops_rejects_non_object_json_value() -> None: + with pytest.raises( + ValueError, + match=r"Invalid patch operation at ops\[0\]: JSON value must be an object", + ): + server._coerce_patch_ops(['["not","object"]']) + + def test_register_tools_uses_default_on_conflict( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: @@ -183,6 +211,132 @@ async def fake_run_sync(func: Callable[[], object]) -> object: assert patch_call[0].preflight_formula_check is False +def test_register_tools_accepts_patch_ops_json_strings( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + app = DummyApp() + policy = PathPolicy(root=tmp_path) + calls: dict[str, tuple[object, ...]] = {} + + def fake_run_extract_tool( + payload: ExtractToolInput, + *, + policy: PathPolicy, + on_conflict: OnConflictPolicy, + ) -> ExtractToolOutput: + return ExtractToolOutput(out_path="out.json") + + def fake_run_read_json_chunk_tool( + payload: ReadJsonChunkToolInput, + *, + policy: PathPolicy, + ) -> ReadJsonChunkToolOutput: + return ReadJsonChunkToolOutput(chunk="{}") + + def fake_run_validate_input_tool( + payload: ValidateInputToolInput, + *, + policy: PathPolicy, + ) -> ValidateInputToolOutput: + return ValidateInputToolOutput(is_readable=True) + + def fake_run_patch_tool( + payload: PatchToolInput, + *, + policy: PathPolicy, + on_conflict: OnConflictPolicy, + ) -> PatchToolOutput: + calls["patch"] = (payload, policy, on_conflict) + return PatchToolOutput(out_path="out.xlsx", patch_diff=[]) + + async def fake_run_sync(func: Callable[[], object]) -> object: + return func() + + monkeypatch.setattr(server, "run_extract_tool", fake_run_extract_tool) + monkeypatch.setattr( + server, "run_read_json_chunk_tool", fake_run_read_json_chunk_tool + ) + monkeypatch.setattr(server, "run_validate_input_tool", fake_run_validate_input_tool) + monkeypatch.setattr(server, "run_patch_tool", fake_run_patch_tool) + monkeypatch.setattr(anyio.to_thread, "run_sync", fake_run_sync) + + server._register_tools(app, policy, default_on_conflict="overwrite") + patch_tool = cast(Callable[..., Awaitable[object]], app.tools["exstruct_patch"]) + anyio.run( + _call_async, + patch_tool, + { + "xlsx_path": "in.xlsx", + "ops": ['{"op":"add_sheet","sheet":"New"}'], + }, + ) + patch_call = cast( + tuple[PatchToolInput, PathPolicy, OnConflictPolicy], calls["patch"] + ) + assert patch_call[0].ops[0].op == "add_sheet" + assert patch_call[0].ops[0].sheet == "New" + + +def test_register_tools_rejects_invalid_patch_ops_json_strings( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + app = DummyApp() + policy = PathPolicy(root=tmp_path) + + def fake_run_extract_tool( + payload: ExtractToolInput, + *, + policy: PathPolicy, + on_conflict: OnConflictPolicy, + ) -> ExtractToolOutput: + return ExtractToolOutput(out_path="out.json") + + def fake_run_read_json_chunk_tool( + payload: ReadJsonChunkToolInput, + *, + policy: PathPolicy, + ) -> ReadJsonChunkToolOutput: + return ReadJsonChunkToolOutput(chunk="{}") + + def fake_run_validate_input_tool( + payload: ValidateInputToolInput, + *, + policy: PathPolicy, + ) -> ValidateInputToolOutput: + return ValidateInputToolOutput(is_readable=True) + + def fake_run_patch_tool( + payload: PatchToolInput, + *, + policy: PathPolicy, + on_conflict: OnConflictPolicy, + ) -> PatchToolOutput: + return PatchToolOutput(out_path="out.xlsx", patch_diff=[]) + + async def fake_run_sync(func: Callable[[], object]) -> object: + return func() + + monkeypatch.setattr(server, "run_extract_tool", fake_run_extract_tool) + monkeypatch.setattr( + server, "run_read_json_chunk_tool", fake_run_read_json_chunk_tool + ) + monkeypatch.setattr(server, "run_validate_input_tool", fake_run_validate_input_tool) + monkeypatch.setattr(server, "run_patch_tool", fake_run_patch_tool) + monkeypatch.setattr(anyio.to_thread, "run_sync", fake_run_sync) + + server._register_tools(app, policy, default_on_conflict="overwrite") + patch_tool = cast(Callable[..., Awaitable[object]], app.tools["exstruct_patch"]) + + with pytest.raises( + ValueError, match=r"Invalid patch operation at ops\[0\]: invalid JSON" + ): + anyio.run( + _call_async, + patch_tool, + {"xlsx_path": "in.xlsx", "ops": ['{"op":"set_value"']}, + ) + + def test_register_tools_passes_patch_default_on_conflict( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: From 397f1f1f112df32751cfb3453c7cf84847f17b1c Mon Sep 17 00:00:00 2001 From: harumiWeb Date: Mon, 16 Feb 2026 21:48:51 +0900 Subject: [PATCH 15/25] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 102 +++++++----------------------------------------------- 1 file changed, 13 insertions(+), 89 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6a7fb01..df297d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,14 +47,10 @@ AI は必ず次のルールに従ってコードを生成してください。 ### 2.4 import の順序 -``` - 1. 標準ライブラリ 2. サードパーティ 3. exstruct 内のモジュール -``` - ### 2.5 Docstring(Google スタイル) 関数・クラスには必ず Docstring を付ける。 @@ -111,51 +107,7 @@ AI は必要であれば **禁止事項を回避するためにリファクタ --- -# 5. AI に依頼するときの標準プロンプト(Developer Prompt) - -AI に実装依頼する際は、以下のフォーマットが推奨されます。 - -``` - -あなたは ExStruct のコア開発エンジニアです。 -mypy strict + Ruff check を完全に通過するコードのみ生成してください。 - -遵守事項: - -- 型ヒント完全 -- 境界は BaseModel、内部は dataclass を返す -- 1 関数 = 1 責務 -- import 順序厳守 -- Google スタイルの docstring -- 外部ライブラリ(xlwings, pandas)は Any 扱い -- 複雑度を抑えること -- 必要に応じて補助関数を追加して良い - -出力は Python コードのみ。 - -``` - ---- - -# 6. AI にエラー修正を依頼する際のルール - -mypy / Ruff のエラーが出た場合、AI への指示は次のテンプレートを使用します: - -``` - -以下は mypy / Ruff のエラーメッセージです。 -すべてのエラーが解消されるようにコードを修正してください。 - -<エラー全文> - -``` - -AI は差分修正タスクが得意なため、 -この方式が最も効率的かつ正確です。 - ---- - -# 7. AI の責務外(人間が担当する領域) +# 5. AI の責務外(人間が担当する領域) AI は以下の領域は担当しません。人間が判断します。 @@ -168,45 +120,17 @@ AI は以下の領域は担当しません。人間が判断します。 --- -# 8. AI を開発パイプラインと組み合わせる例 - -推奨フロー: - -1. 人間:仕様を記述 -2. AI:コード生成(テンプレート+ガイドラインに従う) -3. 自動:`mypy strict` チェック -4. 自動:`ruff check --fix` -5. AI:エラー出たら修正 -6. 人間:レビューしてマージ - -このサイクルにより **高速 & 高品質の開発が可能** になります。 - ---- - -# 9. 最終目標 - -AI エージェントが ExStruct のコードを書く場合でも: - -- バグのない型安全なコード -- 一貫したコーディングスタイル -- 仕様の誤解を最小化した実装 -- 人間が安心してレビューできるコード - -を常に提供できるようにする。 - -この AGENTS ガイドラインは -**Codex / ChatGPT を“チームメンバー”として扱うための基盤** です。 - ---- - -# 10. 各種仕様の確認 - -AI エージェントは必要に応じて以下のドキュメントを参照して ExStruct の開発をする +# 6. 遵守すべき作業手順 -- 処理アーキテクチャ: `docs/architecture/pipeline.md` -- プロジェクトアーキテクチャ: `docs/contributors/architecture.md` -- コーディングガイドライン: `docs/agents/CODING_GUIDELINES.md` -- データモデル: `docs/agents/DATA_MODEL.md` -- タスク: `docs/agents/TASKS.md` +AI はコード生成前に以下の手順を必ず踏んでください。 -**以上。AI はこのガイドラインに従って ExStruct の開発に参加してください。** +1. **要件の理解**:仕様書や設計資料を読み込み、要件を完全に理解する +2. **設計の検討**:必要に応じて関数分割やモデル設計を検討する。 +3. **仕様の定義**: 要件に基づいて、関数の引数・戻り値の型を定義する。 +4. **タスクの割り当て**: 各タスクを明確に定義し、実装順序を決定する。 +5. **コード実装**: 上記の基準を守りながらコードを実装する。 +6. **コードレビュー**: 自動生成されたコードを自己レビューし、品質基準を満たしているか確認する。 +7. **テストコードの生成**: 必要に応じて、テストコードも生成する。 +8. **テストの実行**: 生成されたテストコードを実行し、期待通りの動作を確認する。 +9. **静的解析**: `uv run task precommit-run` を実行し、mypy / Ruff エラーがないことを確認する。 +10. **ドキュメントの更新**: 変更があれば、関連するドキュメントも更新する。 \ No newline at end of file From 63999a4364a100020753b901b2d209baa235419b Mon Sep 17 00:00:00 2001 From: harumiWeb Date: Mon, 16 Feb 2026 22:06:02 +0900 Subject: [PATCH 16/25] feat: improve MCP mode and chunk specification guide, enhance error messages, and support alpha column keys --- docs/agents/FEATURE_SPEC.md | 67 ++++++++++++++++++++++++++++++++ docs/agents/TASKS.md | 13 +++++++ docs/mcp.md | 48 ++++++++++++++++++++++- src/exstruct/mcp/chunk_reader.py | 47 +++++++++++++++++++--- src/exstruct/mcp/server.py | 18 ++++++--- tests/mcp/test_chunk_reader.py | 56 ++++++++++++++++++++++++++ 6 files changed, 237 insertions(+), 12 deletions(-) diff --git a/docs/agents/FEATURE_SPEC.md b/docs/agents/FEATURE_SPEC.md index 2f009c1..6ba8162 100644 --- a/docs/agents/FEATURE_SPEC.md +++ b/docs/agents/FEATURE_SPEC.md @@ -484,3 +484,70 @@ JSON文字列配列として送信する場合がある。 - JSON文字列配列入力でも patch が実行できる - 不正JSON文字列で `ops[index]` を含む明確なエラーが返る - object 以外のJSON型入力で「object必須」エラーが返る + +--- + +## 15. MCP モード/チャンク指定ガイド改善 + +### 15.1 背景 + +MCP運用時、次の迷いが発生しやすい。 + +- `exstruct_extract.mode` に無効値を入れて失敗する +- `exstruct_read_json_chunk` で `sheet` 未指定のまま再試行して失敗する +- `max_bytes` の調整方針が不明でリトライ回数が増える +- `alpha_col=true` 出力時に `filter.cols` が使いにくい + +### 15.2 目的 + +- モード指定とチャンク指定の「最短成功パターン」を文書化する +- エラー時に次の行動が分かるメッセージへ改善する +- `alpha_col=true` でも列フィルタを直感的に使えるようにする + +### 15.3 スコープ + +- `docs/mcp.md` の実運用ガイド追加 +- `src/exstruct/mcp/server.py` のツールDocstring詳細化 +- `src/exstruct/mcp/chunk_reader.py` のエラーメッセージ改善 +- `src/exstruct/mcp/chunk_reader.py` の `filter.cols` を英字列キー対応 +- 関連テスト追加 + +### 15.4 仕様 + +#### 15.4.1 ドキュメント強化(`docs/mcp.md`) + +- `validate_input -> extract(standard) -> read_json_chunk` の推奨3ステップを追加 +- `mode` の許容値(`light`/`standard`/`verbose`)と用途を表形式で明記 +- `read_json_chunk` の主要引数(`sheet`, `max_bytes`, `filter`, `cursor`)を具体化 +- エラー/警告ごとの再試行手順を表形式で明記 + +#### 15.4.2 ツール説明の詳細化(`server.py`) + +- `exstruct_extract` Docstringに `mode` の意味と許容値を明記 +- `exstruct_read_json_chunk` Docstringに `filter.rows/cols` の + 1-based inclusive 仕様と `max_bytes` 調整指針を明記 + +#### 15.4.3 エラーメッセージ改善(`chunk_reader.py`) + +- 大きすぎる生JSONに対するエラー文言を、`sheet`/`filter` 指定例付きへ変更 +- 複数シート時の `sheet` 必須エラーに、利用可能シート名と再実行ヒントを追加 + +#### 15.4.4 `filter.cols` の英字列キー対応(`chunk_reader.py`) + +- 列キーが数値文字列(`"0"`)に加え英字列名(`"A"`, `"AA"`)でも判定可能にする +- `filter.cols` は従来どおり 1-based inclusive の列番号指定を維持する +- 英字列名は大文字小文字を区別しない + +### 15.5 後方互換性 + +- 既存の `filter.cols` 数値キー挙動は維持する +- `read_json_chunk` の入出力スキーマは変更しない +- 変更はガイド追加・メッセージ改善・列キー判定拡張に限定する + +### 15.6 受け入れ基準 + +- `docs/mcp.md` だけで、初回利用者が mode/chunk 再試行を完了できる +- `Output is too large` エラーに次アクションが含まれる +- `Sheet is required when multiple sheets exist` エラーにシート候補が含まれる +- `filter.cols` が数値キー・英字キー双方で機能する +- 既存テストと追加テストが通過する diff --git a/docs/agents/TASKS.md b/docs/agents/TASKS.md index 030de15..bc5d39a 100644 --- a/docs/agents/TASKS.md +++ b/docs/agents/TASKS.md @@ -81,3 +81,16 @@ - [x] `tests/mcp/test_server.py`: 不正JSON文字列 `ops` が明確な `ValueError` になることを検証する - [x] `ruff check src/exstruct/mcp/server.py tests/mcp/test_server.py` を実行し、静的解析を確認する - [x] `pytest tests/mcp/test_server.py` を実行し、回帰がないことを確認する + +## MCP モード/チャンク指定ガイド改善 + +- [x] `docs/agents/FEATURE_SPEC.md`: mode/chunk ガイド改善仕様(目的/スコープ/受け入れ基準)を定義する +- [x] `docs/mcp.md`: 推奨3ステップ(validate/extract/chunk)と mode 比較表を追加する +- [x] `docs/mcp.md`: chunk パラメータ説明、エラー別リトライ表、cursor利用手順を追加する +- [x] `src/exstruct/mcp/server.py`: `exstruct_extract` Docstring に mode の許容値と意図を追記する +- [x] `src/exstruct/mcp/server.py`: `exstruct_read_json_chunk` Docstring に `filter.rows/cols` と `max_bytes` 指針を追記する +- [x] `src/exstruct/mcp/chunk_reader.py`: 大容量時エラー文言に再実行ヒントを追加する +- [x] `src/exstruct/mcp/chunk_reader.py`: 複数シート時エラー文言に利用可能シート名を追加する +- [x] `src/exstruct/mcp/chunk_reader.py`: `filter.cols` を英字列キー(`A`, `AA`)対応に拡張する +- [x] `tests/mcp/test_chunk_reader.py`: 英字列キー対応と新エラーメッセージのテストを追加する +- [x] `uv run task precommit-run` を実行し、静的解析・型検査・テスト通過を確認する diff --git a/docs/mcp.md b/docs/mcp.md index a52c16f..06ec7c8 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -38,16 +38,62 @@ exstruct-mcp --root C:\\data --log-file C:\\logs\\exstruct-mcp.log --on-conflict - `exstruct_read_json_chunk` - `exstruct_validate_input` -### `exstruct_extract` defaults +### `exstruct_extract` defaults and mode guide - `options.alpha_col` defaults to `true` in MCP (column keys become `A`, `B`, ...). - Set `options.alpha_col=false` if you need legacy 0-based numeric string keys. +- `mode` is an extraction detail level (not sheet scope): + +| Mode | When to use | Main output characteristics | +|---|---|---| +| `light` | Fast, structure-first extraction | cells + table candidates + print areas | +| `standard` | Default for most agent flows | balanced detail and size | +| `verbose` | Need the richest metadata | adds links/maps and richer metadata | + +## Quick start for agents (recommended) + +1. Validate file readability with `exstruct_validate_input` +2. Run `exstruct_extract` with `mode="standard"` +3. Read the result with `exstruct_read_json_chunk` using `sheet` and `max_bytes` + +Example sequence: + +```json +{ "tool": "exstruct_validate_input", "xlsx_path": "C:\\data\\book.xlsx" } +{ "tool": "exstruct_extract", "xlsx_path": "C:\\data\\book.xlsx", "mode": "standard", "format": "json" } +{ "tool": "exstruct_read_json_chunk", "out_path": "C:\\data\\book.json", "sheet": "Sheet1", "max_bytes": 50000 } +``` ## Basic flow 1. Call `exstruct_extract` to generate the output JSON file 2. Use `exstruct_read_json_chunk` to read only the parts you need +## Chunking guide + +### Key parameters + +- `sheet`: target sheet name. Strongly recommended when workbook has multiple sheets. +- `max_bytes`: chunk size budget in bytes. Start at `50_000`; increase (for example `120_000`) if chunks are too small. +- `filter.rows`: `[start, end]` (1-based, inclusive). +- `filter.cols`: `[start, end]` (1-based, inclusive). Works for both numeric keys (`"0"`, `"1"`) and alpha keys (`"A"`, `"B"`). +- `cursor`: pagination cursor (`next_cursor` from the previous response). + +### Retry guide by error/warning + +| Message | Meaning | Next action | +|---|---|---| +| `Output is too large...` | Whole JSON cannot fit in one response | Retry with `sheet`, or narrow with `filter.rows`/`filter.cols` | +| `Sheet is required when multiple sheets exist...` | Workbook has multiple sheets and target is ambiguous | Pick one value from `workbook_meta.sheet_names` and set `sheet` | +| `Base payload exceeds max_bytes...` | Even metadata-only payload is larger than `max_bytes` | Increase `max_bytes` | +| `max_bytes too small...` | Row payload is too large for the current size | Increase `max_bytes`, or narrow row/col filters | + +### Cursor example + +1. Call without `cursor` +2. If response has `next_cursor`, call again with that cursor +3. Repeat until `next_cursor` is `null` + ## Edit flow (patch) 1. Inspect workbook structure with `exstruct_extract` (and `exstruct_read_json_chunk` if needed) diff --git a/src/exstruct/mcp/chunk_reader.py b/src/exstruct/mcp/chunk_reader.py index 408c9f9..4e81378 100644 --- a/src/exstruct/mcp/chunk_reader.py +++ b/src/exstruct/mcp/chunk_reader.py @@ -145,7 +145,10 @@ def _chunk_raw_text(text: str, max_bytes: int) -> ReadJsonChunkResult: payload_bytes = text.encode("utf-8") if len(payload_bytes) <= max_bytes: return ReadJsonChunkResult(chunk=text, next_cursor=None, warnings=[]) - raise ValueError("Output is too large. Specify sheet or filter to chunk.") + raise ValueError( + "Output is too large. Retry with `sheet` (e.g., sheet='Sheet1') " + "or `filter` (e.g., filter.rows=[1,200]) to chunk the result." + ) def _select_sheet( @@ -173,7 +176,12 @@ def _select_sheet( if len(sheets) == 1: only_name = next(iter(sheets.keys())) return only_name, sheets[only_name] - raise ValueError("Sheet is required when multiple sheets exist.") + names = ", ".join(str(name) for name in sheets.keys()) + raise ValueError( + "Sheet is required when multiple sheets exist. " + f"Available sheets: {names}. " + "Retry with `sheet=''`." + ) def _extract_rows(sheet_data: dict[str, Any]) -> list[dict[str, Any]]: @@ -281,13 +289,42 @@ def _col_in_range(key: str, start: int, end: int) -> bool: Returns: True if the column index is within range. """ - try: - index = int(key) - except (TypeError, ValueError): + index = _parse_col_index(key) + if index is None: return False return start <= index <= end +def _parse_col_index(key: str) -> int | None: + """Parse a column key into a 0-based index. + + Supports both legacy numeric keys ("0", "1", ...) and + alpha keys ("A", "B", ..., "AA", ...) emitted by alpha_col mode. + + Args: + key: Column key string. + + Returns: + 0-based column index, or None when parsing fails. + """ + try: + return int(key) + except (TypeError, ValueError): + pass + + normalized = key.strip().upper() + if not normalized.isalpha(): + return None + + acc = 0 + for ch in normalized: + code = ord(ch) + if code < ord("A") or code > ord("Z"): + return None + acc = acc * 26 + (code - ord("A") + 1) + return acc - 1 + + def _row_index(row: dict[str, Any]) -> int: """Extract row index from a row dictionary. diff --git a/src/exstruct/mcp/server.py b/src/exstruct/mcp/server.py index ddc84d9..dbc8a16 100644 --- a/src/exstruct/mcp/server.py +++ b/src/exstruct/mcp/server.py @@ -212,13 +212,16 @@ async def _extract_tool( # pylint: disable=redefined-builtin Args: xlsx_path: Path to the Excel workbook. - mode: Extraction mode. + mode: Extraction detail level. Allowed values are: + "light" (cells + table candidates + print areas), + "standard" (recommended default), + "verbose" (adds richer metadata such as links/maps). format: Output format. out_dir: Optional output directory. out_name: Optional output filename. options: Additional options. Supports: pretty (bool), indent (int), sheets_dir (str), print_areas_dir (str), auto_page_breaks_dir (str), - alpha_col (bool – convert column keys to Excel-style ABC names). + alpha_col (bool - convert column keys to Excel-style ABC names). Returns: Extraction result payload. @@ -256,10 +259,13 @@ async def _read_json_chunk_tool( # pylint: disable=redefined-builtin Args: out_path: Path to the JSON output file. - sheet: Optional sheet name. - max_bytes: Maximum chunk size in bytes. - filter: Optional filter payload. - cursor: Optional cursor for pagination. + sheet: Optional sheet name. Required when multiple sheets exist and + no cursor/filter can disambiguate the target. + max_bytes: Maximum chunk size in bytes. Start around 50_000 and + increase (for example 120_000) when chunks are too small. + filter: Optional filter payload with + rows=[start,end], cols=[start,end] (1-based inclusive). + cursor: Optional cursor for pagination (non-negative integer string). Returns: JSON chunk result payload. diff --git a/tests/mcp/test_chunk_reader.py b/tests/mcp/test_chunk_reader.py index d1113cb..4741cd0 100644 --- a/tests/mcp/test_chunk_reader.py +++ b/tests/mcp/test_chunk_reader.py @@ -37,6 +37,15 @@ def test_read_json_chunk_raw_too_large(tmp_path: Path) -> None: read_json_chunk(request) +def test_read_json_chunk_raw_too_large_has_retry_hint(tmp_path: Path) -> None: + data = {"book_name": "book", "sheets": {"Sheet1": {"rows": []}}} + out = tmp_path / "out.json" + _write_json(out, data) + request = ReadJsonChunkRequest(out_path=out, max_bytes=10) + with pytest.raises(ValueError, match=r"Retry with `sheet` .* or `filter`"): + read_json_chunk(request) + + def test_read_json_chunk_with_filters(tmp_path: Path) -> None: data = { "book_name": "book", @@ -78,6 +87,19 @@ def test_read_json_chunk_requires_sheet(tmp_path: Path) -> None: read_json_chunk(request) +def test_read_json_chunk_requires_sheet_lists_available_names(tmp_path: Path) -> None: + data = {"book_name": "book", "sheets": {"A": {"rows": []}, "B": {"rows": []}}} + out = tmp_path / "out.json" + _write_json(out, data) + request = ReadJsonChunkRequest( + out_path=out, + max_bytes=10_000, + filter=ReadJsonChunkFilter(rows=(1, 1)), + ) + with pytest.raises(ValueError, match=r"Available sheets: A, B"): + read_json_chunk(request) + + def test_read_json_chunk_invalid_cursor(tmp_path: Path) -> None: data = {"book_name": "book", "sheets": {"Sheet1": {"rows": []}}} out = tmp_path / "out.json" @@ -188,6 +210,40 @@ def test_read_json_chunk_warns_on_col_filter_inversion(tmp_path: Path) -> None: assert any("Column filter ignored" in warning for warning in result.warnings) +def test_read_json_chunk_with_alpha_col_keys(tmp_path: Path) -> None: + data = { + "book_name": "book", + "sheets": { + "Sheet1": { + "rows": [ + {"r": 1, "c": {"A": "A1", "B": "B1", "AA": "AA1", "ab": "AB1"}}, + ] + } + }, + } + out = tmp_path / "out.json" + _write_json(out, data) + request = ReadJsonChunkRequest( + out_path=out, + sheet="Sheet1", + max_bytes=10_000, + filter=ReadJsonChunkFilter(cols=(1, 2)), + ) + result = read_json_chunk(request) + payload = json.loads(result.chunk) + assert payload["sheet"]["rows"][0]["c"] == {"A": "A1", "B": "B1"} + + request_aa = ReadJsonChunkRequest( + out_path=out, + sheet="Sheet1", + max_bytes=10_000, + filter=ReadJsonChunkFilter(cols=(27, 28)), + ) + result_aa = read_json_chunk(request_aa) + payload_aa = json.loads(result_aa.chunk) + assert payload_aa["sheet"]["rows"][0]["c"] == {"AA": "AA1", "ab": "AB1"} + + def test_read_json_chunk_warns_on_base_payload_exceeds_max_bytes( tmp_path: Path, ) -> None: From 7954e63f8e77692a8e80d02d1ecc7ddb1b20b955 Mon Sep 17 00:00:00 2001 From: harumiWeb Date: Mon, 16 Feb 2026 22:07:47 +0900 Subject: [PATCH 17/25] remove --- docs/agents/API_DESIGN.md | 68 -------------------- docs/agents/FEATURE_CONCEPT.md | 110 --------------------------------- docs/agents/RAG_INTEGRATION.md | 34 ---------- 3 files changed, 212 deletions(-) delete mode 100644 docs/agents/API_DESIGN.md delete mode 100644 docs/agents/FEATURE_CONCEPT.md delete mode 100644 docs/agents/RAG_INTEGRATION.md diff --git a/docs/agents/API_DESIGN.md b/docs/agents/API_DESIGN.md deleted file mode 100644 index fe0b43a..0000000 --- a/docs/agents/API_DESIGN.md +++ /dev/null @@ -1,68 +0,0 @@ -# API Design — ExStruct - -ExStruct のトップレベル Python API 設計。 - -## 基本 API - -```python -import exstruct as xs - -data = xs.extract("file.xlsx") -``` - -### 戻り値 - -`WorkbookData`(Pydantic model) - ---- - -## CLI - -```bash -exstruct file.xlsx --format json > output.json -``` - -オプション: - -- `--image` → PNG 出力 -- `--pdf` → PDF 出力 -- `--dpi` → PNG 解像度 -- `--sheets-dir` → シートごとの分割出力 -- `--multiple` → 複数ファイル処理 - ---- - -## JSON/YAML/TOON 出力 - -```python -data = xs.extract("file.xlsx") - -xs.export("file.xlsx") -data.to_json(pretty=True) -data.to_yaml() -data.to_toon() -data.save("file.json") -data["Sheet1"] # WorkbookData.__getitem__ -for name, sheet in data: # WorkbookData.__iter__ - print(name, len(sheet.rows)) - -# ExStructEngine (per-instance options) -from exstruct import ExStructEngine, FilterOptions, FormatOptions, OutputOptions, StructOptions -engine = ExStructEngine( - options=StructOptions(mode="standard"), - output=OutputOptions( - format=FormatOptions(pretty=True), - filters=FilterOptions(include_shapes=False), - ), -) -wb = engine.extract("file.xlsx") -engine.export(wb, "filtered.json") -``` - ---- - -## 今後拡張予定 - -- `xs.detect_tables()` -- `xs.semantic_shapes()` -- `xs.smartart()` diff --git a/docs/agents/FEATURE_CONCEPT.md b/docs/agents/FEATURE_CONCEPT.md deleted file mode 100644 index 2842bfc..0000000 --- a/docs/agents/FEATURE_CONCEPT.md +++ /dev/null @@ -1,110 +0,0 @@ -ExStructを用いたAIエージェント向け編集ツール設計提案 -背景と目的 -ExcelドキュメントをExStructでJSON構造に変換することで、セル値・数式・図形・チャートなどを含むシート構造をプログラム的に扱えるようになります -。MCPサーバー上では、このExStructの出力JSONをAIエージェントが読み込み・解析し、必要な編集操作を自律的に決定します。エージェント側で推論(判断)を行い、MCPサーバー側では安全にその指示を実行・結果提供する役割分担となります -。本提案の目的は、AIエージェントがユーザーの確認を最小限にしつつExcelの内容編集を行えるよう、ExStruct JSONベースの編集操作ツールを設計することです。これにより、フォームへの自動入力、数式エラーの修正、新規シート作成といった処理を安全かつ再現性高く実現できる基盤を構築します。 -MCP用ツール設計(OpenAPIスキーマベース) -AIエージェントが利用するMCPツールとして、新たにExStruct編集ツールを提案します。既存の抽出系ツール(exstruct_extract, exstruct_read_json_chunk, exstruct_validate_input -)に加えて、編集適用用のツールを設計します。ツール名・概要・パラメータは以下の通りです。 -exstruct_patch(Excel構造編集ツール) -説明: ExStructの出力JSON(構造化Excelデータ)に対する変更パッチを適用し、Excelファイルに編集を書き戻すためのツールです。エージェントが提案した修正内容(セル値更新、数式変更、新規シート追加など)をまとめて受け取り、安全にExcelデータを更新します。 -入力: -xlsx_path (string) – 編集対象とするExcelファイルのパス(読み込み元) -ops (array of object) – 適用する編集操作のリスト。各要素は以下のフィールドを持ちます: -op (string) – 操作種別(例: "set_value", "set_formula", "add_sheet") -sheet (string) – 対象シート名(新規シート作成時は作成する名前) -cell (string) – 対象セル座標(例えば "B3"。set_value/set_formulaで使用) -value (string|number|null) – 設定するセルの値(数値・文字列など。set_value用) -formula (string) – 設定するセルの数式(=から始まるExcel数式文字列。set_formula用) -(※図形テキスト等の編集にも拡張可能:例えば op: "set_shape_text", shape_id: 図形ID, text: 新テキスト など) -out_dir (string, optional) – 編集後のExcelを書き出すディレクトリ(省略時は元ファイルと同じディレクトリ) -out_name (string, optional) – 編集後Excelファイル名(省略時は元のファイル名に自動サフィックス付与) -on_conflict (string, optional) – 出力ファイル名衝突時の扱い("overwrite"/"skip"/"rename"から選択。未指定時はサーバー既定値を使用 -) -出力: -out_path (string) – 更新後のExcelファイルパス(保存先)。 -patch_diff (array of object) – 実際に適用された差分一覧(各変更の結果確認用情報。変更前後の値やシート名などを含む) -warnings (array of string, optional) – 適用時の警告メッセージ(必要に応じて) -設計の意図・特徴: -エージェントは提案する複数の編集操作をバッチ(opsリスト)で指定でき、ツール側で原子的に適用します。例えば、一度の呼び出しで複数セルの入力補完やシート追加と初期値入力を同時に行えます。xlsx_pathで指定した元ファイルは読み込み専用とし、out_pathへ結果を書き出すことで元データを保持しつつ変更版を別ファイルとして生成します(オプション指定がなければ自動リネーム保存)。このツール自体はExcelファイルを直接操作し、ExStructのJSON構造に対応する変更をExcelに反映します。Windows環境ではExcel COMまたはopenpyxlを用いてセル値・数式を書き込み、シートの追加等を行います(非Windows環境ではopenpyxlで対応)。処理完了後、どの項目がどのように変わったかをpatch_diffとして返し、エージェントはそれを用いて変更結果を検証したりユーザーに報告したりできます。 -ユースケース別の処理フロー -上記ツールを用いて、想定ユースケース毎にAIエージェントがどのように編集を行うか、そのフローを示します。各ケースで、エージェントはまずExStructで既存Excelを解析し、その出力JSONを基に編集箇所と内容を決定、exstruct_patchツールにより変更を適用します。入力にはユーザーの指示や補足データ、出力にはExStruct JSON形式の変更パッチ(差分)が含まれます。 -ユースケース1: 入力フォームの自動入力 -想定シナリオ: ユーザーがExcel内のフォームシートに対し、「氏名」「住所」などの項目に与えられたデータを自動入力してほしいと依頼したケースです。フォームのレイアウトとして、例えばA列に項目名、B列の対応セルが空欄になっていると想定します。 処理フロー: -Excel内容の取得 – エージェントはまずexstruct_extractツールでExcelをJSON変換し -、フォームシートの構造と既存値を把握します。例えば、「氏名」や「住所」という文字列がどのセルにあるかをJSONから検索します(ExStructではセル内容はrows配列内の各行オブジェクトに格納されています -)。 -入力箇所の特定 – 抽出JSONを解析し、フォーム項目名に対応する入力セル位置を特定します。具体的には、例えば「氏名」という文字列がセルA2にあり、その右隣B2が空欄であればB2が入力対象と判断されます。同様に「住所」がA3にありB3が空欄ならB3が対象になります。 -パッチの生成 – ユーザーから提供されたデータ(氏名=山田太郎、住所=東京都… 等)を、対応するセルに埋め込むパッチを構築します。例えば以下のような変更リストを用意します(擬似JSON形式): -"ops": [ -{"op": "set_value", "sheet": "フォーム", "cell": "B2", "value": "山田太郎"}, -{"op": "set_value", "sheet": "フォーム", "cell": "B3", "value": "東京都新宿区..."} -] -これによりフォームシート「フォーム」のB2セルに「山田太郎」、B3セルに住所テキストを入力する指示となります。 -パッチの適用 – エージェントは構築した変更パッチをexstruct_patchツールに渡し実行します。ツール内部では、指定Excelファイルの該当セルを書き換え、新しいExcelを保存します。出力には、例えばB2が空白から「山田太郎」に変わったこと、B3が空白から住所文字列に変わったことを示す差分情報が含まれます。 -結果の検証・利用 – エージェントは返されたpatch_diffを確認し、想定通り入力が行われたか検証します。問題なければユーザーに対し「フォームに指定のデータを入力しました」と回答し、必要に応じて更新後のExcelファイル(out_pathで示されるファイル)を提供します。変更内容は差分として記録されており、後から監査可能です。 -※内部的には、ExStruct JSONのrowsデータ(セル値マップ)でB2, B3に相当する箇所が更新されるイメージです -。ユーザー確認なしでも誤入力を防ぐため、エージェントは項目名と入力内容の対応を二重チェックし、フォーマット不整合(例えば数値フィールドに文字列を入れる等)が無いか検証します。 -ユースケース2: 数式エラーの修正 -想定シナリオ: Excelシート内のあるセルが#DIV/0!や#REF!等のエラーを示しており、ユーザーが「この数式エラーを修正してほしい」と依頼したケースです。例えば、シート「計算」で総計を求める数式が誤参照によりエラーになっている状況を考えます。 処理フロー: -エラーの検出 – エージェントはまずexstruct_extractでブック全体をJSON化し、formulas_mapなどを参照してエラーのある数式を探します。ExStructのformulas_mapには「数式文字列 → セル座標」の対応が含まれており -、これを使って該当セルの数式テキストを取得できます(例えばエラーが発生しているセルC10の数式が=SUM(A1:A9)だった等を把握)。また、rows内の該当セル値がエラー文字列("#DIV/0!" 等)になっていることも確認します。 -原因分析 – 抽出した数式文字列および周辺データからエラー原因を推測します。例えば、#REF!であれば参照範囲のシート名・セル範囲の誤り、#DIV/0!であれば除数が0になり得ることが原因かもしれません。エージェントは他のセル値もrowsデータから参照し、問題の数式が何をしようとしているか理解します。必要に応じてユーザーから追加情報(正しい参照先など)を引き出すことも考えられます。 -修正内容の決定 – 原因に応じて新しい数式を提案します。例えば、#REF!なら参照すべき正しいシート名に修正、#DIV/0!なら除数が0の場合に0を返すようなIF関数の追加、などです。ここではシート「計算」のC10が=SUM(Shee1!A:A)となっておりシート名のタイポで#REF!になっている例を考えます(正しくはSheet1)。修正数式は =SUM(Sheet1!A:A) に直す方針と決定します。 -パッチの生成 – 決定した修正を反映するパッチを作成します。上記例ではシート「計算」のセルC10の数式を書き換える操作となります。パッチ例: -"ops": [ -{"op": "set_formula", "sheet": "計算", "cell": "C10", "formula": "=SUM(Sheet1!A:A)"} -] -これによりC10セルの数式文字列を新しいものに置き換えます(ExStructのformulas_map上も旧数式キーから新数式キーへの更新が行われるイメージです)。必要に応じ、数式結果の再計算はExcel側で開く際に行われるため、ここでは値の更新は行いません(値はユーザーがExcelを開いて再計算した際に得られる)。 -パッチの適用 – エージェントはexstruct_patchを使い上記パッチを適用します。Excelファイル上でセルC10の数式が書き換わり、新しいExcelファイルが保存されます。patch_diffには「C10の数式: =SUM(Shee1!A:A) から =SUM(Sheet1!A:A) に変更した」旨の記録が残ります。 -結果の検証 – エージェントは返された差分を確認し、確かに意図した変更が行われたことを確認します。可能であればexstruct_extractで再度JSONを取得し、エラー表示が消えたか(ExStruct上はセルC10の値が今度はエラー文字列以外になっているか)をチェックしても良いでしょう。修正が正しければ、ユーザーに対し「数式の参照ミスを修正しました。これでエラーは解消するはずです」と伝え、更新後のExcelを提供します。 -※ExStructではformulas_mapにより複数セルで同一数式が使われている場合まとめられていますが -、個別セルの修正では該当エントリのみを差し替えます(内部実装では旧数式キーの該当セル座標を削除し、新数式キーにセル座標を追加する処理となります)。エージェントはこのような低レベル実装詳細を意識せず高水準な操作指定(「セルC10の数式を書き換え」)を行い、ツール側で正確な差分反映を行います。 -ユースケース3: ゼロからのシート作成 -想定シナリオ: 空の新規シートを追加し、そこに必要な表や値を自動生成してほしいケースです。ユーザーが「売上データを集計した新しいシートを作って」と依頼するといった状況を考えます。 処理フロー: -内容と構成のプランニング – エージェントはユーザーの要求を解析し、新シートに何を作るかを決めます。例えば「売上集計」シートを作成し、既存の「売上明細」シートから月別売上合計を計算してまとめる、など具体的なプランを立てます。この段階で必要ならexstruct_extractやread_json_chunkで元データ(売上明細シートの内容)を取得し、計算に使います。 -シート名の決定と競合確認 – 新規シート名を決めます(例: 「売上集計」)。既に同名のシートが存在しないか、ExStruct JSONのsheetsマップを確認して競合を避けます -(存在する場合は自動で別名を付けるかユーザーに確認します)。 -初期データ/書式の生成 – シートに配置するデータやヘッダ行、計算式などを決定します。例えば1行目にヘッダ「月」「売上合計」を入れ、2行目以降に1月~12月の売上合計値を計算して入れる、というようにレイアウトと内容を準備します。計算式もここで用意し(例えばB2に=SUM('売上明細'!B2:B31)など)、論理チェックを行います。 -パッチの生成 – 新シート追加とセル内容設定を含む変更パッチを構築します。例えば以下のようになります: -"ops": [ -{"op": "add_sheet", "sheet": "売上集計"}, -{"op": "set_value", "sheet": "売上集計", "cell": "A1", "value": "月"}, -{"op": "set_value", "sheet": "売上集計", "cell": "B1", "value": "売上合計"}, -{"op": "set_value", "sheet": "売上集計", "cell": "A2", "value": "1月"}, -{"op": "set_formula", "sheet": "売上集計", "cell": "B2", "formula": "=SUM('売上明細'!B2:B31)"} -// 以下 A3に"2月", B3にSUM(...), ... と続く -] -最初のadd_sheetによりブックに名前「売上集計」のシートを追加し、続くset_valueおよびset_formulaでそのシート上のセルを順次設定していきます(ヘッダ行と1月の集計式のみ例示)。パッチ内の操作順序は考慮され、add_sheetが他の操作より先に適用されることで、新シート上に値を書き込む処理が可能になります。 -パッチの適用 – exstruct_patchを呼び出し変更を適用します。これによって元Excelに「売上集計」シートが追加され、指定のセルに値・数式が書き込まれた新しいExcelファイルが生成されます。patch_diffには新規シート作成とセル値入力が一覧化されます。例えば「新規シート '売上集計' を追加」「'売上集計'!A1 に '月' を設定」「'売上集計'!B2 に数式 '=SUM('売上明細'!B2:B31)' を設定」などと記録されます。 -結果確認 – エージェントは差分ログや再抽出結果を検証し、シート追加とデータ配置が指示通りになっていることを確認します。必要ならば新シートの内容をexstruct_read_json_chunkで読み出し、意図した値・式になっているかチェックします。問題なければユーザーに「新しい集計シート '売上集計' を追加しました」と伝え、ファイルパスを共有します。 -この処理により、ゼロからシートを生成する操作も自動化できます。opsリストにまとめて記述できるため、一括適用による一貫性確保(シート追加と内容設定の一体化)や、過程の中間状態がユーザー確認なしでも整合するメリットがあります。 -編集対象フィールドと変更内容の対応 -各ユースケースで操作するExStruct内部フィールドを整理します。提案ツールはExStruct出力JSONを直接修正・拡張する形でExcel編集を行います。 -セル値 (Cells) – フォーム自動入力や値修正では、JSON構造内の各シートのrows配列にある該当セル値を更新します -。例えば上記ケース1ではシート「フォーム」の該当行オブジェクト内のcマップでキー「B」に対応する値を書き換える操作に相当します。新規値はJSON上もExcel上も文字列または数値として保存されます。 -数式 (Formulas) – 数式の変更では、Excel上はセルの数式文字列を書き換えます。ExStruct JSON上はformulas_mapに格納されたエントリを更新することに対応します -(内部的には旧数式のエントリから該当セルを削除し、新数式文字列のキーにセルを紐付ける操作となります)。またExcelファイル上は新しい数式が設定されます。注: ExStructの抽出JSONはセルの計算結果値も保持しますが、再計算が必要な場合この値は古いままの場合があります。エージェントは必要ならユーザーにExcel上で再計算(F9押下等)するよう注意喚起します。 -シート (Sheets) – 新規シートの追加では、JSONのworkbook["sheets"]オブジェクトに新しいキー(シート名)を追加し、その値として空のSheetData構造を作る操作に相当します -。内部ではExcelファイルに対してシートを追加し、ExStructエンジンで扱える構造(空のrows配列等を持つ)を初期化します。その後のセル入力はこの新規シート内のデータ構造に対する更新です。既存シート名の変更や削除も将来的には対応可能ですが、本提案範囲では扱いません。 -図形・その他オブジェクト (Shapes etc) – ユースケースには直接登場しませんでしたが、ExStruct JSONには図形やスマートアート、チャート等の情報も含まれます -。テキストボックス型の図形に入力された文字列を編集する、といった操作も考えられます。この場合、該当シートのshapes配列から対象オブジェクト(例えばidで特定)を見つけ、そのtextフィールドを更新するパッチを用意します。提案ツールではop: "set_shape_text"等の拡張的な操作もサポート可能であり、他の操作と同様に差分として記録・適用します。チャートやSmartArtに対する大規模な構造変更は複雑ですが、例えばチャートタイトルのテキスト差し替え程度であれば同様のアプローチで実現できます。 -安全性とトレーサビリティ -AIエージェントによる自律的なExcel編集においては、誤操作の防止と変更履歴の追跡が極めて重要です。本設計では以下の工夫により安全性とトレーサビリティを確保します。 -ルートディレクトリ固定とパス検証: MCPサーバー起動時に--rootオプションで許可ディレクトリを定め、エージェントはその配下のファイルしか操作できません -。ツール実行時も内部でパス正規化とdeny_globチェックを行い、不正なパスや想定外のファイルアクセスをブロックします -。これにより、エージェントが誤って機密ファイルを編集したり、悪意ある指示で外部に影響を及ぼすリスクを低減します。 -オープンAPIスキーマによる入力制約: 提案ツールのパラメータはOpenAPIスキーマに基づき厳格に定義されています。例えばops配列内の各フィールド型・必須/省略可能要素は事前に決められており、エージェントのリクエストは自動検証されます。スキーマ不一致の入力(存在しないシート名や無効なセル番地形式など)は即座にエラーとなり、実ファイルに手を加える前に検出されます。これにより、エージェントのバグやLLMの勘違いによる不正確な操作を未然に防ぎます。 -差分適用の原子性: 一連の編集opsはすべて成功した場合にのみコミットされます。途中で無効な参照(例: 存在しないセル)や競合が判明した場合、ツールはエラーを返しExcelは変更されません。これにより、部分的変更でデータ不整合が生じることを防ぎます。 -出力ファイルの競合回避: 既定ではexstruct_patchは元ファイルを上書きせず新規ファイルとして保存します。--on-conflict設定により同名ファイルがある場合はリネームやスキップも可能です -。安全策としてデフォルトはrename(連番等で新ファイル名生成)とし、うっかり既存ファイルを消してしまう事故を防ぎます。ユーザーが明示的に許可した場合のみ上書きモードを使う運用を推奨します。 -変更履歴のログ: exstruct_patchは適用内容をpatch_diffとして詳細に返します。各エントリには変更対象(シート名・セル座標など)、変更前の値(可能な場合)と変更後の値が含まれます。これをサーバー側でログ蓄積することで、後から「どのセルをどう変えたか」を追跡できます。ユーザーは履歴をレビューしたり、不備が見つかった場合に該当変更のみをUndo(取り消し)することも可能です。Undoは、記録された変更前値を用いて逆パッチを適用すれば実現できます。例えば、「B2を空白から山田太郎に変更」という記録があれば、その逆操作「B2を山田太郎から空白に」がUndoに相当します。エージェントは自律動作中でも必要に応じて過去ログを参照し、二重入力や再変更による矛盾を避けます。 -ユーザー確認ポイントの最小化と確保: 基本的にエージェントは上記仕組みにより自律的に編集を完了できますが、リスクの高い操作(大量のセル削除や大幅なレイアウト変更など)は事前にユーザー確認を要求することも検討できます。本提案ではフォーム入力や局所的な計算修正といったスコープ限定の変更を対象としており、通常は自動承認で進めます。一方、新規シート追加のようにユーザーの意図を汲み取る必要があるケースでは、エージェントが推測に基づく内容(例: 集計方法)を実装する前に「この方針で進めますがよろしいですか?」と質問することも可能です。ただしこれはプロンプト設計レベルで制御し、MCPツール自体はユーザー確認のフローを持ちません。必要な確認はエージェントが対話を通じて行い、確定したら一括適用する方針です。 -例外とエラー処理: ツール実行時に発生し得るエラー(例えばExcelファイル破損、シート名の競合、権限不足など)はツールからエージェントへ明示的に返されます(warningsやエラーメッセージとして)。エージェントはこれを解釈し、ユーザーへの報告やリトライ(別名で再作成する等)を行います。こうしたフェイルセーフ設計により、意図しないSilent failure(失敗に気付かない状態)を防ぎます。 -以上の対策により、AIエージェントによるExcel編集操作は、閉じた安全な環境内でトレース可能な形で実行されます。ログとパッチによる再現性確保により、後からでも変更を再適用したり原因分析したりが容易です。ユーザーは安心してエージェントにExcel編集を任せることができ、必要に応じて結果だけを確認するという使い方が可能になります。 -プロンプト例(LLMへの指示) -最後に、想定ユースケースに対してユーザーがAIエージェントに与える指示(プロンプト)の代表例を示します。エージェントはこれらの指示を受け取り、内部でExStruct MCPツールを駆使して前述の編集フローを実行します。 -フォーム自動入力の例: 「添付のExcelフォームに、社員情報を自動入力してください。氏名は山田太郎、住所は東京都新宿区西新宿2-8-1、電話番号は03-1234-5678です。」 -数式エラー修正の例: 「Excelの『計算』シートでセルC10に#REF!エラーが出ています。正しい参照範囲は同ブックの『Sheet1』シートです。このエラーを修正してください。」 -新規シート作成の例: 「現在のExcelブックに、新しく『売上集計』シートを追加してください。既存の『売上明細』シートから月ごとの売上合計を計算し、月別に一覧でまとめて配置してください。」 -これらのプロンプトに対し、エージェントは適切にExStructの抽出結果を分析し、設計したツールを用いて安全に編集を行います。例えばフォーム入力の依頼では、自動で対応セルを見つけ出し値を埋め、ユーザーに完成したフォームを提示します。数式修正では、誤った参照を正しいものに直し、新規シート作成では要求通りの集計シートを構築します。全ての操作はExStruct JSON中間表現を介して行われるため、AIエージェントはExcelのGUI操作をすることなく構造的かつ再現性のある編集を実現できます。ユーザーは最終的に編集後のExcelファイルと、必要なら変更概要の説明を受け取り、従来手作業では困難だった高度な処理も自動化された形で享受できます。 diff --git a/docs/agents/RAG_INTEGRATION.md b/docs/agents/RAG_INTEGRATION.md deleted file mode 100644 index a442a29..0000000 --- a/docs/agents/RAG_INTEGRATION.md +++ /dev/null @@ -1,34 +0,0 @@ -# RAG Integration — How to Use ExStruct in AI Pipelines - -ExStruct の想定用途は RAG(Retrieval Augmented Generation)です。 - -## 推奨フロー - -```txt -Excel → ExStruct(JSON) → Chunking → VectorDB → LLM -``` - -## JSON そのまま格納(構造優先) - -- ChartType / AutoShapeType / Coordinates が強い検索キー -- テーブル検索が精確 -- Lookup 型 RAG に最適 - -## Markdown 化して格納(生成品質優先) - -- LLM が読みやすい -- 回答が自然になる - -## ハイブリッド設計(最も推奨) - -```txt -VectorDB #1: Structural JSON -VectorDB #2: Markdown Summary -``` - -RAG 実行時: - -- #1 で構造マッチ -- #2 で自然言語マッチ - -両方合わせて LLM に送る。 From 60d168a9e5af3e459db8b6ac0870a51440816390 Mon Sep 17 00:00:00 2001 From: harumiWeb Date: Mon, 16 Feb 2026 22:27:08 +0900 Subject: [PATCH 18/25] feat: Add read range, read cells, and read formulas tools - Introduced new tools for reading ranges, specific cells, and formulas from extracted JSON data. - Implemented input and output models for each tool, including validation. - Enhanced server registration to support the new tools. - Updated existing tools and models to accommodate the new functionalities. - Added comprehensive tests for the new tools and their interactions with the server. - Created a new sheet_reader module to handle reading logic for ranges, cells, and formulas. --- docs/agents/FEATURE_SPEC.md | 144 ++++++++++ docs/agents/TASKS.md | 17 ++ docs/mcp.md | 43 +++ src/exstruct/mcp/__init__.py | 42 +++ src/exstruct/mcp/server.py | 117 ++++++++ src/exstruct/mcp/sheet_reader.py | 472 +++++++++++++++++++++++++++++++ src/exstruct/mcp/tools.py | 196 +++++++++++++ tests/mcp/test_server.py | 125 ++++++++ tests/mcp/test_sheet_reader.py | 161 +++++++++++ tests/mcp/test_tool_models.py | 34 ++- tests/mcp/test_tools_handlers.py | 73 +++++ 11 files changed, 1423 insertions(+), 1 deletion(-) create mode 100644 src/exstruct/mcp/sheet_reader.py create mode 100644 tests/mcp/test_sheet_reader.py diff --git a/docs/agents/FEATURE_SPEC.md b/docs/agents/FEATURE_SPEC.md index 6ba8162..c78a1bf 100644 --- a/docs/agents/FEATURE_SPEC.md +++ b/docs/agents/FEATURE_SPEC.md @@ -551,3 +551,147 @@ MCP運用時、次の迷いが発生しやすい。 - `Sheet is required when multiple sheets exist` エラーにシート候補が含まれる - `filter.cols` が数値キー・英字キー双方で機能する - 既存テストと追加テストが通過する + +--- + +## 16. 読み取りツール拡張(`read_range` / `read_cells` / `read_formulas`) + +### 16.1 背景 + +`exstruct_read_json_chunk` は汎用性が高い一方、確認作業では以下が冗長になりやすい。 + +- 範囲取得に `sheet` / `filter` / `cursor` の組み合わせが必要 +- 単発セル確認でもチャンク前提の手順が必要 +- 数式点検で `formulas_map` から座標を手作業で読む必要がある + +### 16.2 目的 + +- `A1:G10` のような直感的指定で値確認できるようにする +- 指定セル群のピンポイント確認を1コールで完結させる +- 数式点検を専用ツールで高速化する + +### 16.3 スコープ + +- 新規MCPツールを3つ追加する + - `exstruct_read_range` + - `exstruct_read_cells` + - `exstruct_read_formulas` +- 既存の `exstruct_extract` / `exstruct_read_json_chunk` は後方互換を維持し、変更しない + +### 16.4 共通方針 + +- 入力元は `exstruct_extract` で生成された `out_path`(JSON)とする +- シート選択ルールは `read_json_chunk` と同一 + - `sheet` 指定あり: そのシートを使用 + - `sheet` 省略かつ単一シート: 自動選択 + - `sheet` 省略かつ複数シート: エラー +- 参照座標はA1形式で統一する +- `alpha_col=true` 出力(英字キー)と数値キー出力の両方を透過的に扱う + +### 16.5 ツールI/F + +#### 16.5.1 `exstruct_read_range` + +Input: + +- `out_path: str` +- `sheet: str | None = None` +- `range: str`(A1範囲。例: `A1:G10`) +- `include_formulas: bool = false` +- `include_empty: bool = true` +- `max_cells: int = 10000` + +Output: + +- `book_name: str | None` +- `sheet_name: str` +- `range: str` +- `cells: list[CellReadItem]` +- `warnings: list[str]` + +`CellReadItem`: + +- `cell: str`(A1) +- `value: str | int | float | bool | None` +- `formula: str | None`(`include_formulas=true` の場合のみ設定) + +#### 16.5.2 `exstruct_read_cells` + +Input: + +- `out_path: str` +- `sheet: str | None = None` +- `addresses: list[str]`(A1セル番地。例: `["J98","J124"]`) +- `include_formulas: bool = true` + +Output: + +- `book_name: str | None` +- `sheet_name: str` +- `cells: list[CellReadItem]`(入力順を維持) +- `missing_cells: list[str]`(シート範囲外など) +- `warnings: list[str]` + +#### 16.5.3 `exstruct_read_formulas` + +Input: + +- `out_path: str` +- `sheet: str | None = None` +- `range: str | None = None`(未指定時はシート全体) +- `include_values: bool = false` + +Output: + +- `book_name: str | None` +- `sheet_name: str` +- `range: str | None` +- `formulas: list[FormulaReadItem]` +- `warnings: list[str]` + +`FormulaReadItem`: + +- `cell: str`(A1) +- `formula: str` +- `value: str | int | float | bool | None`(`include_values=true` の場合のみ設定) + +### 16.6 バリデーション規則 + +- `out_path` は存在ファイルかつ許可パス内であること +- `range` はA1範囲形式であること(`A1:G10`) +- `addresses` は空配列禁止、各要素がA1セル形式であること +- `max_cells` は正数で、展開セル数が上限を超えた場合は `ValueError` +- `exstruct_read_formulas` で `formulas_map` が無い場合: + - エラーではなく空結果 + `warnings` で `mode=verbose` 推奨を返す + +### 16.7 実行セマンティクス + +- 1回の呼び出しでJSONを1回だけ読み込み・パースする +- セル値参照は `sheet.rows[].c` から取得する +- 数式参照は `sheet.formulas_map` を主データソースとする +- 返却順: + - `read_range`: 行優先(top-left -> bottom-right) + - `read_cells`: 入力順 + - `read_formulas`: セル番地昇順(row, col) + +### 16.8 エラー処理 + +- A1形式不正: `ValueError` +- シート不在: `ValueError` +- 複数シートで `sheet` 未指定: `ValueError`(候補名を含める) +- JSON構造不正: `ValueError` + +### 16.9 後方互換性 + +- 既存ツールの入出力スキーマは変更しない +- 新規ツール追加のみ(破壊的変更なし) +- `read_json_chunk` の既存運用はそのまま利用可能 + +### 16.10 受け入れ基準 + +- `exstruct_read_range(range="A1:G10")` で70セルを取得できる +- `exstruct_read_cells(addresses=["J98","J124"])` で2セルを入力順に取得できる +- `exstruct_read_formulas(range="J2:J201")` で対象範囲の式一覧を取得できる +- `alpha_col=true/false` どちらのJSONでも同じ結果を返せる +- 不正A1指定で明確なエラーが返る +- `uv run task precommit-run` が通過する diff --git a/docs/agents/TASKS.md b/docs/agents/TASKS.md index bc5d39a..504b130 100644 --- a/docs/agents/TASKS.md +++ b/docs/agents/TASKS.md @@ -94,3 +94,20 @@ - [x] `src/exstruct/mcp/chunk_reader.py`: `filter.cols` を英字列キー(`A`, `AA`)対応に拡張する - [x] `tests/mcp/test_chunk_reader.py`: 英字列キー対応と新エラーメッセージのテストを追加する - [x] `uv run task precommit-run` を実行し、静的解析・型検査・テスト通過を確認する + +## 読み取りツール拡張(`read_range` / `read_cells` / `read_formulas`) + +- [x] `docs/agents/FEATURE_SPEC.md`: 新規3ツールのI/F・バリデーション・受け入れ基準を定義する +- [x] `src/exstruct/mcp/sheet_reader.py`: A1セル/範囲パーサーと共通読み取りロジックを実装する +- [x] `src/exstruct/mcp/sheet_reader.py`: `read_range`(A1範囲指定)を実装する +- [x] `src/exstruct/mcp/sheet_reader.py`: `read_cells`(複数セル指定)を実装する +- [x] `src/exstruct/mcp/sheet_reader.py`: `read_formulas`(数式一覧+任意で値)を実装する +- [x] `src/exstruct/mcp/tools.py`: `ReadRangeToolInput/Output`、`ReadCellsToolInput/Output`、`ReadFormulasToolInput/Output` を追加する +- [x] `src/exstruct/mcp/tools.py`: `run_read_range_tool`、`run_read_cells_tool`、`run_read_formulas_tool` を追加する +- [x] `src/exstruct/mcp/server.py`: `exstruct_read_range`、`exstruct_read_cells`、`exstruct_read_formulas` を登録する +- [x] `src/exstruct/mcp/server.py`: Docstringに利用例(`A1:G10`、`[\"J98\",\"J124\"]`)を追加する +- [x] `tests/mcp/test_sheet_reader.py`: A1パース、範囲上限、alpha_col互換、エラー系テストを追加する +- [x] `tests/mcp/test_tools_handlers.py`: 新規3ツールの入出力ハンドリングテストを追加する +- [x] `tests/mcp/test_server.py`: 新規3ツールの登録・引数連携テストを追加する +- [x] `docs/mcp.md`: 新規3ツールの使用手順と推奨フロー(extract→read_*)を追記する +- [x] `uv run task precommit-run` を実行し、静的解析・型検査・テスト通過を確認する diff --git a/docs/mcp.md b/docs/mcp.md index 06ec7c8..3786df4 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -8,6 +8,7 @@ so AI agents can call it safely as a tool. - Convert Excel into structured JSON (file output) - Edit Excel by applying patch operations (cell/sheet updates) - Read large JSON outputs in chunks +- Read A1 ranges / specific cells / formulas directly from extracted JSON - Pre-validate input files ## Installation @@ -36,6 +37,9 @@ exstruct-mcp --root C:\\data --log-file C:\\logs\\exstruct-mcp.log --on-conflict - `exstruct_extract` - `exstruct_patch` - `exstruct_read_json_chunk` +- `exstruct_read_range` +- `exstruct_read_cells` +- `exstruct_read_formulas` - `exstruct_validate_input` ### `exstruct_extract` defaults and mode guide @@ -69,6 +73,45 @@ Example sequence: 1. Call `exstruct_extract` to generate the output JSON file 2. Use `exstruct_read_json_chunk` to read only the parts you need +## Direct read tools (A1-oriented) + +Use these tools when you already know the target addresses and want faster, +less verbose reads than chunk traversal. + +- `exstruct_read_range` + - Read a rectangular A1 range (example: `A1:G10`) + - Optional: `include_formulas`, `include_empty`, `max_cells` +- `exstruct_read_cells` + - Read specific cells in one call (example: `["J98", "J124"]`) + - Optional: `include_formulas` +- `exstruct_read_formulas` + - Read formulas only (optionally restricted by A1 range) + - Optional: `include_values` + +Examples: + +```json +{ + "tool": "exstruct_read_range", + "out_path": "C:\\data\\book.json", + "sheet": "Data", + "range": "A1:G10" +} +{ + "tool": "exstruct_read_cells", + "out_path": "C:\\data\\book.json", + "sheet": "Data", + "addresses": ["J98", "J124"] +} +{ + "tool": "exstruct_read_formulas", + "out_path": "C:\\data\\book.json", + "sheet": "Data", + "range": "J2:J201", + "include_values": true +} +``` + ## Chunking guide ### Key parameters diff --git a/src/exstruct/mcp/__init__.py b/src/exstruct/mcp/__init__.py index cd37198..4be3bc5 100644 --- a/src/exstruct/mcp/__init__.py +++ b/src/exstruct/mcp/__init__.py @@ -26,18 +26,40 @@ PatchValue, run_patch, ) +from .sheet_reader import ( + CellReadItem, + FormulaReadItem, + ReadCellsRequest, + ReadCellsResult, + ReadFormulasRequest, + ReadFormulasResult, + ReadRangeRequest, + ReadRangeResult, + read_cells, + read_formulas, + read_range, +) from .tools import ( ExtractToolInput, ExtractToolOutput, PatchToolInput, PatchToolOutput, + ReadCellsToolInput, + ReadCellsToolOutput, + ReadFormulasToolInput, + ReadFormulasToolOutput, ReadJsonChunkToolInput, ReadJsonChunkToolOutput, + ReadRangeToolInput, + ReadRangeToolOutput, ValidateInputToolInput, ValidateInputToolOutput, run_extract_tool, run_patch_tool, + run_read_cells_tool, + run_read_formulas_tool, run_read_json_chunk_tool, + run_read_range_tool, run_validate_input_tool, ) from .validate_input import ( @@ -53,6 +75,7 @@ "ExtractToolInput", "ExtractToolOutput", "FormulaIssue", + "FormulaReadItem", "PatchDiffItem", "PatchErrorDetail", "PatchOp", @@ -62,11 +85,24 @@ "PatchToolOutput", "PatchValue", "PathPolicy", + "ReadCellsRequest", + "ReadCellsResult", + "ReadCellsToolInput", + "ReadCellsToolOutput", + "ReadFormulasRequest", + "ReadFormulasResult", + "ReadFormulasToolInput", + "ReadFormulasToolOutput", "ReadJsonChunkFilter", "ReadJsonChunkRequest", "ReadJsonChunkResult", "ReadJsonChunkToolInput", "ReadJsonChunkToolOutput", + "ReadRangeRequest", + "ReadRangeResult", + "ReadRangeToolInput", + "ReadRangeToolOutput", + "CellReadItem", "ValidateInputRequest", "ValidateInputResult", "ValidateInputToolInput", @@ -78,6 +114,12 @@ "run_extract_tool", "run_patch", "run_patch_tool", + "read_cells", + "read_formulas", "run_read_json_chunk_tool", + "read_range", + "run_read_cells_tool", + "run_read_formulas_tool", + "run_read_range_tool", "run_validate_input_tool", ] diff --git a/src/exstruct/mcp/server.py b/src/exstruct/mcp/server.py index dbc8a16..2051057 100644 --- a/src/exstruct/mcp/server.py +++ b/src/exstruct/mcp/server.py @@ -22,13 +22,22 @@ ExtractToolOutput, PatchToolInput, PatchToolOutput, + ReadCellsToolInput, + ReadCellsToolOutput, + ReadFormulasToolInput, + ReadFormulasToolOutput, ReadJsonChunkToolInput, ReadJsonChunkToolOutput, + ReadRangeToolInput, + ReadRangeToolOutput, ValidateInputToolInput, ValidateInputToolOutput, run_extract_tool, run_patch_tool, + run_read_cells_tool, + run_read_formulas_tool, run_read_json_chunk_tool, + run_read_range_tool, run_validate_input_tool, ) @@ -288,6 +297,114 @@ async def _read_json_chunk_tool( # pylint: disable=redefined-builtin chunk_tool = app.tool(name="exstruct_read_json_chunk") chunk_tool(_read_json_chunk_tool) + async def _read_range_tool( # pylint: disable=redefined-builtin + out_path: str, + sheet: str | None = None, + range: str = "A1", # noqa: A002 + include_formulas: bool = False, + include_empty: bool = True, + max_cells: int = 10_000, + ) -> ReadRangeToolOutput: + """Read a rectangular cell range from extracted JSON. + + Args: + out_path: Path to the extracted JSON file. + sheet: Optional sheet name. Required when workbook has multiple sheets. + range: A1 range (for example, \"A1:G10\"). + include_formulas: Include formula text for each returned cell. + include_empty: Include empty cells in the range. + max_cells: Safety limit for expanded cells. + + Returns: + Range read result payload. + """ + payload = ReadRangeToolInput( + out_path=out_path, + sheet=sheet, + range=range, + include_formulas=include_formulas, + include_empty=include_empty, + max_cells=max_cells, + ) + work = functools.partial( + run_read_range_tool, + payload, + policy=policy, + ) + result = cast(ReadRangeToolOutput, await anyio.to_thread.run_sync(work)) + return result + + read_range_tool = app.tool(name="exstruct_read_range") + read_range_tool(_read_range_tool) + + async def _read_cells_tool( + out_path: str, + addresses: list[str], + sheet: str | None = None, + include_formulas: bool = True, + ) -> ReadCellsToolOutput: + """Read specific cells from extracted JSON. + + Args: + out_path: Path to the extracted JSON file. + addresses: Target A1 addresses (for example, [\"J98\", \"J124\"]). + sheet: Optional sheet name. Required when workbook has multiple sheets. + include_formulas: Include formula text for each requested cell. + + Returns: + Cell read result payload. + """ + payload = ReadCellsToolInput( + out_path=out_path, + sheet=sheet, + addresses=addresses, + include_formulas=include_formulas, + ) + work = functools.partial( + run_read_cells_tool, + payload, + policy=policy, + ) + result = cast(ReadCellsToolOutput, await anyio.to_thread.run_sync(work)) + return result + + read_cells_tool = app.tool(name="exstruct_read_cells") + read_cells_tool(_read_cells_tool) + + async def _read_formulas_tool( # pylint: disable=redefined-builtin + out_path: str, + sheet: str | None = None, + range: str | None = None, # noqa: A002 + include_values: bool = False, + ) -> ReadFormulasToolOutput: + """Read formulas from extracted JSON. + + Args: + out_path: Path to the extracted JSON file. + sheet: Optional sheet name. Required when workbook has multiple sheets. + range: Optional A1 range to limit formula results (for example, \"J2:J201\"). + include_values: Include stored cell values with formulas. + + Returns: + Formula read result payload. + """ + payload = ReadFormulasToolInput( + out_path=out_path, + sheet=sheet, + range=range, + include_values=include_values, + ) + work = functools.partial( + run_read_formulas_tool, + payload, + policy=policy, + ) + result = cast(ReadFormulasToolOutput, await anyio.to_thread.run_sync(work)) + return result + + read_formulas_tool = app.tool(name="exstruct_read_formulas") + read_formulas_tool(_read_formulas_tool) + async def _validate_input_tool(xlsx_path: str) -> ValidateInputToolOutput: """Handle input validation tool call. diff --git a/src/exstruct/mcp/sheet_reader.py b/src/exstruct/mcp/sheet_reader.py new file mode 100644 index 0000000..64b154d --- /dev/null +++ b/src/exstruct/mcp/sheet_reader.py @@ -0,0 +1,472 @@ +from __future__ import annotations + +import json +from pathlib import Path +import re +from typing import Any, TypeAlias, cast + +from pydantic import BaseModel, Field + +from .io import PathPolicy + +JsonScalar: TypeAlias = str | int | float | bool | None +CellCoordinate: TypeAlias = tuple[int, int] +RangeCoordinate: TypeAlias = tuple[int, int, int, int] + +_CELL_RE = re.compile(r"^([A-Za-z]+)([1-9][0-9]*)$") + + +class CellReadItem(BaseModel): + """Cell read result item.""" + + cell: str + value: JsonScalar = None + formula: str | None = None + + +class FormulaReadItem(BaseModel): + """Formula read result item.""" + + cell: str + formula: str + value: JsonScalar = None + + +class ReadRangeRequest(BaseModel): + """Input model for range reading.""" + + out_path: Path + sheet: str | None = None + range: str + include_formulas: bool = False + include_empty: bool = True + max_cells: int = Field(default=10_000, ge=1) + + +class ReadRangeResult(BaseModel): + """Output model for range reading.""" + + book_name: str | None = None + sheet_name: str + range: str + cells: list[CellReadItem] = Field(default_factory=list) + warnings: list[str] = Field(default_factory=list) + + +class ReadCellsRequest(BaseModel): + """Input model for cell list reading.""" + + out_path: Path + sheet: str | None = None + addresses: list[str] = Field(min_length=1) + include_formulas: bool = True + + +class ReadCellsResult(BaseModel): + """Output model for cell list reading.""" + + book_name: str | None = None + sheet_name: str + cells: list[CellReadItem] = Field(default_factory=list) + missing_cells: list[str] = Field(default_factory=list) + warnings: list[str] = Field(default_factory=list) + + +class ReadFormulasRequest(BaseModel): + """Input model for formula reading.""" + + out_path: Path + sheet: str | None = None + range: str | None = None + include_values: bool = False + + +class ReadFormulasResult(BaseModel): + """Output model for formula reading.""" + + book_name: str | None = None + sheet_name: str + range: str | None = None + formulas: list[FormulaReadItem] = Field(default_factory=list) + warnings: list[str] = Field(default_factory=list) + + +def read_range( + request: ReadRangeRequest, *, policy: PathPolicy | None = None +) -> ReadRangeResult: + """Read a rectangular range from extracted JSON. + + Args: + request: Range read request. + policy: Optional path policy for access control. + + Returns: + Range read result. + """ + resolved = _resolve_output_path(request.out_path, policy=policy) + data = _parse_json(_read_text(resolved)) + sheet_name, sheet_data = _select_sheet(data, request.sheet) + start_row, start_col, end_row, end_col = _parse_range(request.range) + normalized_range = _format_range(start_row, start_col, end_row, end_col) + cell_count = (end_row - start_row + 1) * (end_col - start_col + 1) + if cell_count > request.max_cells: + raise ValueError( + "Requested range exceeds max_cells. " + f"range={normalized_range}, cells={cell_count}, max_cells={request.max_cells}" + ) + + value_map = _build_value_map(sheet_data) + formula_map, has_formula_map = _build_formula_map(sheet_data) + warnings: list[str] = [] + if request.include_formulas and not has_formula_map: + warnings.append( + "formulas_map is not available. Re-run exstruct_extract with mode='verbose' " + "to inspect formulas." + ) + + items: list[CellReadItem] = [] + for row in range(start_row, end_row + 1): + for col in range(start_col, end_col + 1): + coord = (row, col) + value = value_map.get(coord) + formula = formula_map.get(coord) if request.include_formulas else None + if not request.include_empty and value is None and formula is None: + continue + items.append( + CellReadItem( + cell=_format_cell(row, col), + value=value, + formula=formula, + ) + ) + + return ReadRangeResult( + book_name=_as_optional_str(data.get("book_name")), + sheet_name=sheet_name, + range=normalized_range, + cells=items, + warnings=warnings, + ) + + +def read_cells( + request: ReadCellsRequest, *, policy: PathPolicy | None = None +) -> ReadCellsResult: + """Read specific cells from extracted JSON. + + Args: + request: Cell list read request. + policy: Optional path policy for access control. + + Returns: + Cell list read result. + """ + resolved = _resolve_output_path(request.out_path, policy=policy) + data = _parse_json(_read_text(resolved)) + sheet_name, sheet_data = _select_sheet(data, request.sheet) + value_map = _build_value_map(sheet_data) + formula_map, has_formula_map = _build_formula_map(sheet_data) + + warnings: list[str] = [] + if request.include_formulas and not has_formula_map: + warnings.append( + "formulas_map is not available. Re-run exstruct_extract with mode='verbose' " + "to inspect formulas." + ) + + items: list[CellReadItem] = [] + missing_cells: list[str] = [] + for address in request.addresses: + row, col = _parse_cell(address) + normalized = _format_cell(row, col) + coord = (row, col) + value = value_map.get(coord) + formula = formula_map.get(coord) if request.include_formulas else None + if value is None and formula is None: + missing_cells.append(normalized) + items.append(CellReadItem(cell=normalized, value=value, formula=formula)) + + return ReadCellsResult( + book_name=_as_optional_str(data.get("book_name")), + sheet_name=sheet_name, + cells=items, + missing_cells=missing_cells, + warnings=warnings, + ) + + +def read_formulas( + request: ReadFormulasRequest, *, policy: PathPolicy | None = None +) -> ReadFormulasResult: + """Read formulas from extracted JSON. + + Args: + request: Formula read request. + policy: Optional path policy for access control. + + Returns: + Formula read result. + """ + resolved = _resolve_output_path(request.out_path, policy=policy) + data = _parse_json(_read_text(resolved)) + sheet_name, sheet_data = _select_sheet(data, request.sheet) + value_map = _build_value_map(sheet_data) + formula_map, has_formula_map = _build_formula_map(sheet_data) + + range_filter: RangeCoordinate | None = None + normalized_range: str | None = None + if request.range is not None: + range_filter = _parse_range(request.range) + normalized_range = _format_range(*range_filter) + + warnings: list[str] = [] + if not has_formula_map: + warnings.append( + "formulas_map is not available. Re-run exstruct_extract with mode='verbose' " + "to inspect formulas." + ) + return ReadFormulasResult( + book_name=_as_optional_str(data.get("book_name")), + sheet_name=sheet_name, + range=normalized_range, + formulas=[], + warnings=warnings, + ) + + items: list[FormulaReadItem] = [] + for row, col in sorted(formula_map): + if range_filter is not None and not _in_range((row, col), range_filter): + continue + coord = (row, col) + items.append( + FormulaReadItem( + cell=_format_cell(row, col), + formula=formula_map[coord], + value=value_map.get(coord) if request.include_values else None, + ) + ) + + return ReadFormulasResult( + book_name=_as_optional_str(data.get("book_name")), + sheet_name=sheet_name, + range=normalized_range, + formulas=items, + warnings=warnings, + ) + + +def _resolve_output_path(path: Path, *, policy: PathPolicy | None) -> Path: + """Resolve and validate output path.""" + resolved = policy.ensure_allowed(path) if policy else path.resolve() + if not resolved.exists(): + raise FileNotFoundError(f"Output file not found: {resolved}") + if not resolved.is_file(): + raise ValueError(f"Output path is not a file: {resolved}") + return resolved + + +def _read_text(path: Path) -> str: + """Read UTF-8 text from file.""" + return path.read_text(encoding="utf-8") + + +def _parse_json(text: str) -> dict[str, Any]: + """Parse JSON text into object root.""" + parsed = json.loads(text) + if not isinstance(parsed, dict): + raise ValueError("Invalid workbook JSON: expected object at root.") + return cast(dict[str, Any], parsed) + + +def _select_sheet( + data: dict[str, Any], sheet: str | None +) -> tuple[str, dict[str, Any]]: + """Select sheet payload from workbook object.""" + sheets = data.get("sheets") + if not isinstance(sheets, dict): + raise ValueError("Invalid workbook JSON: sheets is not a mapping.") + if sheet is not None: + selected = sheets.get(sheet) + if not isinstance(selected, dict): + raise ValueError(f"Sheet not found: {sheet}") + return sheet, selected + if len(sheets) == 1: + only_name = next(iter(sheets.keys())) + only_payload = sheets[only_name] + if not isinstance(only_payload, dict): + raise ValueError("Invalid workbook JSON: sheet payload is not an object.") + return str(only_name), only_payload + names = ", ".join(str(name) for name in sheets.keys()) + raise ValueError( + "Sheet is required when multiple sheets exist. " + f"Available sheets: {names}. " + "Retry with `sheet=''`." + ) + + +def _parse_cell(address: str) -> CellCoordinate: + """Parse A1 cell address into row/column tuple (1-based).""" + match = _CELL_RE.fullmatch(address.strip()) + if match is None: + raise ValueError(f"Invalid A1 cell address: {address}") + col_label, row_text = match.groups() + row = int(row_text) + col = _alpha_to_col(col_label) + return row, col + + +def _parse_range(range_text: str) -> RangeCoordinate: + """Parse A1 range text into start/end tuple.""" + cleaned = range_text.strip() + if ":" in cleaned: + left, right = cleaned.split(":", maxsplit=1) + start_row, start_col = _parse_cell(left) + end_row, end_col = _parse_cell(right) + else: + start_row, start_col = _parse_cell(cleaned) + end_row, end_col = start_row, start_col + if start_row > end_row or start_col > end_col: + raise ValueError(f"Invalid A1 range: {range_text}") + return start_row, start_col, end_row, end_col + + +def _build_value_map(sheet_data: dict[str, Any]) -> dict[CellCoordinate, JsonScalar]: + """Build coordinate->value map from sheet rows.""" + rows = sheet_data.get("rows") + if not isinstance(rows, list): + return {} + value_map: dict[CellCoordinate, JsonScalar] = {} + for row_payload in rows: + if not isinstance(row_payload, dict): + continue + row_index = row_payload.get("r") + if not isinstance(row_index, int) or row_index <= 0: + continue + cols = row_payload.get("c") + if not isinstance(cols, dict): + continue + for key, raw_value in cols.items(): + if not isinstance(key, str): + continue + col_index = _parse_col_key(key) + if col_index is None: + continue + value = _normalize_scalar(raw_value) + value_map[(row_index, col_index)] = value + return value_map + + +def _build_formula_map( + sheet_data: dict[str, Any], +) -> tuple[dict[CellCoordinate, str], bool]: + """Build coordinate->formula map from formulas_map.""" + formulas_raw = sheet_data.get("formulas_map") + if formulas_raw is None: + return {}, False + if not isinstance(formulas_raw, dict): + return {}, False + formula_map: dict[CellCoordinate, str] = {} + for formula, positions in formulas_raw.items(): + if not isinstance(formula, str): + continue + if not isinstance(positions, list): + continue + for position in positions: + parsed = _parse_formula_position(position) + if parsed is None: + continue + formula_map[parsed] = formula + return formula_map, True + + +def _parse_formula_position(position: object) -> CellCoordinate | None: + """Parse formulas_map position into row/column tuple.""" + if not isinstance(position, list | tuple): + return None + if len(position) != 2: + return None + row = position[0] + col = position[1] + if not isinstance(row, int) or not isinstance(col, int): + return None + if row <= 0 or col < 0: + return None + return row, col + 1 + + +def _parse_col_key(key: str) -> int | None: + """Parse cell column key into 1-based column index.""" + stripped = key.strip() + try: + zero_based = int(stripped) + except (TypeError, ValueError): + pass + else: + if zero_based < 0: + return None + return zero_based + 1 + normalized = stripped.upper() + if not normalized.isalpha(): + return None + return _alpha_to_col(normalized) + + +def _alpha_to_col(label: str) -> int: + """Convert alphabetic column label to 1-based index.""" + acc = 0 + for char in label.strip().upper(): + code = ord(char) + if code < ord("A") or code > ord("Z"): + raise ValueError(f"Invalid column label: {label}") + acc = acc * 26 + (code - ord("A") + 1) + return acc + + +def _col_to_alpha(col_index: int) -> str: + """Convert 1-based column index to alphabetic column label.""" + if col_index <= 0: + raise ValueError(f"Invalid column index: {col_index}") + current = col_index + chars: list[str] = [] + while current > 0: + current -= 1 + chars.append(chr(ord("A") + (current % 26))) + current //= 26 + return "".join(reversed(chars)) + + +def _format_cell(row: int, col: int) -> str: + """Format row/column as A1 cell address.""" + return f"{_col_to_alpha(col)}{row}" + + +def _format_range(start_row: int, start_col: int, end_row: int, end_col: int) -> str: + """Format start/end coordinates as A1 range.""" + start = _format_cell(start_row, start_col) + end = _format_cell(end_row, end_col) + if start == end: + return start + return f"{start}:{end}" + + +def _in_range(cell: CellCoordinate, bounds: RangeCoordinate) -> bool: + """Check whether cell is included in range bounds.""" + row, col = cell + start_row, start_col, end_row, end_col = bounds + return start_row <= row <= end_row and start_col <= col <= end_col + + +def _normalize_scalar(value: object) -> JsonScalar: + """Normalize arbitrary JSON value to scalar.""" + if isinstance(value, str | int | float | bool) or value is None: + return value + return str(value) + + +def _as_optional_str(value: object) -> str | None: + """Convert object to optional string.""" + if value is None: + return None + if isinstance(value, str): + return value + return str(value) diff --git a/src/exstruct/mcp/tools.py b/src/exstruct/mcp/tools.py index 0bd5a6a..cd2377e 100644 --- a/src/exstruct/mcp/tools.py +++ b/src/exstruct/mcp/tools.py @@ -31,6 +31,19 @@ PatchResult, run_patch, ) +from .sheet_reader import ( + CellReadItem, + FormulaReadItem, + ReadCellsRequest, + ReadCellsResult, + ReadFormulasRequest, + ReadFormulasResult, + ReadRangeRequest, + ReadRangeResult, + read_cells, + read_formulas, + read_range, +) from .validate_input import ( ValidateInputRequest, ValidateInputResult, @@ -77,6 +90,65 @@ class ReadJsonChunkToolOutput(BaseModel): warnings: list[str] = Field(default_factory=list) +class ReadRangeToolInput(BaseModel): + """MCP tool input for range reading.""" + + out_path: str + sheet: str | None = None + range: str = Field(...) # noqa: A003 + include_formulas: bool = False + include_empty: bool = True + max_cells: int = Field(default=10_000, ge=1) + + +class ReadRangeToolOutput(BaseModel): + """MCP tool output for range reading.""" + + book_name: str | None = None + sheet_name: str + range: str # noqa: A003 + cells: list[CellReadItem] = Field(default_factory=list) + warnings: list[str] = Field(default_factory=list) + + +class ReadCellsToolInput(BaseModel): + """MCP tool input for cell list reading.""" + + out_path: str + sheet: str | None = None + addresses: list[str] = Field(min_length=1) + include_formulas: bool = True + + +class ReadCellsToolOutput(BaseModel): + """MCP tool output for cell list reading.""" + + book_name: str | None = None + sheet_name: str + cells: list[CellReadItem] = Field(default_factory=list) + missing_cells: list[str] = Field(default_factory=list) + warnings: list[str] = Field(default_factory=list) + + +class ReadFormulasToolInput(BaseModel): + """MCP tool input for formula reading.""" + + out_path: str + sheet: str | None = None + range: str | None = None # noqa: A003 + include_values: bool = False + + +class ReadFormulasToolOutput(BaseModel): + """MCP tool output for formula reading.""" + + book_name: str | None = None + sheet_name: str + range: str | None = None # noqa: A003 + formulas: list[FormulaReadItem] = Field(default_factory=list) + warnings: list[str] = Field(default_factory=list) + + class ValidateInputToolInput(BaseModel): """MCP tool input for validating Excel files.""" @@ -167,6 +239,74 @@ def run_read_json_chunk_tool( return _to_read_json_chunk_output(result) +def run_read_range_tool( + payload: ReadRangeToolInput, *, policy: PathPolicy | None = None +) -> ReadRangeToolOutput: + """Run the range read tool handler. + + Args: + payload: Tool input payload. + policy: Optional path policy for access control. + + Returns: + Tool output payload. + """ + request = ReadRangeRequest( + out_path=Path(payload.out_path), + sheet=payload.sheet, + range=payload.range, + include_formulas=payload.include_formulas, + include_empty=payload.include_empty, + max_cells=payload.max_cells, + ) + result = read_range(request, policy=policy) + return _to_read_range_tool_output(result) + + +def run_read_cells_tool( + payload: ReadCellsToolInput, *, policy: PathPolicy | None = None +) -> ReadCellsToolOutput: + """Run the cell list read tool handler. + + Args: + payload: Tool input payload. + policy: Optional path policy for access control. + + Returns: + Tool output payload. + """ + request = ReadCellsRequest( + out_path=Path(payload.out_path), + sheet=payload.sheet, + addresses=payload.addresses, + include_formulas=payload.include_formulas, + ) + result = read_cells(request, policy=policy) + return _to_read_cells_tool_output(result) + + +def run_read_formulas_tool( + payload: ReadFormulasToolInput, *, policy: PathPolicy | None = None +) -> ReadFormulasToolOutput: + """Run the formulas read tool handler. + + Args: + payload: Tool input payload. + policy: Optional path policy for access control. + + Returns: + Tool output payload. + """ + request = ReadFormulasRequest( + out_path=Path(payload.out_path), + sheet=payload.sheet, + range=payload.range, + include_values=payload.include_values, + ) + result = read_formulas(request, policy=policy) + return _to_read_formulas_tool_output(result) + + def run_validate_input_tool( payload: ValidateInputToolInput, *, policy: PathPolicy | None = None ) -> ValidateInputToolOutput: @@ -250,6 +390,62 @@ def _to_read_json_chunk_output( ) +def _to_read_range_tool_output(result: ReadRangeResult) -> ReadRangeToolOutput: + """Convert internal result to range tool output. + + Args: + result: Internal range read result. + + Returns: + Tool output payload. + """ + return ReadRangeToolOutput( + book_name=result.book_name, + sheet_name=result.sheet_name, + range=result.range, + cells=result.cells, + warnings=result.warnings, + ) + + +def _to_read_cells_tool_output(result: ReadCellsResult) -> ReadCellsToolOutput: + """Convert internal result to cell list tool output. + + Args: + result: Internal cell list read result. + + Returns: + Tool output payload. + """ + return ReadCellsToolOutput( + book_name=result.book_name, + sheet_name=result.sheet_name, + cells=result.cells, + missing_cells=result.missing_cells, + warnings=result.warnings, + ) + + +def _to_read_formulas_tool_output( + result: ReadFormulasResult, +) -> ReadFormulasToolOutput: + """Convert internal result to formulas tool output. + + Args: + result: Internal formula read result. + + Returns: + Tool output payload. + """ + return ReadFormulasToolOutput( + book_name=result.book_name, + sheet_name=result.sheet_name, + range=result.range, + formulas=result.formulas, + warnings=result.warnings, + ) + + def _to_validate_input_output( result: ValidateInputResult, ) -> ValidateInputToolOutput: diff --git a/tests/mcp/test_server.py b/tests/mcp/test_server.py index af7ed5b..6064f4a 100644 --- a/tests/mcp/test_server.py +++ b/tests/mcp/test_server.py @@ -16,8 +16,14 @@ ExtractToolOutput, PatchToolInput, PatchToolOutput, + ReadCellsToolInput, + ReadCellsToolOutput, + ReadFormulasToolInput, + ReadFormulasToolOutput, ReadJsonChunkToolInput, ReadJsonChunkToolOutput, + ReadRangeToolInput, + ReadRangeToolOutput, ValidateInputToolInput, ValidateInputToolOutput, ) @@ -211,6 +217,125 @@ async def fake_run_sync(func: Callable[[], object]) -> object: assert patch_call[0].preflight_formula_check is False +def test_register_tools_passes_read_tool_arguments( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + app = DummyApp() + policy = PathPolicy(root=tmp_path) + calls: dict[str, tuple[object, ...]] = {} + + def fake_run_extract_tool( + payload: ExtractToolInput, + *, + policy: PathPolicy, + on_conflict: OnConflictPolicy, + ) -> ExtractToolOutput: + return ExtractToolOutput(out_path="out.json") + + def fake_run_read_json_chunk_tool( + payload: ReadJsonChunkToolInput, + *, + policy: PathPolicy, + ) -> ReadJsonChunkToolOutput: + return ReadJsonChunkToolOutput(chunk="{}") + + def fake_run_read_range_tool( + payload: ReadRangeToolInput, + *, + policy: PathPolicy, + ) -> ReadRangeToolOutput: + calls["range"] = (payload, policy) + return ReadRangeToolOutput( + book_name="book", + sheet_name="Sheet1", + range="A1:B2", + cells=[], + ) + + def fake_run_read_cells_tool( + payload: ReadCellsToolInput, + *, + policy: PathPolicy, + ) -> ReadCellsToolOutput: + calls["cells"] = (payload, policy) + return ReadCellsToolOutput(book_name="book", sheet_name="Sheet1", cells=[]) + + def fake_run_read_formulas_tool( + payload: ReadFormulasToolInput, + *, + policy: PathPolicy, + ) -> ReadFormulasToolOutput: + calls["formulas"] = (payload, policy) + return ReadFormulasToolOutput( + book_name="book", sheet_name="Sheet1", formulas=[] + ) + + def fake_run_validate_input_tool( + payload: ValidateInputToolInput, + *, + policy: PathPolicy, + ) -> ValidateInputToolOutput: + return ValidateInputToolOutput(is_readable=True) + + def fake_run_patch_tool( + payload: PatchToolInput, + *, + policy: PathPolicy, + on_conflict: OnConflictPolicy, + ) -> PatchToolOutput: + return PatchToolOutput(out_path="out.xlsx", patch_diff=[]) + + async def fake_run_sync(func: Callable[[], object]) -> object: + return func() + + monkeypatch.setattr(server, "run_extract_tool", fake_run_extract_tool) + monkeypatch.setattr( + server, "run_read_json_chunk_tool", fake_run_read_json_chunk_tool + ) + monkeypatch.setattr(server, "run_read_range_tool", fake_run_read_range_tool) + monkeypatch.setattr(server, "run_read_cells_tool", fake_run_read_cells_tool) + monkeypatch.setattr(server, "run_read_formulas_tool", fake_run_read_formulas_tool) + monkeypatch.setattr(server, "run_validate_input_tool", fake_run_validate_input_tool) + monkeypatch.setattr(server, "run_patch_tool", fake_run_patch_tool) + monkeypatch.setattr(anyio.to_thread, "run_sync", fake_run_sync) + + server._register_tools(app, policy, default_on_conflict="overwrite") + + read_range_tool = cast( + Callable[..., Awaitable[object]], app.tools["exstruct_read_range"] + ) + anyio.run( + _call_async, + read_range_tool, + {"out_path": "out.json", "range": "A1:B2", "include_formulas": True}, + ) + read_cells_tool = cast( + Callable[..., Awaitable[object]], app.tools["exstruct_read_cells"] + ) + anyio.run( + _call_async, + read_cells_tool, + {"out_path": "out.json", "addresses": ["J98", "J124"]}, + ) + read_formulas_tool = cast( + Callable[..., Awaitable[object]], app.tools["exstruct_read_formulas"] + ) + anyio.run( + _call_async, + read_formulas_tool, + {"out_path": "out.json", "range": "J2:J201", "include_values": True}, + ) + + range_call = cast(tuple[ReadRangeToolInput, PathPolicy], calls["range"]) + assert range_call[0].range == "A1:B2" + assert range_call[0].include_formulas is True + cells_call = cast(tuple[ReadCellsToolInput, PathPolicy], calls["cells"]) + assert cells_call[0].addresses == ["J98", "J124"] + formulas_call = cast(tuple[ReadFormulasToolInput, PathPolicy], calls["formulas"]) + assert formulas_call[0].range == "J2:J201" + assert formulas_call[0].include_values is True + + def test_register_tools_accepts_patch_ops_json_strings( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: diff --git a/tests/mcp/test_sheet_reader.py b/tests/mcp/test_sheet_reader.py new file mode 100644 index 0000000..f176ac9 --- /dev/null +++ b/tests/mcp/test_sheet_reader.py @@ -0,0 +1,161 @@ +from __future__ import annotations + +from collections.abc import Mapping +import json +from pathlib import Path + +import pytest + +from exstruct.mcp.sheet_reader import ( + ReadCellsRequest, + ReadFormulasRequest, + ReadRangeRequest, + read_cells, + read_formulas, + read_range, +) + + +def _write_json(path: Path, data: Mapping[str, object]) -> None: + path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8") + + +def test_read_range_with_a1_range(tmp_path: Path) -> None: + data = { + "book_name": "book", + "sheets": { + "Data": { + "rows": [ + {"r": 1, "c": {"0": "A1", "1": "B1"}}, + {"r": 2, "c": {"0": "A2"}}, + ] + } + }, + } + out = tmp_path / "out.json" + _write_json(out, data) + + request = ReadRangeRequest(out_path=out, sheet="Data", range="A1:B2") + result = read_range(request) + + assert result.range == "A1:B2" + assert [item.cell for item in result.cells] == ["A1", "B1", "A2", "B2"] + assert [item.value for item in result.cells] == ["A1", "B1", "A2", None] + + +def test_read_range_rejects_invalid_range(tmp_path: Path) -> None: + out = tmp_path / "out.json" + _write_json(out, {"book_name": "book", "sheets": {"Data": {"rows": []}}}) + request = ReadRangeRequest(out_path=out, sheet="Data", range="1A:B2") + with pytest.raises(ValueError, match="Invalid A1 cell address"): + read_range(request) + + +def test_read_range_rejects_too_large_range(tmp_path: Path) -> None: + out = tmp_path / "out.json" + _write_json(out, {"book_name": "book", "sheets": {"Data": {"rows": []}}}) + request = ReadRangeRequest(out_path=out, sheet="Data", range="A1:C3", max_cells=8) + with pytest.raises(ValueError, match="Requested range exceeds max_cells"): + read_range(request) + + +def test_read_cells_keeps_input_order_and_reports_missing(tmp_path: Path) -> None: + data = { + "book_name": "book", + "sheets": { + "Data": { + "rows": [ + {"r": 1, "c": {"0": "head"}}, + {"r": 98, "c": {"9": 12345}}, + ], + "formulas_map": {"=SUM(D124:F124)*G124*(1-H124)": [[124, 9]]}, + } + }, + } + out = tmp_path / "out.json" + _write_json(out, data) + + request = ReadCellsRequest( + out_path=out, + sheet="Data", + addresses=["J124", "B2", "J98", "A1"], + include_formulas=True, + ) + result = read_cells(request) + + assert [item.cell for item in result.cells] == ["J124", "B2", "J98", "A1"] + assert result.missing_cells == ["B2"] + assert result.cells[0].formula == "=SUM(D124:F124)*G124*(1-H124)" + assert result.cells[2].value == 12345 + assert result.cells[3].value == "head" + + +def test_read_formulas_with_range_and_values_alpha_col(tmp_path: Path) -> None: + data = { + "book_name": "book", + "sheets": { + "Data": { + "rows": [ + {"r": 1, "c": {"A": "header"}}, + {"r": 98, "c": {"J": 98765}}, + ], + "formulas_map": { + "=SUM(D98:F98)*G98*(1-H98)": [[98, 9]], + "=A1": [[1, 0]], + }, + } + }, + } + out = tmp_path / "out.json" + _write_json(out, data) + + request = ReadFormulasRequest( + out_path=out, + sheet="Data", + range="J2:J200", + include_values=True, + ) + result = read_formulas(request) + + assert result.range == "J2:J200" + assert len(result.formulas) == 1 + assert result.formulas[0].cell == "J98" + assert result.formulas[0].formula == "=SUM(D98:F98)*G98*(1-H98)" + assert result.formulas[0].value == 98765 + + +def test_read_formulas_warns_when_formulas_map_missing(tmp_path: Path) -> None: + data = {"book_name": "book", "sheets": {"Data": {"rows": []}}} + out = tmp_path / "out.json" + _write_json(out, data) + + request = ReadFormulasRequest(out_path=out, sheet="Data") + result = read_formulas(request) + + assert result.formulas == [] + assert any("mode='verbose'" in warning for warning in result.warnings) + + +def test_read_range_warns_when_include_formulas_without_map(tmp_path: Path) -> None: + data = {"book_name": "book", "sheets": {"Data": {"rows": []}}} + out = tmp_path / "out.json" + _write_json(out, data) + + request = ReadRangeRequest( + out_path=out, sheet="Data", range="A1:A1", include_formulas=True + ) + result = read_range(request) + + assert len(result.cells) == 1 + assert result.cells[0].cell == "A1" + assert any("mode='verbose'" in warning for warning in result.warnings) + + +def test_read_cells_requires_sheet_for_multi_sheet_payload(tmp_path: Path) -> None: + data = {"book_name": "book", "sheets": {"A": {"rows": []}, "B": {"rows": []}}} + out = tmp_path / "out.json" + _write_json(out, data) + + request = ReadCellsRequest(out_path=out, addresses=["A1"]) + with pytest.raises(ValueError, match=r"Available sheets: A, B"): + read_cells(request) diff --git a/tests/mcp/test_tool_models.py b/tests/mcp/test_tool_models.py index 7a4f920..9606961 100644 --- a/tests/mcp/test_tool_models.py +++ b/tests/mcp/test_tool_models.py @@ -3,7 +3,14 @@ from pydantic import ValidationError import pytest -from exstruct.mcp.tools import ExtractToolInput, PatchToolInput, ReadJsonChunkToolInput +from exstruct.mcp.tools import ( + ExtractToolInput, + PatchToolInput, + ReadCellsToolInput, + ReadFormulasToolInput, + ReadJsonChunkToolInput, + ReadRangeToolInput, +) def test_extract_tool_input_defaults() -> None: @@ -30,3 +37,28 @@ def test_patch_tool_input_defaults() -> None: assert payload.dry_run is False assert payload.return_inverse_ops is False assert payload.preflight_formula_check is False + + +def test_read_range_tool_input_defaults() -> None: + payload = ReadRangeToolInput(out_path="out.json", range="A1:B2") + assert payload.sheet is None + assert payload.include_formulas is False + assert payload.include_empty is True + assert payload.max_cells == 10_000 + + +def test_read_range_tool_input_rejects_invalid_max_cells() -> None: + with pytest.raises(ValidationError): + ReadRangeToolInput(out_path="out.json", range="A1:B2", max_cells=0) + + +def test_read_cells_tool_input_rejects_empty_addresses() -> None: + with pytest.raises(ValidationError): + ReadCellsToolInput(out_path="out.json", addresses=[]) + + +def test_read_formulas_tool_input_defaults() -> None: + payload = ReadFormulasToolInput(out_path="out.json") + assert payload.sheet is None + assert payload.range is None + assert payload.include_values is False diff --git a/tests/mcp/test_tools_handlers.py b/tests/mcp/test_tools_handlers.py index 1e790d8..d06effd 100644 --- a/tests/mcp/test_tools_handlers.py +++ b/tests/mcp/test_tools_handlers.py @@ -12,6 +12,14 @@ ) from exstruct.mcp.extract_runner import ExtractRequest, ExtractResult from exstruct.mcp.patch_runner import PatchRequest, PatchResult +from exstruct.mcp.sheet_reader import ( + ReadCellsRequest, + ReadCellsResult, + ReadFormulasRequest, + ReadFormulasResult, + ReadRangeRequest, + ReadRangeResult, +) from exstruct.mcp.validate_input import ValidateInputRequest, ValidateInputResult @@ -75,6 +83,71 @@ def _fake_read_json_chunk( assert request.filter is not None +def test_run_read_range_tool_builds_request( + monkeypatch: pytest.MonkeyPatch, +) -> None: + captured: dict[str, object] = {} + + def _fake_read_range( + request: ReadRangeRequest, *, policy: object | None = None + ) -> ReadRangeResult: + captured["request"] = request + return ReadRangeResult( + book_name="book", + sheet_name="Sheet1", + range="A1:B2", + cells=[], + ) + + monkeypatch.setattr(tools, "read_range", _fake_read_range) + payload = tools.ReadRangeToolInput(out_path="out.json", range="A1:B2") + tools.run_read_range_tool(payload) + request = captured["request"] + assert isinstance(request, ReadRangeRequest) + assert request.out_path == Path("out.json") + assert request.range == "A1:B2" + + +def test_run_read_cells_tool_builds_request( + monkeypatch: pytest.MonkeyPatch, +) -> None: + captured: dict[str, object] = {} + + def _fake_read_cells( + request: ReadCellsRequest, *, policy: object | None = None + ) -> ReadCellsResult: + captured["request"] = request + return ReadCellsResult(book_name="book", sheet_name="Sheet1", cells=[]) + + monkeypatch.setattr(tools, "read_cells", _fake_read_cells) + payload = tools.ReadCellsToolInput(out_path="out.json", addresses=["A1", "B2"]) + tools.run_read_cells_tool(payload) + request = captured["request"] + assert isinstance(request, ReadCellsRequest) + assert request.out_path == Path("out.json") + assert request.addresses == ["A1", "B2"] + + +def test_run_read_formulas_tool_builds_request( + monkeypatch: pytest.MonkeyPatch, +) -> None: + captured: dict[str, object] = {} + + def _fake_read_formulas( + request: ReadFormulasRequest, *, policy: object | None = None + ) -> ReadFormulasResult: + captured["request"] = request + return ReadFormulasResult(book_name="book", sheet_name="Sheet1", formulas=[]) + + monkeypatch.setattr(tools, "read_formulas", _fake_read_formulas) + payload = tools.ReadFormulasToolInput(out_path="out.json", range="J2:J20") + tools.run_read_formulas_tool(payload) + request = captured["request"] + assert isinstance(request, ReadFormulasRequest) + assert request.out_path == Path("out.json") + assert request.range == "J2:J20" + + def test_run_validate_input_tool_builds_request( monkeypatch: pytest.MonkeyPatch, ) -> None: From 69c20ced98ddd8d16f6cebb6fe9993db2194e1e9 Mon Sep 17 00:00:00 2001 From: harumiWeb Date: Mon, 16 Feb 2026 22:47:19 +0900 Subject: [PATCH 19/25] feat: Add release notes for v0.4.4, detailing new Excel editing MVP, read tools, and improvements --- CHANGELOG.md | 15 +++++++++++++++ docs/release-notes/v0.4.4.md | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 docs/release-notes/v0.4.4.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 851081c..6bae0a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,21 @@ All notable changes to this project are documented in this file. This changelog - _No unreleased changes yet._ +## [0.4.4] - 2026-02-16 + +### Added + +- Added an MVP of Excel editing for MCP via `exstruct_patch`, including atomic apply semantics and expanded operations: `set_range_values`, `fill_formula`, `set_value_if`, and `set_formula_if`. +- Added direct A1-oriented MCP read tools for extracted JSON: `exstruct_read_range`, `exstruct_read_cells`, and `exstruct_read_formulas`. +- Added patch safety/review options: `dry_run`, `return_inverse_ops`, `preflight_formula_check`, and `auto_formula`. + +### Changed + +- Improved `exstruct_patch` input compatibility: `ops` now accepts both object lists (recommended) and JSON object strings. +- Enabled `alpha_col` support more broadly across extraction/read flows, and added `merged_ranges` output support for alpha-column mode. +- Updated MCP documentation and chunking guidance, including clearer error messages and mode guidance. +- Changed MCP default conflict policy to `overwrite` for output handling. + ## [0.4.2] - 2026-01-23 ### Changed diff --git a/docs/release-notes/v0.4.4.md b/docs/release-notes/v0.4.4.md new file mode 100644 index 0000000..7969efd --- /dev/null +++ b/docs/release-notes/v0.4.4.md @@ -0,0 +1,34 @@ +# v0.4.4 Release Notes + +This release delivers an MVP for Excel editing on the MCP server, plus new +direct-read tools to make agent workflows faster and more precise. + +## Highlights + +- Added Excel edit MVP via `exstruct_patch` with atomic apply semantics: + - Core operations: `set_value`, `set_formula`, `add_sheet` + - Extended operations: `set_range_values`, `fill_formula`, + `set_value_if`, `set_formula_if` + - Safety and review options: `dry_run`, `return_inverse_ops`, + `preflight_formula_check`, `auto_formula` + - Better output handling: output directory creation, structured errors, and + default conflict policy set to `overwrite` + - Compatibility improvement: `ops` now accepts both object lists + (recommended) and JSON object strings +- Added direct A1-oriented read tools for extracted JSON: + - `exstruct_read_range` + - `exstruct_read_cells` + - `exstruct_read_formulas` +- Improved MCP extraction/read consistency: + - Added `alpha_col` support (Excel-style column keys like `A`, `B`, ...) + across CLI/MCP extraction paths + - Added `merged_ranges` output for `alpha_col` mode + - Clarified MCP mode/chunk guidance and improved chunk-reader error messages +- Expanded MCP documentation, tool schemas, and tests for patch/read flows. + +## Notes + +- In MCP, `options.alpha_col` now defaults to `true`. Set + `options.alpha_col=false` if you need legacy numeric string column keys. +- `exstruct_patch` follows server-level `--on-conflict` unless overridden in + the tool call. From 068919b0a5c03e03c879580afacf513031948905 Mon Sep 17 00:00:00 2001 From: harumiWeb Date: Mon, 16 Feb 2026 23:04:02 +0900 Subject: [PATCH 20/25] feat: Add Codacy Issues Fetcher skill with workflow and error handling documentation --- .agents/skills/codacy-issues-fetcher/SKILL.md | 54 +++++++++++++++++++ .../codacy-issues-fetcher/agents/openai.yaml | 4 ++ 2 files changed, 58 insertions(+) create mode 100644 .agents/skills/codacy-issues-fetcher/SKILL.md create mode 100644 .agents/skills/codacy-issues-fetcher/agents/openai.yaml diff --git a/.agents/skills/codacy-issues-fetcher/SKILL.md b/.agents/skills/codacy-issues-fetcher/SKILL.md new file mode 100644 index 0000000..11d23ec --- /dev/null +++ b/.agents/skills/codacy-issues-fetcher/SKILL.md @@ -0,0 +1,54 @@ +--- +name: codacy-issues-fetcher +description: Retrieve and format Codacy analysis issues by running `scripts/codacy_issues.py` in the ExStruct workspace. Use when users ask to inspect repository or pull-request Codacy findings, filter by severity, or produce structured issue output for review and fix planning. +--- + +# Codacy Issues Fetcher + +Run `scripts/codacy_issues.py` as the primary interface to Codacy issue retrieval. +Avoid reimplementing API calls unless the script itself must be changed. + +## Workflow + +1. Confirm prerequisites. +- Run from repository root so `scripts/codacy_issues.py` is reachable. +- Ensure `CODACY_API_TOKEN` is set and valid. +- Prefer explicit `org` and `repo` if user provides them; otherwise rely on Git `origin` auto-detection. + +2. Choose scope and severity. +- Repository scope: omit `--pr`. +- Pull request scope: pass `--pr `. +- Severity filter (`--min-level`): `Error`, `High`, `Warning`, `Info`. +- Provider (`--provider`): `gh`, `gl`, `bb` (default is effectively `gh`). + +3. Run one of the command patterns. + +```powershell +# Repository issues (explicit target) +python scripts/codacy_issues.py --provider gh --min-level Warning + +# Pull request issues (explicit target) +python scripts/codacy_issues.py --pr --provider gh --min-level Warning + +# Pull request issues (auto-detect org/repo from git origin) +python scripts/codacy_issues.py --pr --min-level Warning +``` + +4. Parse output JSON and respond with actionable summary. +- Trust payload fields: `scope`, `organization`, `repository`, `pullRequest`, `minLevel`, `total`, `issues`. +- `issues` entries are formatted as: + ` | : | | | ` +- Report high-severity findings first, then summarize counts. + +## Error Handling + +- `HTTP 401` / `Unauthorized`: token invalid or missing permissions. Ask user to set or refresh `CODACY_API_TOKEN`. +- `CODACY_API_TOKEN is not set`: export the environment variable before retrying. +- `Invalid --provider`: use only `gh`, `gl`, or `bb`. +- Segment validation errors (`Invalid org/repo/pr`): sanitize input and rerun. + +## Output Policy + +- Return concise triage-ready results, not raw command logs. +- Include the exact command you used when reproducibility matters. +- If no issues match the selected `--min-level`, state that explicitly. diff --git a/.agents/skills/codacy-issues-fetcher/agents/openai.yaml b/.agents/skills/codacy-issues-fetcher/agents/openai.yaml new file mode 100644 index 0000000..703b240 --- /dev/null +++ b/.agents/skills/codacy-issues-fetcher/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Codacy Issues Fetcher" + short_description: "Run codacy_issues.py to fetch and triage Codacy findings" + default_prompt: "Use $codacy-issues-fetcher to collect Codacy issues for this repository or PR and summarize the highest-priority fixes." From 54a18dfcced4ae1fc94d13963dd52ce1a4e9469a Mon Sep 17 00:00:00 2001 From: harumiWeb Date: Mon, 16 Feb 2026 23:15:58 +0900 Subject: [PATCH 21/25] fix: address PR57 review comments --- docs/README.ja.md | 4 + docs/mcp.md | 12 +++ src/exstruct/mcp/patch_runner.py | 78 +++++++++++++++-- tests/com/test_charts_extraction.py | 3 +- tests/com/test_shapes_extraction.py | 4 +- tests/conftest.py | 17 +++- tests/engine/test_engine_alpha_col.py | 10 ++- tests/mcp/test_extract_alpha_col.py | 6 ++ tests/mcp/test_patch_runner.py | 117 ++++++++++++++++++++++++++ 9 files changed, 237 insertions(+), 14 deletions(-) diff --git a/docs/README.ja.md b/docs/README.ja.md index cc11956..328597e 100644 --- a/docs/README.ja.md +++ b/docs/README.ja.md @@ -72,8 +72,12 @@ exstruct-mcp --root C:\data --log-file C:\logs\exstruct-mcp.log --on-conflict re - `exstruct_extract` - `exstruct_patch` - `exstruct_read_json_chunk` +- `exstruct_read_range` +- `exstruct_read_cells` +- `exstruct_read_formulas` - `exstruct_validate_input` +- `exstruct_read_range` / `exstruct_read_cells` / `exstruct_read_formulas` は v0.4.4 で追加され、MCPサーバー実装とテストに登録済みです。 - MCPでは `exstruct_extract` の `options.alpha_col=true` が既定です(列キーは `A`, `B`, ...)。従来の0始まり数値キーが必要な場合は `options.alpha_col=false` を指定してください。 注意点: diff --git a/docs/mcp.md b/docs/mcp.md index 3786df4..45070e4 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -64,7 +64,13 @@ Example sequence: ```json { "tool": "exstruct_validate_input", "xlsx_path": "C:\\data\\book.xlsx" } +``` + +```json { "tool": "exstruct_extract", "xlsx_path": "C:\\data\\book.xlsx", "mode": "standard", "format": "json" } +``` + +```json { "tool": "exstruct_read_json_chunk", "out_path": "C:\\data\\book.json", "sheet": "Sheet1", "max_bytes": 50000 } ``` @@ -97,12 +103,18 @@ Examples: "sheet": "Data", "range": "A1:G10" } +``` + +```json { "tool": "exstruct_read_cells", "out_path": "C:\\data\\book.json", "sheet": "Data", "addresses": ["J98", "J124"] } +``` + +```json { "tool": "exstruct_read_formulas", "out_path": "C:\\data\\book.json", diff --git a/src/exstruct/mcp/patch_runner.py b/src/exstruct/mcp/patch_runner.py index 8baaf0a..3472e99 100644 --- a/src/exstruct/mcp/patch_runner.py +++ b/src/exstruct/mcp/patch_runner.py @@ -238,8 +238,16 @@ def _validate_add_sheet(op: PatchOp) -> None: """Validate add_sheet operation.""" if op.cell is not None: raise ValueError("add_sheet does not accept cell.") + if op.range is not None: + raise ValueError("add_sheet does not accept range.") + if op.base_cell is not None: + raise ValueError("add_sheet does not accept base_cell.") + if op.expected is not None: + raise ValueError("add_sheet does not accept expected.") if op.value is not None: raise ValueError("add_sheet does not accept value.") + if op.values is not None: + raise ValueError("add_sheet does not accept values.") if op.formula is not None: raise ValueError("add_sheet does not accept formula.") @@ -252,12 +260,28 @@ def _validate_cell_required(op: PatchOp) -> None: def _validate_set_value(op: PatchOp) -> None: """Validate set_value operation.""" + if op.range is not None: + raise ValueError("set_value does not accept range.") + if op.base_cell is not None: + raise ValueError("set_value does not accept base_cell.") + if op.expected is not None: + raise ValueError("set_value does not accept expected.") + if op.values is not None: + raise ValueError("set_value does not accept values.") if op.formula is not None: raise ValueError("set_value does not accept formula.") def _validate_set_formula(op: PatchOp) -> None: """Validate set_formula operation.""" + if op.range is not None: + raise ValueError("set_formula does not accept range.") + if op.base_cell is not None: + raise ValueError("set_formula does not accept base_cell.") + if op.expected is not None: + raise ValueError("set_formula does not accept expected.") + if op.values is not None: + raise ValueError("set_formula does not accept values.") if op.value is not None: raise ValueError("set_formula does not accept value.") if op.formula is None: @@ -432,7 +456,7 @@ def run_patch( warnings: list[str] = [] if warning: warnings.append(warning) - if skipped: + if skipped and not request.dry_run: return PatchResult( out_path=str(output_path), patch_diff=[], @@ -440,6 +464,10 @@ def run_patch( formula_issues=[], warnings=warnings, ) + if skipped and request.dry_run: + warnings.append( + "Dry-run mode ignores on_conflict=skip and simulates patch without writing." + ) com = get_com_availability() if resolved_input.suffix.lower() == ".xls" and not com.available: @@ -529,7 +557,10 @@ def _apply_with_openpyxl( except Exception as exc: raise RuntimeError(f"openpyxl patch failed: {exc}") from exc - warnings.append("openpyxl editing may drop shapes/charts or unsupported elements.") + if not request.dry_run: + warnings.append( + "openpyxl editing may drop shapes/charts or unsupported elements." + ) _append_skip_warnings(warnings, diff) if ( not request.dry_run @@ -537,9 +568,10 @@ def _apply_with_openpyxl( and any(issue.level == "error" for issue in formula_issues) ): issue = formula_issues[0] + op_index, op_name = _find_preflight_issue_origin(issue, request.ops) error = PatchErrorDetail( - op_index=0, - op=request.ops[0].op if request.ops else "set_value", + op_index=op_index, + op=op_name, sheet=issue.sheet, cell=issue.cell, message=f"Formula health check failed: {issue.message}", @@ -571,6 +603,30 @@ def _append_skip_warnings(warnings: list[str], diff: list[PatchDiffItem]) -> Non ) +def _find_preflight_issue_origin( + issue: FormulaIssue, ops: list[PatchOp] +) -> tuple[int, PatchOpType]: + """Find the most likely op index/op name for a preflight formula issue.""" + for index, op in enumerate(ops): + if _op_targets_issue_cell(op, issue.sheet, issue.cell): + return index, op.op + return -1, "set_value" + + +def _op_targets_issue_cell(op: PatchOp, sheet: str, cell: str) -> bool: + """Return True when an op can affect the specified sheet/cell.""" + if op.sheet != sheet: + return False + if op.cell is not None: + return op.cell == cell + if op.range is None: + return False + for row in _expand_range_coordinates(op.range): + if cell in row: + return True + return False + + def _maybe_fallback_openpyxl( request: PatchRequest, input_path: Path, @@ -701,7 +757,10 @@ def _apply_ops_openpyxl( if input_path.suffix.lower() == ".xls": raise ValueError("openpyxl cannot edit .xls files.") - workbook = load_workbook(input_path) + if input_path.suffix.lower() == ".xlsm": + workbook = load_workbook(input_path, keep_vba=True) + else: + workbook = load_workbook(input_path) try: diff, inverse_ops = _apply_ops_to_openpyxl_workbook( workbook, @@ -1141,6 +1200,10 @@ def _apply_xlwings_op( auto_formula: bool, ) -> PatchDiffItem: """Apply a single op to an xlwings workbook.""" + # Extended ops are routed to openpyxl by _requires_openpyxl_backend. + # Keep this explicit guard to prevent accidental path regressions. + if op.op not in {"add_sheet", "set_value", "set_formula"}: + raise ValueError(f"Unsupported op: {op.op}") if op.op == "add_sheet": if op.sheet in sheets: raise ValueError(f"Sheet already exists: {op.sheet}") @@ -1226,7 +1289,10 @@ def _xlwings_workbook(file_path: Path) -> Iterator[XlwingsWorkbookProtocol]: try: app.quit() except Exception: - pass + try: + app.kill() + except Exception: + pass class PatchOpError(ValueError): diff --git a/tests/com/test_charts_extraction.py b/tests/com/test_charts_extraction.py index b89486b..88f53f3 100644 --- a/tests/com/test_charts_extraction.py +++ b/tests/com/test_charts_extraction.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections.abc import Iterator from contextlib import contextmanager from pathlib import Path @@ -12,7 +13,7 @@ @contextmanager -def _excel_app() -> xw.App: +def _excel_app() -> Iterator[xw.App]: app = xw.App(add_book=False, visible=False) try: yield app diff --git a/tests/com/test_shapes_extraction.py b/tests/com/test_shapes_extraction.py index 5ecd3db..1f0b78d 100644 --- a/tests/com/test_shapes_extraction.py +++ b/tests/com/test_shapes_extraction.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections.abc import Iterator from contextlib import contextmanager from pathlib import Path @@ -13,7 +14,7 @@ @contextmanager -def _excel_app() -> xw.App: +def _excel_app() -> Iterator[xw.App]: app = xw.App(add_book=False, visible=False) try: yield app @@ -127,6 +128,7 @@ def test_line_direction(tmp_path: Path) -> None: def test_connector_connections(tmp_path: Path) -> None: + """Connectorの接続先IDが抽出結果のshape IDに整合することを確認する。""" path = tmp_path / "connectors.xlsx" _make_workbook_with_shapes(path) diff --git a/tests/conftest.py b/tests/conftest.py index 29976fa..e48c556 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ from functools import lru_cache import importlib.util import os +import re import sys import pytest @@ -13,10 +14,22 @@ RUN_RENDER_SMOKE = os.getenv("RUN_RENDER_SMOKE") == "1" +def _markexpr_requests_com(markexpr: str) -> bool: + """Return True when markexpr explicitly requests the ``com`` marker.""" + tokens = re.findall(r"[A-Za-z_][A-Za-z0-9_]*", markexpr.lower()) + for index, token in enumerate(tokens): + if token != "com": + continue + prev = tokens[index - 1] if index > 0 else "" + if prev != "not": + return True + return False + + def pytest_configure(config: pytest.Config) -> None: """Register custom markers to avoid pytest warnings.""" markexpr = getattr(config.option, "markexpr", "") or "" - if "com" in markexpr: + if _markexpr_requests_com(markexpr): os.environ.pop("SKIP_COM_TESTS", None) config.addinivalue_line("markers", "com: requires Excel COM (Windows + Excel).") config.addinivalue_line( @@ -105,7 +118,7 @@ def _skip_com_for_non_com_tests( """Disable COM usage for tests that are not marked as COM/render.""" node_path = str(request.node.path) markexpr = getattr(request.config.option, "markexpr", "") or "" - if "com" in markexpr: + if _markexpr_requests_com(markexpr): monkeypatch.delenv("SKIP_COM_TESTS", raising=False) return if "tests\\com\\" in node_path or "tests/com/" in node_path: diff --git a/tests/engine/test_engine_alpha_col.py b/tests/engine/test_engine_alpha_col.py index f2f1b49..46b2126 100644 --- a/tests/engine/test_engine_alpha_col.py +++ b/tests/engine/test_engine_alpha_col.py @@ -5,7 +5,7 @@ import json from pathlib import Path -from _pytest.monkeypatch import MonkeyPatch +import pytest from exstruct.engine import ExStructEngine, StructOptions from exstruct.models import CellRow, MergedCells, SheetData, WorkbookData @@ -34,7 +34,9 @@ def _fake_workbook(path: Path, **kwargs: object) -> WorkbookData: ) -def test_engine_alpha_col_false(monkeypatch: MonkeyPatch, tmp_path: Path) -> None: +def test_engine_alpha_col_false( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: """With alpha_col=False, column keys remain numeric.""" monkeypatch.setattr("exstruct.engine.extract_workbook", _fake_workbook) engine = ExStructEngine(options=StructOptions(mode="light", alpha_col=False)) @@ -45,7 +47,7 @@ def test_engine_alpha_col_false(monkeypatch: MonkeyPatch, tmp_path: Path) -> Non assert result.sheets["Sheet1"].merged_ranges == [] -def test_engine_alpha_col_true(monkeypatch: MonkeyPatch, tmp_path: Path) -> None: +def test_engine_alpha_col_true(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: """With alpha_col=True, column keys are converted to ABC-style.""" monkeypatch.setattr("exstruct.engine.extract_workbook", _fake_workbook) engine = ExStructEngine(options=StructOptions(mode="light", alpha_col=True)) @@ -57,7 +59,7 @@ def test_engine_alpha_col_true(monkeypatch: MonkeyPatch, tmp_path: Path) -> None def test_engine_serialize_alpha_col_includes_merged_ranges( - monkeypatch: MonkeyPatch, tmp_path: Path + monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: """Serialized output should include merged_ranges when alpha_col=True.""" monkeypatch.setattr("exstruct.engine.extract_workbook", _fake_workbook) diff --git a/tests/mcp/test_extract_alpha_col.py b/tests/mcp/test_extract_alpha_col.py index c02516a..45916c4 100644 --- a/tests/mcp/test_extract_alpha_col.py +++ b/tests/mcp/test_extract_alpha_col.py @@ -17,6 +17,12 @@ def test_extract_options_alpha_col_true() -> None: assert opts.alpha_col is True +def test_extract_options_alpha_col_false() -> None: + """alpha_col should be settable to False to opt out.""" + opts = ExtractOptions(alpha_col=False) + assert opts.alpha_col is False + + def test_extract_options_from_dict() -> None: """alpha_col should be parseable from a plain dict (MCP JSON payload).""" opts = ExtractOptions.model_validate({"alpha_col": True, "pretty": True}) diff --git a/tests/mcp/test_patch_runner.py b/tests/mcp/test_patch_runner.py index 57de2c6..860ec8c 100644 --- a/tests/mcp/test_patch_runner.py +++ b/tests/mcp/test_patch_runner.py @@ -179,6 +179,29 @@ def test_run_patch_conflict_skip( assert any("skipping" in warning for warning in result.warnings) +def test_run_patch_conflict_skip_dry_run_still_simulates( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _disable_com(monkeypatch) + input_path = tmp_path / "book.xlsx" + _create_workbook(input_path) + default_out = tmp_path / "book_patched.xlsx" + default_out.write_text("dummy", encoding="utf-8") + ops = [PatchOp(op="set_value", sheet="Sheet1", cell="A1", value="x")] + request = PatchRequest( + xlsx_path=input_path, + ops=ops, + on_conflict="skip", + dry_run=True, + ) + result = run_patch(request, policy=PathPolicy(root=tmp_path)) + assert result.error is None + assert len(result.patch_diff) == 1 + assert any("ignores on_conflict=skip" in warning for warning in result.warnings) + assert not any("may drop shapes/charts" in warning for warning in result.warnings) + assert default_out.read_text(encoding="utf-8") == "dummy" + + def test_run_patch_conflict_overwrite( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -245,6 +268,68 @@ def test_run_patch_xls_requires_com( run_patch(request, policy=PathPolicy(root=tmp_path)) +def test_run_patch_xlsm_openpyxl_uses_keep_vba( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _disable_com(monkeypatch) + input_path = tmp_path / "book.xlsm" + input_path.write_bytes(b"dummy") + calls: dict[str, object] = {} + + class FakeCell: + def __init__(self, value: str | int | float | None) -> None: + self.value = value + self.data_type: str | None = None + + class FakeSheet: + def __init__(self) -> None: + self._cells: dict[str, FakeCell] = {"A1": FakeCell("old")} + + def __getitem__(self, key: str) -> FakeCell: + if key not in self._cells: + self._cells[key] = FakeCell(None) + return self._cells[key] + + class FakeWorkbook: + def __init__(self) -> None: + self._sheets: dict[str, FakeSheet] = {"Sheet1": FakeSheet()} + self.sheetnames = ["Sheet1"] + + def __getitem__(self, key: str) -> FakeSheet: + return self._sheets[key] + + def create_sheet(self, title: str) -> FakeSheet: + sheet = FakeSheet() + self._sheets[title] = sheet + self.sheetnames.append(title) + return sheet + + def save(self, filename: str | Path) -> None: + calls["saved"] = str(filename) + + def close(self) -> None: + calls["closed"] = True + + fake_workbook = FakeWorkbook() + + def _fake_load_workbook(path: Path, **kwargs: object) -> FakeWorkbook: + calls["path"] = str(path) + calls["keep_vba"] = kwargs.get("keep_vba", False) + return fake_workbook + + monkeypatch.setattr("openpyxl.load_workbook", _fake_load_workbook) + + request = PatchRequest( + xlsx_path=input_path, + ops=[PatchOp(op="set_value", sheet="Sheet1", cell="A1", value="new")], + on_conflict="rename", + ) + result = run_patch(request, policy=PathPolicy(root=tmp_path)) + assert result.error is None + assert calls["keep_vba"] is True + assert calls["closed"] is True + + def test_run_patch_dry_run_does_not_write( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -413,3 +498,35 @@ def test_run_patch_formula_health_check( assert result.error is not None assert result.formula_issues assert result.formula_issues[0].code == "ref_error" + + +def test_run_patch_formula_health_check_reports_matching_op( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _disable_com(monkeypatch) + input_path = tmp_path / "book.xlsx" + _create_workbook(input_path) + ops = [ + PatchOp(op="set_formula", sheet="Sheet1", cell="B1", formula="=SUM(1,1)"), + PatchOp(op="set_formula", sheet="Sheet1", cell="A1", formula="=#REF!+1"), + ] + request = PatchRequest( + xlsx_path=input_path, + ops=ops, + on_conflict="rename", + preflight_formula_check=True, + ) + result = run_patch(request, policy=PathPolicy(root=tmp_path)) + assert result.error is not None + assert result.error.op_index == 1 + assert result.error.op == "set_formula" + + +def test_patch_op_add_sheet_rejects_unrelated_fields() -> None: + with pytest.raises(ValidationError, match="add_sheet does not accept range"): + PatchOp(op="add_sheet", sheet="NewSheet", range="A1:A1") + + +def test_patch_op_set_value_rejects_expected() -> None: + with pytest.raises(ValidationError, match="set_value does not accept expected"): + PatchOp(op="set_value", sheet="Sheet1", cell="A1", value="x", expected="old") From 9c76edefdd4805cde4f970138ffbbf910de77bf2 Mon Sep 17 00:00:00 2001 From: harumiWeb Date: Tue, 17 Feb 2026 19:40:12 +0900 Subject: [PATCH 22/25] fix: regenerate uv.lock after merging main --- uv.lock | 1418 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 736 insertions(+), 682 deletions(-) diff --git a/uv.lock b/uv.lock index d483e72..02a67b9 100644 --- a/uv.lock +++ b/uv.lock @@ -2,8 +2,12 @@ version = 1 revision = 3 requires-python = ">=3.11" resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version < '3.12'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] [manifest] @@ -12,6 +16,15 @@ members = [ "exstruct", ] +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -39,7 +52,7 @@ name = "appscript" version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "lxml" }, + { name = "lxml", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ca/52/2fa70edfd98f0058219ecc2e365a3ba7aabd42db14ff9d7f44bbdcc5400d/appscript-1.4.0.tar.gz", hash = "sha256:b2c6fc770bf822ea45529c7084bc0ee340e67ab260016b01d28e0449ec8723be", size = 295279, upload-time = "2025-10-08T07:56:39.126Z" } wheels = [ @@ -64,25 +77,25 @@ wheels = [ [[package]] name = "babel" -version = "2.17.0" +version = "2.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, ] [[package]] name = "backrefs" -version = "6.1" +version = "6.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/86/e3/bb3a439d5cb255c4774724810ad8073830fac9c9dee123555820c1bcc806/backrefs-6.1.tar.gz", hash = "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231", size = 7011962, upload-time = "2025-11-15T14:52:08.323Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/a6/e325ec73b638d3ede4421b5445d4a0b8b219481826cc079d510100af356c/backrefs-6.2.tar.gz", hash = "sha256:f44ff4d48808b243b6c0cdc6231e22195c32f77046018141556c66f8bab72a49", size = 7012303, upload-time = "2026-02-16T19:10:15.828Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/ee/c216d52f58ea75b5e1841022bbae24438b19834a29b163cb32aa3a2a7c6e/backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1", size = 381059, upload-time = "2025-11-15T14:51:59.758Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9a/8da246d988ded941da96c7ed945d63e94a445637eaad985a0ed88787cb89/backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7", size = 392854, upload-time = "2025-11-15T14:52:01.194Z" }, - { url = "https://files.pythonhosted.org/packages/37/c9/fd117a6f9300c62bbc33bc337fd2b3c6bfe28b6e9701de336b52d7a797ad/backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a", size = 398770, upload-time = "2025-11-15T14:52:02.584Z" }, - { url = "https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl", hash = "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05", size = 400726, upload-time = "2025-11-15T14:52:04.093Z" }, - { url = "https://files.pythonhosted.org/packages/1d/72/6296bad135bfafd3254ae3648cd152980a424bd6fed64a101af00cc7ba31/backrefs-6.1-py314-none-any.whl", hash = "sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853", size = 412584, upload-time = "2025-11-15T14:52:05.233Z" }, - { url = "https://files.pythonhosted.org/packages/02/e3/a4fa1946722c4c7b063cc25043a12d9ce9b4323777f89643be74cef2993c/backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0", size = 381058, upload-time = "2025-11-15T14:52:06.698Z" }, + { url = "https://files.pythonhosted.org/packages/1b/39/3765df263e08a4df37f4f43cb5aa3c6c17a4bdd42ecfe841e04c26037171/backrefs-6.2-py310-none-any.whl", hash = "sha256:0fdc7b012420b6b144410342caeb8adc54c6866cf12064abc9bb211302e496f8", size = 381075, upload-time = "2026-02-16T19:10:04.322Z" }, + { url = "https://files.pythonhosted.org/packages/0f/f0/35240571e1b67ffb19dafb29ab34150b6f59f93f717b041082cdb1bfceb1/backrefs-6.2-py311-none-any.whl", hash = "sha256:08aa7fae530c6b2361d7bdcbda1a7c454e330cc9dbcd03f5c23205e430e5c3be", size = 392874, upload-time = "2026-02-16T19:10:06.314Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/77e8c9745b4d227cce9f5e0a6f68041278c5f9b18588b35905f5f19c1beb/backrefs-6.2-py312-none-any.whl", hash = "sha256:c3f4b9cb2af8cda0d87ab4f57800b57b95428488477be164dd2b47be54db0c90", size = 398787, upload-time = "2026-02-16T19:10:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/c5/71/c754b1737ad99102e03fa3235acb6cb6d3ac9d6f596cbc3e5f236705abd8/backrefs-6.2-py313-none-any.whl", hash = "sha256:12df81596ab511f783b7d87c043ce26bc5b0288cf3bb03610fe76b8189282b2b", size = 400747, upload-time = "2026-02-16T19:10:09.791Z" }, + { url = "https://files.pythonhosted.org/packages/af/75/be12ba31a6eb20dccef2320cd8ccb3f7d9013b68ba4c70156259fee9e409/backrefs-6.2-py314-none-any.whl", hash = "sha256:e5f805ae09819caa1aa0623b4a83790e7028604aa2b8c73ba602c4454e665de7", size = 412602, upload-time = "2026-02-16T19:10:12.317Z" }, + { url = "https://files.pythonhosted.org/packages/21/f8/d02f650c47d05034dcd6f9c8cf94f39598b7a89c00ecda0ecb2911bc27e9/backrefs-6.2-py39-none-any.whl", hash = "sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7", size = 381077, upload-time = "2026-02-16T19:10:13.74Z" }, ] [[package]] @@ -149,11 +162,11 @@ dev = [ [[package]] name = "certifi" -version = "2025.11.12" +version = "2026.1.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, ] [[package]] @@ -430,89 +443,101 @@ wheels = [ [[package]] name = "coverage" -version = "7.12.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/89/26/4a96807b193b011588099c3b5c89fbb05294e5b90e71018e065465f34eb6/coverage-7.12.0.tar.gz", hash = "sha256:fc11e0a4e372cb5f282f16ef90d4a585034050ccda536451901abfb19a57f40c", size = 819341, upload-time = "2025-11-18T13:34:20.766Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/0c/0dfe7f0487477d96432e4815537263363fb6dd7289743a796e8e51eabdf2/coverage-7.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa124a3683d2af98bd9d9c2bfa7a5076ca7e5ab09fdb96b81fa7d89376ae928f", size = 217535, upload-time = "2025-11-18T13:32:08.812Z" }, - { url = "https://files.pythonhosted.org/packages/9b/f5/f9a4a053a5bbff023d3bec259faac8f11a1e5a6479c2ccf586f910d8dac7/coverage-7.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d93fbf446c31c0140208dcd07c5d882029832e8ed7891a39d6d44bd65f2316c3", size = 218044, upload-time = "2025-11-18T13:32:10.329Z" }, - { url = "https://files.pythonhosted.org/packages/95/c5/84fc3697c1fa10cd8571919bf9693f693b7373278daaf3b73e328d502bc8/coverage-7.12.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:52ca620260bd8cd6027317bdd8b8ba929be1d741764ee765b42c4d79a408601e", size = 248440, upload-time = "2025-11-18T13:32:12.536Z" }, - { url = "https://files.pythonhosted.org/packages/f4/36/2d93fbf6a04670f3874aed397d5a5371948a076e3249244a9e84fb0e02d6/coverage-7.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f3433ffd541380f3a0e423cff0f4926d55b0cc8c1d160fdc3be24a4c03aa65f7", size = 250361, upload-time = "2025-11-18T13:32:13.852Z" }, - { url = "https://files.pythonhosted.org/packages/5d/49/66dc65cc456a6bfc41ea3d0758c4afeaa4068a2b2931bf83be6894cf1058/coverage-7.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7bbb321d4adc9f65e402c677cd1c8e4c2d0105d3ce285b51b4d87f1d5db5245", size = 252472, upload-time = "2025-11-18T13:32:15.068Z" }, - { url = "https://files.pythonhosted.org/packages/35/1f/ebb8a18dffd406db9fcd4b3ae42254aedcaf612470e8712f12041325930f/coverage-7.12.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22a7aade354a72dff3b59c577bfd18d6945c61f97393bc5fb7bd293a4237024b", size = 248592, upload-time = "2025-11-18T13:32:16.328Z" }, - { url = "https://files.pythonhosted.org/packages/da/a8/67f213c06e5ea3b3d4980df7dc344d7fea88240b5fe878a5dcbdfe0e2315/coverage-7.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3ff651dcd36d2fea66877cd4a82de478004c59b849945446acb5baf9379a1b64", size = 250167, upload-time = "2025-11-18T13:32:17.687Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/e52aef68154164ea40cc8389c120c314c747fe63a04b013a5782e989b77f/coverage-7.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:31b8b2e38391a56e3cea39d22a23faaa7c3fc911751756ef6d2621d2a9daf742", size = 248238, upload-time = "2025-11-18T13:32:19.2Z" }, - { url = "https://files.pythonhosted.org/packages/1f/a4/4d88750bcf9d6d66f77865e5a05a20e14db44074c25fd22519777cb69025/coverage-7.12.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:297bc2da28440f5ae51c845a47c8175a4db0553a53827886e4fb25c66633000c", size = 247964, upload-time = "2025-11-18T13:32:21.027Z" }, - { url = "https://files.pythonhosted.org/packages/a7/6b/b74693158899d5b47b0bf6238d2c6722e20ba749f86b74454fac0696bb00/coverage-7.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ff7651cc01a246908eac162a6a86fc0dbab6de1ad165dfb9a1e2ec660b44984", size = 248862, upload-time = "2025-11-18T13:32:22.304Z" }, - { url = "https://files.pythonhosted.org/packages/18/de/6af6730227ce0e8ade307b1cc4a08e7f51b419a78d02083a86c04ccceb29/coverage-7.12.0-cp311-cp311-win32.whl", hash = "sha256:313672140638b6ddb2c6455ddeda41c6a0b208298034544cfca138978c6baed6", size = 220033, upload-time = "2025-11-18T13:32:23.714Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a1/e7f63021a7c4fe20994359fcdeae43cbef4a4d0ca36a5a1639feeea5d9e1/coverage-7.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1783ed5bd0d5938d4435014626568dc7f93e3cb99bc59188cc18857c47aa3c4", size = 220966, upload-time = "2025-11-18T13:32:25.599Z" }, - { url = "https://files.pythonhosted.org/packages/77/e8/deae26453f37c20c3aa0c4433a1e32cdc169bf415cce223a693117aa3ddd/coverage-7.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:4648158fd8dd9381b5847622df1c90ff314efbfc1df4550092ab6013c238a5fc", size = 219637, upload-time = "2025-11-18T13:32:27.265Z" }, - { url = "https://files.pythonhosted.org/packages/02/bf/638c0427c0f0d47638242e2438127f3c8ee3cfc06c7fdeb16778ed47f836/coverage-7.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:29644c928772c78512b48e14156b81255000dcfd4817574ff69def189bcb3647", size = 217704, upload-time = "2025-11-18T13:32:28.906Z" }, - { url = "https://files.pythonhosted.org/packages/08/e1/706fae6692a66c2d6b871a608bbde0da6281903fa0e9f53a39ed441da36a/coverage-7.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8638cbb002eaa5d7c8d04da667813ce1067080b9a91099801a0053086e52b736", size = 218064, upload-time = "2025-11-18T13:32:30.161Z" }, - { url = "https://files.pythonhosted.org/packages/a9/8b/eb0231d0540f8af3ffda39720ff43cb91926489d01524e68f60e961366e4/coverage-7.12.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:083631eeff5eb9992c923e14b810a179798bb598e6a0dd60586819fc23be6e60", size = 249560, upload-time = "2025-11-18T13:32:31.835Z" }, - { url = "https://files.pythonhosted.org/packages/e9/a1/67fb52af642e974d159b5b379e4d4c59d0ebe1288677fbd04bbffe665a82/coverage-7.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99d5415c73ca12d558e07776bd957c4222c687b9f1d26fa0e1b57e3598bdcde8", size = 252318, upload-time = "2025-11-18T13:32:33.178Z" }, - { url = "https://files.pythonhosted.org/packages/41/e5/38228f31b2c7665ebf9bdfdddd7a184d56450755c7e43ac721c11a4b8dab/coverage-7.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e949ebf60c717c3df63adb4a1a366c096c8d7fd8472608cd09359e1bd48ef59f", size = 253403, upload-time = "2025-11-18T13:32:34.45Z" }, - { url = "https://files.pythonhosted.org/packages/ec/4b/df78e4c8188f9960684267c5a4897836f3f0f20a20c51606ee778a1d9749/coverage-7.12.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d907ddccbca819afa2cd014bc69983b146cca2735a0b1e6259b2a6c10be1e70", size = 249984, upload-time = "2025-11-18T13:32:35.747Z" }, - { url = "https://files.pythonhosted.org/packages/ba/51/bb163933d195a345c6f63eab9e55743413d064c291b6220df754075c2769/coverage-7.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1518ecbad4e6173f4c6e6c4a46e49555ea5679bf3feda5edb1b935c7c44e8a0", size = 251339, upload-time = "2025-11-18T13:32:37.352Z" }, - { url = "https://files.pythonhosted.org/packages/15/40/c9b29cdb8412c837cdcbc2cfa054547dd83affe6cbbd4ce4fdb92b6ba7d1/coverage-7.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51777647a749abdf6f6fd8c7cffab12de68ab93aab15efc72fbbb83036c2a068", size = 249489, upload-time = "2025-11-18T13:32:39.212Z" }, - { url = "https://files.pythonhosted.org/packages/c8/da/b3131e20ba07a0de4437a50ef3b47840dfabf9293675b0cd5c2c7f66dd61/coverage-7.12.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:42435d46d6461a3b305cdfcad7cdd3248787771f53fe18305548cba474e6523b", size = 249070, upload-time = "2025-11-18T13:32:40.598Z" }, - { url = "https://files.pythonhosted.org/packages/70/81/b653329b5f6302c08d683ceff6785bc60a34be9ae92a5c7b63ee7ee7acec/coverage-7.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5bcead88c8423e1855e64b8057d0544e33e4080b95b240c2a355334bb7ced937", size = 250929, upload-time = "2025-11-18T13:32:42.915Z" }, - { url = "https://files.pythonhosted.org/packages/a3/00/250ac3bca9f252a5fb1338b5ad01331ebb7b40223f72bef5b1b2cb03aa64/coverage-7.12.0-cp312-cp312-win32.whl", hash = "sha256:dcbb630ab034e86d2a0f79aefd2be07e583202f41e037602d438c80044957baa", size = 220241, upload-time = "2025-11-18T13:32:44.665Z" }, - { url = "https://files.pythonhosted.org/packages/64/1c/77e79e76d37ce83302f6c21980b45e09f8aa4551965213a10e62d71ce0ab/coverage-7.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:2fd8354ed5d69775ac42986a691fbf68b4084278710cee9d7c3eaa0c28fa982a", size = 221051, upload-time = "2025-11-18T13:32:46.008Z" }, - { url = "https://files.pythonhosted.org/packages/31/f5/641b8a25baae564f9e52cac0e2667b123de961985709a004e287ee7663cc/coverage-7.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:737c3814903be30695b2de20d22bcc5428fdae305c61ba44cdc8b3252984c49c", size = 219692, upload-time = "2025-11-18T13:32:47.372Z" }, - { url = "https://files.pythonhosted.org/packages/b8/14/771700b4048774e48d2c54ed0c674273702713c9ee7acdfede40c2666747/coverage-7.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47324fffca8d8eae7e185b5bb20c14645f23350f870c1649003618ea91a78941", size = 217725, upload-time = "2025-11-18T13:32:49.22Z" }, - { url = "https://files.pythonhosted.org/packages/17/a7/3aa4144d3bcb719bf67b22d2d51c2d577bf801498c13cb08f64173e80497/coverage-7.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ccf3b2ede91decd2fb53ec73c1f949c3e034129d1e0b07798ff1d02ea0c8fa4a", size = 218098, upload-time = "2025-11-18T13:32:50.78Z" }, - { url = "https://files.pythonhosted.org/packages/fc/9c/b846bbc774ff81091a12a10203e70562c91ae71badda00c5ae5b613527b1/coverage-7.12.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b365adc70a6936c6b0582dc38746b33b2454148c02349345412c6e743efb646d", size = 249093, upload-time = "2025-11-18T13:32:52.554Z" }, - { url = "https://files.pythonhosted.org/packages/76/b6/67d7c0e1f400b32c883e9342de4a8c2ae7c1a0b57c5de87622b7262e2309/coverage-7.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc13baf85cd8a4cfcf4a35c7bc9d795837ad809775f782f697bf630b7e200211", size = 251686, upload-time = "2025-11-18T13:32:54.862Z" }, - { url = "https://files.pythonhosted.org/packages/cc/75/b095bd4b39d49c3be4bffbb3135fea18a99a431c52dd7513637c0762fecb/coverage-7.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:099d11698385d572ceafb3288a5b80fe1fc58bf665b3f9d362389de488361d3d", size = 252930, upload-time = "2025-11-18T13:32:56.417Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f3/466f63015c7c80550bead3093aacabf5380c1220a2a93c35d374cae8f762/coverage-7.12.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:473dc45d69694069adb7680c405fb1e81f60b2aff42c81e2f2c3feaf544d878c", size = 249296, upload-time = "2025-11-18T13:32:58.074Z" }, - { url = "https://files.pythonhosted.org/packages/27/86/eba2209bf2b7e28c68698fc13437519a295b2d228ba9e0ec91673e09fa92/coverage-7.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:583f9adbefd278e9de33c33d6846aa8f5d164fa49b47144180a0e037f0688bb9", size = 251068, upload-time = "2025-11-18T13:32:59.646Z" }, - { url = "https://files.pythonhosted.org/packages/ec/55/ca8ae7dbba962a3351f18940b359b94c6bafdd7757945fdc79ec9e452dc7/coverage-7.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2089cc445f2dc0af6f801f0d1355c025b76c24481935303cf1af28f636688f0", size = 249034, upload-time = "2025-11-18T13:33:01.481Z" }, - { url = "https://files.pythonhosted.org/packages/7a/d7/39136149325cad92d420b023b5fd900dabdd1c3a0d1d5f148ef4a8cedef5/coverage-7.12.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:950411f1eb5d579999c5f66c62a40961f126fc71e5e14419f004471957b51508", size = 248853, upload-time = "2025-11-18T13:33:02.935Z" }, - { url = "https://files.pythonhosted.org/packages/fe/b6/76e1add8b87ef60e00643b0b7f8f7bb73d4bf5249a3be19ebefc5793dd25/coverage-7.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b1aab7302a87bafebfe76b12af681b56ff446dc6f32ed178ff9c092ca776e6bc", size = 250619, upload-time = "2025-11-18T13:33:04.336Z" }, - { url = "https://files.pythonhosted.org/packages/95/87/924c6dc64f9203f7a3c1832a6a0eee5a8335dbe5f1bdadcc278d6f1b4d74/coverage-7.12.0-cp313-cp313-win32.whl", hash = "sha256:d7e0d0303c13b54db495eb636bc2465b2fb8475d4c8bcec8fe4b5ca454dfbae8", size = 220261, upload-time = "2025-11-18T13:33:06.493Z" }, - { url = "https://files.pythonhosted.org/packages/91/77/dd4aff9af16ff776bf355a24d87eeb48fc6acde54c907cc1ea89b14a8804/coverage-7.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:ce61969812d6a98a981d147d9ac583a36ac7db7766f2e64a9d4d059c2fe29d07", size = 221072, upload-time = "2025-11-18T13:33:07.926Z" }, - { url = "https://files.pythonhosted.org/packages/70/49/5c9dc46205fef31b1b226a6e16513193715290584317fd4df91cdaf28b22/coverage-7.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bcec6f47e4cb8a4c2dc91ce507f6eefc6a1b10f58df32cdc61dff65455031dfc", size = 219702, upload-time = "2025-11-18T13:33:09.631Z" }, - { url = "https://files.pythonhosted.org/packages/9b/62/f87922641c7198667994dd472a91e1d9b829c95d6c29529ceb52132436ad/coverage-7.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:459443346509476170d553035e4a3eed7b860f4fe5242f02de1010501956ce87", size = 218420, upload-time = "2025-11-18T13:33:11.153Z" }, - { url = "https://files.pythonhosted.org/packages/85/dd/1cc13b2395ef15dbb27d7370a2509b4aee77890a464fb35d72d428f84871/coverage-7.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04a79245ab2b7a61688958f7a855275997134bc84f4a03bc240cf64ff132abf6", size = 218773, upload-time = "2025-11-18T13:33:12.569Z" }, - { url = "https://files.pythonhosted.org/packages/74/40/35773cc4bb1e9d4658d4fb669eb4195b3151bef3bbd6f866aba5cd5dac82/coverage-7.12.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:09a86acaaa8455f13d6a99221d9654df249b33937b4e212b4e5a822065f12aa7", size = 260078, upload-time = "2025-11-18T13:33:14.037Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ee/231bb1a6ffc2905e396557585ebc6bdc559e7c66708376d245a1f1d330fc/coverage-7.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:907e0df1b71ba77463687a74149c6122c3f6aac56c2510a5d906b2f368208560", size = 262144, upload-time = "2025-11-18T13:33:15.601Z" }, - { url = "https://files.pythonhosted.org/packages/28/be/32f4aa9f3bf0b56f3971001b56508352c7753915345d45fab4296a986f01/coverage-7.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b57e2d0ddd5f0582bae5437c04ee71c46cd908e7bc5d4d0391f9a41e812dd12", size = 264574, upload-time = "2025-11-18T13:33:17.354Z" }, - { url = "https://files.pythonhosted.org/packages/68/7c/00489fcbc2245d13ab12189b977e0cf06ff3351cb98bc6beba8bd68c5902/coverage-7.12.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58c1c6aa677f3a1411fe6fb28ec3a942e4f665df036a3608816e0847fad23296", size = 259298, upload-time = "2025-11-18T13:33:18.958Z" }, - { url = "https://files.pythonhosted.org/packages/96/b4/f0760d65d56c3bea95b449e02570d4abd2549dc784bf39a2d4721a2d8ceb/coverage-7.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4c589361263ab2953e3c4cd2a94db94c4ad4a8e572776ecfbad2389c626e4507", size = 262150, upload-time = "2025-11-18T13:33:20.644Z" }, - { url = "https://files.pythonhosted.org/packages/c5/71/9a9314df00f9326d78c1e5a910f520d599205907432d90d1c1b7a97aa4b1/coverage-7.12.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:91b810a163ccad2e43b1faa11d70d3cf4b6f3d83f9fd5f2df82a32d47b648e0d", size = 259763, upload-time = "2025-11-18T13:33:22.189Z" }, - { url = "https://files.pythonhosted.org/packages/10/34/01a0aceed13fbdf925876b9a15d50862eb8845454301fe3cdd1df08b2182/coverage-7.12.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:40c867af715f22592e0d0fb533a33a71ec9e0f73a6945f722a0c85c8c1cbe3a2", size = 258653, upload-time = "2025-11-18T13:33:24.239Z" }, - { url = "https://files.pythonhosted.org/packages/8d/04/81d8fd64928acf1574bbb0181f66901c6c1c6279c8ccf5f84259d2c68ae9/coverage-7.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:68b0d0a2d84f333de875666259dadf28cc67858bc8fd8b3f1eae84d3c2bec455", size = 260856, upload-time = "2025-11-18T13:33:26.365Z" }, - { url = "https://files.pythonhosted.org/packages/f2/76/fa2a37bfaeaf1f766a2d2360a25a5297d4fb567098112f6517475eee120b/coverage-7.12.0-cp313-cp313t-win32.whl", hash = "sha256:73f9e7fbd51a221818fd11b7090eaa835a353ddd59c236c57b2199486b116c6d", size = 220936, upload-time = "2025-11-18T13:33:28.165Z" }, - { url = "https://files.pythonhosted.org/packages/f9/52/60f64d932d555102611c366afb0eb434b34266b1d9266fc2fe18ab641c47/coverage-7.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:24cff9d1f5743f67db7ba46ff284018a6e9aeb649b67aa1e70c396aa1b7cb23c", size = 222001, upload-time = "2025-11-18T13:33:29.656Z" }, - { url = "https://files.pythonhosted.org/packages/77/df/c303164154a5a3aea7472bf323b7c857fed93b26618ed9fc5c2955566bb0/coverage-7.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:c87395744f5c77c866d0f5a43d97cc39e17c7f1cb0115e54a2fe67ca75c5d14d", size = 220273, upload-time = "2025-11-18T13:33:31.415Z" }, - { url = "https://files.pythonhosted.org/packages/bf/2e/fc12db0883478d6e12bbd62d481210f0c8daf036102aa11434a0c5755825/coverage-7.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a1c59b7dc169809a88b21a936eccf71c3895a78f5592051b1af8f4d59c2b4f92", size = 217777, upload-time = "2025-11-18T13:33:32.86Z" }, - { url = "https://files.pythonhosted.org/packages/1f/c1/ce3e525d223350c6ec16b9be8a057623f54226ef7f4c2fee361ebb6a02b8/coverage-7.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8787b0f982e020adb732b9f051f3e49dd5054cebbc3f3432061278512a2b1360", size = 218100, upload-time = "2025-11-18T13:33:34.532Z" }, - { url = "https://files.pythonhosted.org/packages/15/87/113757441504aee3808cb422990ed7c8bcc2d53a6779c66c5adef0942939/coverage-7.12.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ea5a9f7dc8877455b13dd1effd3202e0bca72f6f3ab09f9036b1bcf728f69ac", size = 249151, upload-time = "2025-11-18T13:33:36.135Z" }, - { url = "https://files.pythonhosted.org/packages/d9/1d/9529d9bd44049b6b05bb319c03a3a7e4b0a8a802d28fa348ad407e10706d/coverage-7.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fdba9f15849534594f60b47c9a30bc70409b54947319a7c4fd0e8e3d8d2f355d", size = 251667, upload-time = "2025-11-18T13:33:37.996Z" }, - { url = "https://files.pythonhosted.org/packages/11/bb/567e751c41e9c03dc29d3ce74b8c89a1e3396313e34f255a2a2e8b9ebb56/coverage-7.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a00594770eb715854fb1c57e0dea08cce6720cfbc531accdb9850d7c7770396c", size = 253003, upload-time = "2025-11-18T13:33:39.553Z" }, - { url = "https://files.pythonhosted.org/packages/e4/b3/c2cce2d8526a02fb9e9ca14a263ca6fc074449b33a6afa4892838c903528/coverage-7.12.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5560c7e0d82b42eb1951e4f68f071f8017c824ebfd5a6ebe42c60ac16c6c2434", size = 249185, upload-time = "2025-11-18T13:33:42.086Z" }, - { url = "https://files.pythonhosted.org/packages/0e/a7/967f93bb66e82c9113c66a8d0b65ecf72fc865adfba5a145f50c7af7e58d/coverage-7.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2e26b481c9159c2773a37947a9718cfdc58893029cdfb177531793e375cfc", size = 251025, upload-time = "2025-11-18T13:33:43.634Z" }, - { url = "https://files.pythonhosted.org/packages/b9/b2/f2f6f56337bc1af465d5b2dc1ee7ee2141b8b9272f3bf6213fcbc309a836/coverage-7.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6e1a8c066dabcde56d5d9fed6a66bc19a2883a3fe051f0c397a41fc42aedd4cc", size = 248979, upload-time = "2025-11-18T13:33:46.04Z" }, - { url = "https://files.pythonhosted.org/packages/f4/7a/bf4209f45a4aec09d10a01a57313a46c0e0e8f4c55ff2965467d41a92036/coverage-7.12.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f7ba9da4726e446d8dd8aae5a6cd872511184a5d861de80a86ef970b5dacce3e", size = 248800, upload-time = "2025-11-18T13:33:47.546Z" }, - { url = "https://files.pythonhosted.org/packages/b8/b7/1e01b8696fb0521810f60c5bbebf699100d6754183e6cc0679bf2ed76531/coverage-7.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e0f483ab4f749039894abaf80c2f9e7ed77bbf3c737517fb88c8e8e305896a17", size = 250460, upload-time = "2025-11-18T13:33:49.537Z" }, - { url = "https://files.pythonhosted.org/packages/71/ae/84324fb9cb46c024760e706353d9b771a81b398d117d8c1fe010391c186f/coverage-7.12.0-cp314-cp314-win32.whl", hash = "sha256:76336c19a9ef4a94b2f8dc79f8ac2da3f193f625bb5d6f51a328cd19bfc19933", size = 220533, upload-time = "2025-11-18T13:33:51.16Z" }, - { url = "https://files.pythonhosted.org/packages/e2/71/1033629deb8460a8f97f83e6ac4ca3b93952e2b6f826056684df8275e015/coverage-7.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c1059b600aec6ef090721f8f633f60ed70afaffe8ecab85b59df748f24b31fe", size = 221348, upload-time = "2025-11-18T13:33:52.776Z" }, - { url = "https://files.pythonhosted.org/packages/0a/5f/ac8107a902f623b0c251abdb749be282dc2ab61854a8a4fcf49e276fce2f/coverage-7.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:172cf3a34bfef42611963e2b661302a8931f44df31629e5b1050567d6b90287d", size = 219922, upload-time = "2025-11-18T13:33:54.316Z" }, - { url = "https://files.pythonhosted.org/packages/79/6e/f27af2d4da367f16077d21ef6fe796c874408219fa6dd3f3efe7751bd910/coverage-7.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:aa7d48520a32cb21c7a9b31f81799e8eaec7239db36c3b670be0fa2403828d1d", size = 218511, upload-time = "2025-11-18T13:33:56.343Z" }, - { url = "https://files.pythonhosted.org/packages/67/dd/65fd874aa460c30da78f9d259400d8e6a4ef457d61ab052fd248f0050558/coverage-7.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:90d58ac63bc85e0fb919f14d09d6caa63f35a5512a2205284b7816cafd21bb03", size = 218771, upload-time = "2025-11-18T13:33:57.966Z" }, - { url = "https://files.pythonhosted.org/packages/55/e0/7c6b71d327d8068cb79c05f8f45bf1b6145f7a0de23bbebe63578fe5240a/coverage-7.12.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca8ecfa283764fdda3eae1bdb6afe58bf78c2c3ec2b2edcb05a671f0bba7b3f9", size = 260151, upload-time = "2025-11-18T13:33:59.597Z" }, - { url = "https://files.pythonhosted.org/packages/49/ce/4697457d58285b7200de6b46d606ea71066c6e674571a946a6ea908fb588/coverage-7.12.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:874fe69a0785d96bd066059cd4368022cebbec1a8958f224f0016979183916e6", size = 262257, upload-time = "2025-11-18T13:34:01.166Z" }, - { url = "https://files.pythonhosted.org/packages/2f/33/acbc6e447aee4ceba88c15528dbe04a35fb4d67b59d393d2e0d6f1e242c1/coverage-7.12.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3c889c0b8b283a24d721a9eabc8ccafcfc3aebf167e4cd0d0e23bf8ec4e339", size = 264671, upload-time = "2025-11-18T13:34:02.795Z" }, - { url = "https://files.pythonhosted.org/packages/87/ec/e2822a795c1ed44d569980097be839c5e734d4c0c1119ef8e0a073496a30/coverage-7.12.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bb5b894b3ec09dcd6d3743229dc7f2c42ef7787dc40596ae04c0edda487371e", size = 259231, upload-time = "2025-11-18T13:34:04.397Z" }, - { url = "https://files.pythonhosted.org/packages/72/c5/a7ec5395bb4a49c9b7ad97e63f0c92f6bf4a9e006b1393555a02dae75f16/coverage-7.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:79a44421cd5fba96aa57b5e3b5a4d3274c449d4c622e8f76882d76635501fd13", size = 262137, upload-time = "2025-11-18T13:34:06.068Z" }, - { url = "https://files.pythonhosted.org/packages/67/0c/02c08858b764129f4ecb8e316684272972e60777ae986f3865b10940bdd6/coverage-7.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:33baadc0efd5c7294f436a632566ccc1f72c867f82833eb59820ee37dc811c6f", size = 259745, upload-time = "2025-11-18T13:34:08.04Z" }, - { url = "https://files.pythonhosted.org/packages/5a/04/4fd32b7084505f3829a8fe45c1a74a7a728cb251aaadbe3bec04abcef06d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c406a71f544800ef7e9e0000af706b88465f3573ae8b8de37e5f96c59f689ad1", size = 258570, upload-time = "2025-11-18T13:34:09.676Z" }, - { url = "https://files.pythonhosted.org/packages/48/35/2365e37c90df4f5342c4fa202223744119fe31264ee2924f09f074ea9b6d/coverage-7.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e71bba6a40883b00c6d571599b4627f50c360b3d0d02bfc658168936be74027b", size = 260899, upload-time = "2025-11-18T13:34:11.259Z" }, - { url = "https://files.pythonhosted.org/packages/05/56/26ab0464ca733fa325e8e71455c58c1c374ce30f7c04cebb88eabb037b18/coverage-7.12.0-cp314-cp314t-win32.whl", hash = "sha256:9157a5e233c40ce6613dead4c131a006adfda70e557b6856b97aceed01b0e27a", size = 221313, upload-time = "2025-11-18T13:34:12.863Z" }, - { url = "https://files.pythonhosted.org/packages/da/1c/017a3e1113ed34d998b27d2c6dba08a9e7cb97d362f0ec988fcd873dcf81/coverage-7.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e84da3a0fd233aeec797b981c51af1cabac74f9bd67be42458365b30d11b5291", size = 222423, upload-time = "2025-11-18T13:34:15.14Z" }, - { url = "https://files.pythonhosted.org/packages/4c/36/bcc504fdd5169301b52568802bb1b9cdde2e27a01d39fbb3b4b508ab7c2c/coverage-7.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:01d24af36fedda51c2b1aca56e4330a3710f83b02a5ff3743a6b015ffa7c9384", size = 220459, upload-time = "2025-11-18T13:34:17.222Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a3/43b749004e3c09452e39bb56347a008f0a0668aad37324a99b5c8ca91d9e/coverage-7.12.0-py3-none-any.whl", hash = "sha256:159d50c0b12e060b15ed3d39f87ed43d4f7f7ad40b8a534f4dd331adbb51104a", size = 209503, upload-time = "2025-11-18T13:34:18.892Z" }, +version = "7.13.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" }, + { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" }, + { url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" }, + { url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" }, + { url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" }, + { url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" }, + { url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" }, + { url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" }, + { url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" }, + { url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" }, + { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, + { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, + { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, + { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, + { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, + { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, + { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, + { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, + { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, + { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, + { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, + { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, + { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, + { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, + { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, + { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, + { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, + { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, + { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, + { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, + { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, + { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, + { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, + { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, ] [package.optional-dependencies] @@ -588,15 +613,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, ] -[[package]] -name = "cycler" -version = "0.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, -] - [[package]] name = "distlib" version = "0.4.0" @@ -714,11 +730,11 @@ dev = [ [[package]] name = "filelock" -version = "3.20.3" +version = "3.24.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/a8/dae62680be63cbb3ff87cfa2f51cf766269514ea5488479d42fec5aa6f3a/filelock-3.24.2.tar.gz", hash = "sha256:c22803117490f156e59fafce621f0550a7a853e2bbf4f87f112b11d469b6c81b", size = 37601, upload-time = "2026-02-16T02:50:45.614Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, + { url = "https://files.pythonhosted.org/packages/e7/04/a94ebfb4eaaa08db56725a40de2887e95de4e8641b9e902c311bfa00aa39/filelock-3.24.2-py3-none-any.whl", hash = "sha256:667d7dc0b7d1e1064dd5f8f8e80bdac157a6482e8d2e02cd16fd3b6b33bd6556", size = 24152, upload-time = "2026-02-16T02:50:44Z" }, ] [[package]] @@ -784,14 +800,34 @@ wheels = [ [[package]] name = "griffe" -version = "1.15.0" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffecli" }, + { name = "griffelib" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/94/ee21d41e7eb4f823b94603b9d40f86d3c7fde80eacc2c3c71845476dddaa/griffe-2.0.0-py3-none-any.whl", hash = "sha256:5418081135a391c3e6e757a7f3f156f1a1a746cc7b4023868ff7d5e2f9a980aa", size = 5214, upload-time = "2026-02-09T19:09:44.105Z" }, +] + +[[package]] +name = "griffecli" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, + { name = "griffelib" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ed/d93f7a447bbf7a935d8868e9617cbe1cadf9ee9ee6bd275d3040fbf93d60/griffecli-2.0.0-py3-none-any.whl", hash = "sha256:9f7cd9ee9b21d55e91689358978d2385ae65c22f307a63fb3269acf3f21e643d", size = 9345, upload-time = "2026-02-09T19:09:42.554Z" }, +] + +[[package]] +name = "griffelib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" }, ] [[package]] @@ -842,11 +878,11 @@ wheels = [ [[package]] name = "identify" -version = "2.6.15" +version = "2.6.16" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, + { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" }, ] [[package]] @@ -961,87 +997,87 @@ wheels = [ [[package]] name = "jiter" -version = "0.12.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435, upload-time = "2025-11-09T20:47:02.087Z" }, - { url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548, upload-time = "2025-11-09T20:47:03.409Z" }, - { url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915, upload-time = "2025-11-09T20:47:05.171Z" }, - { url = "https://files.pythonhosted.org/packages/15/2d/c06e659888c128ad1e838123d0638f0efad90cc30860cb5f74dd3f2fc0b3/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966, upload-time = "2025-11-09T20:47:06.508Z" }, - { url = "https://files.pythonhosted.org/packages/6b/20/058db4ae5fb07cf6a4ab2e9b9294416f606d8e467fb74c2184b2a1eeacba/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047, upload-time = "2025-11-09T20:47:08.382Z" }, - { url = "https://files.pythonhosted.org/packages/49/bb/dc2b1c122275e1de2eb12905015d61e8316b2f888bdaac34221c301495d6/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835, upload-time = "2025-11-09T20:47:09.81Z" }, - { url = "https://files.pythonhosted.org/packages/23/7d/38f9cd337575349de16da575ee57ddb2d5a64d425c9367f5ef9e4612e32e/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587, upload-time = "2025-11-09T20:47:11.529Z" }, - { url = "https://files.pythonhosted.org/packages/f0/a3/b13e8e61e70f0bb06085099c4e2462647f53cc2ca97614f7fedcaa2bb9f3/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492, upload-time = "2025-11-09T20:47:12.993Z" }, - { url = "https://files.pythonhosted.org/packages/07/71/e0d11422ed027e21422f7bc1883c61deba2d9752b720538430c1deadfbca/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046, upload-time = "2025-11-09T20:47:14.6Z" }, - { url = "https://files.pythonhosted.org/packages/9f/59/b968a9aa7102a8375dbbdfbd2aeebe563c7e5dddf0f47c9ef1588a97e224/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392, upload-time = "2025-11-09T20:47:16.011Z" }, - { url = "https://files.pythonhosted.org/packages/ca/e4/7df62002499080dbd61b505c5cb351aa09e9959d176cac2aa8da6f93b13b/jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c", size = 206096, upload-time = "2025-11-09T20:47:17.344Z" }, - { url = "https://files.pythonhosted.org/packages/bb/60/1032b30ae0572196b0de0e87dce3b6c26a1eff71aad5fe43dee3082d32e0/jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f", size = 204899, upload-time = "2025-11-09T20:47:19.365Z" }, - { url = "https://files.pythonhosted.org/packages/49/d5/c145e526fccdb834063fb45c071df78b0cc426bbaf6de38b0781f45d956f/jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5", size = 188070, upload-time = "2025-11-09T20:47:20.75Z" }, - { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" }, - { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" }, - { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" }, - { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" }, - { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" }, - { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" }, - { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" }, - { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" }, - { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" }, - { url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546, upload-time = "2025-11-09T20:47:40.47Z" }, - { url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196, upload-time = "2025-11-09T20:47:41.794Z" }, - { url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100, upload-time = "2025-11-09T20:47:43.007Z" }, - { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" }, - { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" }, - { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" }, - { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" }, - { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" }, - { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" }, - { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" }, - { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" }, - { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" }, - { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" }, - { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" }, - { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" }, - { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" }, - { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" }, - { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" }, - { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" }, - { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" }, - { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" }, - { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" }, - { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" }, - { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" }, - { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" }, - { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" }, - { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" }, - { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" }, - { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" }, - { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" }, - { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" }, - { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" }, - { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" }, - { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" }, - { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" }, - { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" }, - { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" }, - { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" }, - { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" }, - { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" }, - { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" }, - { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" }, - { url = "https://files.pythonhosted.org/packages/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144, upload-time = "2025-11-09T20:49:10.503Z" }, - { url = "https://files.pythonhosted.org/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877, upload-time = "2025-11-09T20:49:12.269Z" }, - { url = "https://files.pythonhosted.org/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419, upload-time = "2025-11-09T20:49:13.803Z" }, - { url = "https://files.pythonhosted.org/packages/21/01/857d4608f5edb0664aa791a3d45702e1a5bcfff9934da74035e7b9803846/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d", size = 347212, upload-time = "2025-11-09T20:49:15.643Z" }, - { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" }, - { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" }, - { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" }, - { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" }, +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" }, + { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" }, + { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" }, + { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" }, + { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" }, + { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" }, + { url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709, upload-time = "2026-02-02T12:35:52.467Z" }, + { url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560, upload-time = "2026-02-02T12:35:53.925Z" }, + { url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608, upload-time = "2026-02-02T12:35:55.304Z" }, + { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, + { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, + { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, + { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, + { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, + { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, + { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, + { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, + { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, + { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, + { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, + { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, + { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, + { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, + { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, + { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, + { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, + { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, + { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, + { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, + { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, + { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, + { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, + { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, + { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, + { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, + { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" }, + { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" }, + { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" }, + { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, ] [[package]] @@ -1163,65 +1199,75 @@ wheels = [ [[package]] name = "librt" -version = "0.7.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b3/d9/6f3d3fcf5e5543ed8a60cc70fa7d50508ed60b8a10e9af6d2058159ab54e/librt-0.7.3.tar.gz", hash = "sha256:3ec50cf65235ff5c02c5b747748d9222e564ad48597122a361269dd3aa808798", size = 144549, upload-time = "2025-12-06T19:04:45.553Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/21/e6/f6391f5c6f158d31ed9af6bd1b1bcd3ffafdea1d816bc4219d0d90175a7f/librt-0.7.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:687403cced6a29590e6be6964463835315905221d797bc5c934a98750fe1a9af", size = 54711, upload-time = "2025-12-06T19:03:24.6Z" }, - { url = "https://files.pythonhosted.org/packages/ab/1b/53c208188c178987c081560a0fcf36f5ca500d5e21769596c845ef2f40d4/librt-0.7.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:24d70810f6e2ea853ff79338001533716b373cc0f63e2a0be5bc96129edb5fb5", size = 56664, upload-time = "2025-12-06T19:03:25.969Z" }, - { url = "https://files.pythonhosted.org/packages/cb/5c/d9da832b9a1e5f8366e8a044ec80217945385b26cb89fd6f94bfdc7d80b0/librt-0.7.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bf8c7735fbfc0754111f00edda35cf9e98a8d478de6c47b04eaa9cef4300eaa7", size = 161701, upload-time = "2025-12-06T19:03:27.035Z" }, - { url = "https://files.pythonhosted.org/packages/20/aa/1e0a7aba15e78529dd21f233076b876ee58c8b8711b1793315bdd3b263b0/librt-0.7.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32d43610dff472eab939f4d7fbdd240d1667794192690433672ae22d7af8445", size = 171040, upload-time = "2025-12-06T19:03:28.482Z" }, - { url = "https://files.pythonhosted.org/packages/69/46/3cfa325c1c2bc25775ec6ec1718cfbec9cff4ac767d37d2d3a2d1cc6f02c/librt-0.7.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:adeaa886d607fb02563c1f625cf2ee58778a2567c0c109378da8f17ec3076ad7", size = 184720, upload-time = "2025-12-06T19:03:29.599Z" }, - { url = "https://files.pythonhosted.org/packages/99/bb/e4553433d7ac47f4c75d0a7e59b13aee0e08e88ceadbee356527a9629b0a/librt-0.7.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:572a24fc5958c61431da456a0ef1eeea6b4989d81eeb18b8e5f1f3077592200b", size = 180731, upload-time = "2025-12-06T19:03:31.201Z" }, - { url = "https://files.pythonhosted.org/packages/35/89/51cd73006232981a3106d4081fbaa584ac4e27b49bc02266468d3919db03/librt-0.7.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6488e69d408b492e08bfb68f20c4a899a354b4386a446ecd490baff8d0862720", size = 174565, upload-time = "2025-12-06T19:03:32.818Z" }, - { url = "https://files.pythonhosted.org/packages/42/54/0578a78b587e5aa22486af34239a052c6366835b55fc307bc64380229e3f/librt-0.7.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ed028fc3d41adda916320712838aec289956c89b4f0a361ceadf83a53b4c047a", size = 195247, upload-time = "2025-12-06T19:03:34.434Z" }, - { url = "https://files.pythonhosted.org/packages/b5/0a/ee747cd999753dd9447e50b98fc36ee433b6c841a42dbf6d47b64b32a56e/librt-0.7.3-cp311-cp311-win32.whl", hash = "sha256:2cf9d73499486ce39eebbff5f42452518cc1f88d8b7ea4a711ab32962b176ee2", size = 47514, upload-time = "2025-12-06T19:03:35.959Z" }, - { url = "https://files.pythonhosted.org/packages/ec/af/8b13845178dec488e752878f8e290f8f89e7e34ae1528b70277aa1a6dd1e/librt-0.7.3-cp311-cp311-win_amd64.whl", hash = "sha256:35f1609e3484a649bb80431310ddbec81114cd86648f1d9482bc72a3b86ded2e", size = 54695, upload-time = "2025-12-06T19:03:36.956Z" }, - { url = "https://files.pythonhosted.org/packages/02/7a/ae59578501b1a25850266778f59279f4f3e726acc5c44255bfcb07b4bc57/librt-0.7.3-cp311-cp311-win_arm64.whl", hash = "sha256:550fdbfbf5bba6a2960b27376ca76d6aaa2bd4b1a06c4255edd8520c306fcfc0", size = 48142, upload-time = "2025-12-06T19:03:38.263Z" }, - { url = "https://files.pythonhosted.org/packages/29/90/ed8595fa4e35b6020317b5ea8d226a782dcbac7a997c19ae89fb07a41c66/librt-0.7.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0fa9ac2e49a6bee56e47573a6786cb635e128a7b12a0dc7851090037c0d397a3", size = 55687, upload-time = "2025-12-06T19:03:39.245Z" }, - { url = "https://files.pythonhosted.org/packages/dd/f6/6a20702a07b41006cb001a759440cb6b5362530920978f64a2b2ae2bf729/librt-0.7.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e980cf1ed1a2420a6424e2ed884629cdead291686f1048810a817de07b5eb18", size = 57127, upload-time = "2025-12-06T19:03:40.3Z" }, - { url = "https://files.pythonhosted.org/packages/79/f3/b0c4703d5ffe9359b67bb2ccb86c42d4e930a363cfc72262ac3ba53cff3e/librt-0.7.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e094e445c37c57e9ec612847812c301840239d34ccc5d153a982fa9814478c60", size = 165336, upload-time = "2025-12-06T19:03:41.369Z" }, - { url = "https://files.pythonhosted.org/packages/02/69/3ba05b73ab29ccbe003856232cea4049769be5942d799e628d1470ed1694/librt-0.7.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aca73d70c3f553552ba9133d4a09e767dcfeee352d8d8d3eb3f77e38a3beb3ed", size = 174237, upload-time = "2025-12-06T19:03:42.44Z" }, - { url = "https://files.pythonhosted.org/packages/22/ad/d7c2671e7bf6c285ef408aa435e9cd3fdc06fd994601e1f2b242df12034f/librt-0.7.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c634a0a6db395fdaba0361aa78395597ee72c3aad651b9a307a3a7eaf5efd67e", size = 189017, upload-time = "2025-12-06T19:03:44.01Z" }, - { url = "https://files.pythonhosted.org/packages/f4/94/d13f57193148004592b618555f296b41d2d79b1dc814ff8b3273a0bf1546/librt-0.7.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a59a69deeb458c858b8fea6acf9e2acd5d755d76cd81a655256bc65c20dfff5b", size = 183983, upload-time = "2025-12-06T19:03:45.834Z" }, - { url = "https://files.pythonhosted.org/packages/02/10/b612a9944ebd39fa143c7e2e2d33f2cb790205e025ddd903fb509a3a3bb3/librt-0.7.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d91e60ac44bbe3a77a67af4a4c13114cbe9f6d540337ce22f2c9eaf7454ca71f", size = 177602, upload-time = "2025-12-06T19:03:46.944Z" }, - { url = "https://files.pythonhosted.org/packages/1f/48/77bc05c4cc232efae6c5592c0095034390992edbd5bae8d6cf1263bb7157/librt-0.7.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:703456146dc2bf430f7832fd1341adac5c893ec3c1430194fdcefba00012555c", size = 199282, upload-time = "2025-12-06T19:03:48.069Z" }, - { url = "https://files.pythonhosted.org/packages/12/aa/05916ccd864227db1ffec2a303ae34f385c6b22d4e7ce9f07054dbcf083c/librt-0.7.3-cp312-cp312-win32.whl", hash = "sha256:b7c1239b64b70be7759554ad1a86288220bbb04d68518b527783c4ad3fb4f80b", size = 47879, upload-time = "2025-12-06T19:03:49.289Z" }, - { url = "https://files.pythonhosted.org/packages/50/92/7f41c42d31ea818b3c4b9cc1562e9714bac3c676dd18f6d5dd3d0f2aa179/librt-0.7.3-cp312-cp312-win_amd64.whl", hash = "sha256:ef59c938f72bdbc6ab52dc50f81d0637fde0f194b02d636987cea2ab30f8f55a", size = 54972, upload-time = "2025-12-06T19:03:50.335Z" }, - { url = "https://files.pythonhosted.org/packages/3f/dc/53582bbfb422311afcbc92adb75711f04e989cec052f08ec0152fbc36c9c/librt-0.7.3-cp312-cp312-win_arm64.whl", hash = "sha256:ff21c554304e8226bf80c3a7754be27c6c3549a9fec563a03c06ee8f494da8fc", size = 48338, upload-time = "2025-12-06T19:03:51.431Z" }, - { url = "https://files.pythonhosted.org/packages/93/7d/e0ce1837dfb452427db556e6d4c5301ba3b22fe8de318379fbd0593759b9/librt-0.7.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56f2a47beda8409061bc1c865bef2d4bd9ff9255219402c0817e68ab5ad89aed", size = 55742, upload-time = "2025-12-06T19:03:52.459Z" }, - { url = "https://files.pythonhosted.org/packages/be/c0/3564262301e507e1d5cf31c7d84cb12addf0d35e05ba53312494a2eba9a4/librt-0.7.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:14569ac5dd38cfccf0a14597a88038fb16811a6fede25c67b79c6d50fc2c8fdc", size = 57163, upload-time = "2025-12-06T19:03:53.516Z" }, - { url = "https://files.pythonhosted.org/packages/be/ac/245e72b7e443d24a562f6047563c7f59833384053073ef9410476f68505b/librt-0.7.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6038ccbd5968325a5d6fd393cf6e00b622a8de545f0994b89dd0f748dcf3e19e", size = 165840, upload-time = "2025-12-06T19:03:54.918Z" }, - { url = "https://files.pythonhosted.org/packages/98/af/587e4491f40adba066ba39a450c66bad794c8d92094f936a201bfc7c2b5f/librt-0.7.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d39079379a9a28e74f4d57dc6357fa310a1977b51ff12239d7271ec7e71d67f5", size = 174827, upload-time = "2025-12-06T19:03:56.082Z" }, - { url = "https://files.pythonhosted.org/packages/78/21/5b8c60ea208bc83dd00421022a3874330685d7e856404128dc3728d5d1af/librt-0.7.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8837d5a52a2d7aa9f4c3220a8484013aed1d8ad75240d9a75ede63709ef89055", size = 189612, upload-time = "2025-12-06T19:03:57.507Z" }, - { url = "https://files.pythonhosted.org/packages/da/2f/8b819169ef696421fb81cd04c6cdf225f6e96f197366001e9d45180d7e9e/librt-0.7.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:399bbd7bcc1633c3e356ae274a1deb8781c7bf84d9c7962cc1ae0c6e87837292", size = 184584, upload-time = "2025-12-06T19:03:58.686Z" }, - { url = "https://files.pythonhosted.org/packages/6c/fc/af9d225a9395b77bd7678362cb055d0b8139c2018c37665de110ca388022/librt-0.7.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8d8cf653e798ee4c4e654062b633db36984a1572f68c3aa25e364a0ddfbbb910", size = 178269, upload-time = "2025-12-06T19:03:59.769Z" }, - { url = "https://files.pythonhosted.org/packages/6c/d8/7b4fa1683b772966749d5683aa3fd605813defffe157833a8fa69cc89207/librt-0.7.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2f03484b54bf4ae80ab2e504a8d99d20d551bfe64a7ec91e218010b467d77093", size = 199852, upload-time = "2025-12-06T19:04:00.901Z" }, - { url = "https://files.pythonhosted.org/packages/77/e8/4598413aece46ca38d9260ef6c51534bd5f34b5c21474fcf210ce3a02123/librt-0.7.3-cp313-cp313-win32.whl", hash = "sha256:44b3689b040df57f492e02cd4f0bacd1b42c5400e4b8048160c9d5e866de8abe", size = 47936, upload-time = "2025-12-06T19:04:02.054Z" }, - { url = "https://files.pythonhosted.org/packages/af/80/ac0e92d5ef8c6791b3e2c62373863827a279265e0935acdf807901353b0e/librt-0.7.3-cp313-cp313-win_amd64.whl", hash = "sha256:6b407c23f16ccc36614c136251d6b32bf30de7a57f8e782378f1107be008ddb0", size = 54965, upload-time = "2025-12-06T19:04:03.224Z" }, - { url = "https://files.pythonhosted.org/packages/f1/fd/042f823fcbff25c1449bb4203a29919891ca74141b68d3a5f6612c4ce283/librt-0.7.3-cp313-cp313-win_arm64.whl", hash = "sha256:abfc57cab3c53c4546aee31859ef06753bfc136c9d208129bad23e2eca39155a", size = 48350, upload-time = "2025-12-06T19:04:04.234Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ae/c6ecc7bb97134a71b5241e8855d39964c0e5f4d96558f0d60593892806d2/librt-0.7.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:120dd21d46ff875e849f1aae19346223cf15656be489242fe884036b23d39e93", size = 55175, upload-time = "2025-12-06T19:04:05.308Z" }, - { url = "https://files.pythonhosted.org/packages/cf/bc/2cc0cb0ab787b39aa5c7645cd792433c875982bdf12dccca558b89624594/librt-0.7.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1617bea5ab31266e152871208502ee943cb349c224846928a1173c864261375e", size = 56881, upload-time = "2025-12-06T19:04:06.674Z" }, - { url = "https://files.pythonhosted.org/packages/8e/87/397417a386190b70f5bf26fcedbaa1515f19dce33366e2684c6b7ee83086/librt-0.7.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93b2a1f325fefa1482516ced160c8c7b4b8d53226763fa6c93d151fa25164207", size = 163710, upload-time = "2025-12-06T19:04:08.437Z" }, - { url = "https://files.pythonhosted.org/packages/c9/37/7338f85b80e8a17525d941211451199845093ca242b32efbf01df8531e72/librt-0.7.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d4801db8354436fd3936531e7f0e4feb411f62433a6b6cb32bb416e20b529f", size = 172471, upload-time = "2025-12-06T19:04:10.124Z" }, - { url = "https://files.pythonhosted.org/packages/3b/e0/741704edabbfae2c852fedc1b40d9ed5a783c70ed3ed8e4fe98f84b25d13/librt-0.7.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11ad45122bbed42cfc8b0597450660126ef28fd2d9ae1a219bc5af8406f95678", size = 186804, upload-time = "2025-12-06T19:04:11.586Z" }, - { url = "https://files.pythonhosted.org/packages/f4/d1/0a82129d6ba242f3be9af34815be089f35051bc79619f5c27d2c449ecef6/librt-0.7.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6b4e7bff1d76dd2b46443078519dc75df1b5e01562345f0bb740cea5266d8218", size = 181817, upload-time = "2025-12-06T19:04:12.802Z" }, - { url = "https://files.pythonhosted.org/packages/4f/32/704f80bcf9979c68d4357c46f2af788fbf9d5edda9e7de5786ed2255e911/librt-0.7.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:d86f94743a11873317094326456b23f8a5788bad9161fd2f0e52088c33564620", size = 175602, upload-time = "2025-12-06T19:04:14.004Z" }, - { url = "https://files.pythonhosted.org/packages/f7/6d/4355cfa0fae0c062ba72f541d13db5bc575770125a7ad3d4f46f4109d305/librt-0.7.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:754a0d09997095ad764ccef050dd5bf26cbf457aab9effcba5890dad081d879e", size = 196497, upload-time = "2025-12-06T19:04:15.487Z" }, - { url = "https://files.pythonhosted.org/packages/2e/eb/ac6d8517d44209e5a712fde46f26d0055e3e8969f24d715f70bd36056230/librt-0.7.3-cp314-cp314-win32.whl", hash = "sha256:fbd7351d43b80d9c64c3cfcb50008f786cc82cba0450e8599fdd64f264320bd3", size = 44678, upload-time = "2025-12-06T19:04:16.688Z" }, - { url = "https://files.pythonhosted.org/packages/e9/93/238f026d141faf9958da588c761a0812a1a21c98cc54a76f3608454e4e59/librt-0.7.3-cp314-cp314-win_amd64.whl", hash = "sha256:d376a35c6561e81d2590506804b428fc1075fcc6298fc5bb49b771534c0ba010", size = 51689, upload-time = "2025-12-06T19:04:17.726Z" }, - { url = "https://files.pythonhosted.org/packages/52/44/43f462ad9dcf9ed7d3172fe2e30d77b980956250bd90e9889a9cca93df2a/librt-0.7.3-cp314-cp314-win_arm64.whl", hash = "sha256:cbdb3f337c88b43c3b49ca377731912c101178be91cb5071aac48faa898e6f8e", size = 44662, upload-time = "2025-12-06T19:04:18.771Z" }, - { url = "https://files.pythonhosted.org/packages/1d/35/fed6348915f96b7323241de97f26e2af481e95183b34991df12fd5ce31b1/librt-0.7.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9f0e0927efe87cd42ad600628e595a1a0aa1c64f6d0b55f7e6059079a428641a", size = 57347, upload-time = "2025-12-06T19:04:19.812Z" }, - { url = "https://files.pythonhosted.org/packages/9a/f2/045383ccc83e3fea4fba1b761796584bc26817b6b2efb6b8a6731431d16f/librt-0.7.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:020c6db391268bcc8ce75105cb572df8cb659a43fd347366aaa407c366e5117a", size = 59223, upload-time = "2025-12-06T19:04:20.862Z" }, - { url = "https://files.pythonhosted.org/packages/77/3f/c081f8455ab1d7f4a10dbe58463ff97119272ff32494f21839c3b9029c2c/librt-0.7.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7af7785f5edd1f418da09a8cdb9ec84b0213e23d597413e06525340bcce1ea4f", size = 183861, upload-time = "2025-12-06T19:04:21.963Z" }, - { url = "https://files.pythonhosted.org/packages/1d/f5/73c5093c22c31fbeaebc25168837f05ebfd8bf26ce00855ef97a5308f36f/librt-0.7.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ccadf260bb46a61b9c7e89e2218f6efea9f3eeaaab4e3d1f58571890e54858e", size = 194594, upload-time = "2025-12-06T19:04:23.14Z" }, - { url = "https://files.pythonhosted.org/packages/78/b8/d5f17d4afe16612a4a94abfded94c16c5a033f183074fb130dfe56fc1a42/librt-0.7.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9883b2d819ce83f87ba82a746c81d14ada78784db431e57cc9719179847376e", size = 206759, upload-time = "2025-12-06T19:04:24.328Z" }, - { url = "https://files.pythonhosted.org/packages/36/2e/021765c1be85ee23ffd5b5b968bb4cba7526a4db2a0fc27dcafbdfc32da7/librt-0.7.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:59cb0470612d21fa1efddfa0dd710756b50d9c7fb6c1236bbf8ef8529331dc70", size = 203210, upload-time = "2025-12-06T19:04:25.544Z" }, - { url = "https://files.pythonhosted.org/packages/77/f0/9923656e42da4fd18c594bd08cf6d7e152d4158f8b808e210d967f0dcceb/librt-0.7.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:1fe603877e1865b5fd047a5e40379509a4a60204aa7aa0f72b16f7a41c3f0712", size = 196708, upload-time = "2025-12-06T19:04:26.725Z" }, - { url = "https://files.pythonhosted.org/packages/fc/0b/0708b886ac760e64d6fbe7e16024e4be3ad1a3629d19489a97e9cf4c3431/librt-0.7.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5460d99ed30f043595bbdc888f542bad2caeb6226b01c33cda3ae444e8f82d42", size = 217212, upload-time = "2025-12-06T19:04:27.892Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7f/12a73ff17bca4351e73d585dd9ebf46723c4a8622c4af7fe11a2e2d011ff/librt-0.7.3-cp314-cp314t-win32.whl", hash = "sha256:d09f677693328503c9e492e33e9601464297c01f9ebd966ea8fc5308f3069bfd", size = 45586, upload-time = "2025-12-06T19:04:29.116Z" }, - { url = "https://files.pythonhosted.org/packages/e2/df/8decd032ac9b995e4f5606cde783711a71094128d88d97a52e397daf2c89/librt-0.7.3-cp314-cp314t-win_amd64.whl", hash = "sha256:25711f364c64cab2c910a0247e90b51421e45dbc8910ceeb4eac97a9e132fc6f", size = 53002, upload-time = "2025-12-06T19:04:30.173Z" }, - { url = "https://files.pythonhosted.org/packages/de/0c/6605b6199de8178afe7efc77ca1d8e6db00453bc1d3349d27605c0f42104/librt-0.7.3-cp314-cp314t-win_arm64.whl", hash = "sha256:a9f9b661f82693eb56beb0605156c7fca57f535704ab91837405913417d6990b", size = 45647, upload-time = "2025-12-06T19:04:31.302Z" }, +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/3f/4ca7dd7819bf8ff303aca39c3c60e5320e46e766ab7f7dd627d3b9c11bdf/librt-0.8.0.tar.gz", hash = "sha256:cb74cdcbc0103fc988e04e5c58b0b31e8e5dd2babb9182b6f9490488eb36324b", size = 177306, upload-time = "2026-02-12T14:53:54.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/e9/42af181c89b65abfd557c1b017cba5b82098eef7bf26d1649d82ce93ccc7/librt-0.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ce33a9778e294507f3a0e3468eccb6a698b5166df7db85661543eca1cfc5369", size = 65314, upload-time = "2026-02-12T14:52:14.778Z" }, + { url = "https://files.pythonhosted.org/packages/9d/4a/15a847fca119dc0334a4b8012b1e15fdc5fc19d505b71e227eaf1bcdba09/librt-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8070aa3368559de81061ef752770d03ca1f5fc9467d4d512d405bd0483bfffe6", size = 68015, upload-time = "2026-02-12T14:52:15.797Z" }, + { url = "https://files.pythonhosted.org/packages/e1/87/ffc8dbd6ab68dd91b736c88529411a6729649d2b74b887f91f3aaff8d992/librt-0.8.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:20f73d4fecba969efc15cdefd030e382502d56bb6f1fc66b580cce582836c9fa", size = 194508, upload-time = "2026-02-12T14:52:16.835Z" }, + { url = "https://files.pythonhosted.org/packages/89/92/a7355cea28d6c48ff6ff5083ac4a2a866fb9b07b786aa70d1f1116680cd5/librt-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a512c88900bdb1d448882f5623a0b1ad27ba81a9bd75dacfe17080b72272ca1f", size = 205630, upload-time = "2026-02-12T14:52:18.58Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5e/54509038d7ac527828db95b8ba1c8f5d2649bc32fd8f39b1718ec9957dce/librt-0.8.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:015e2dde6e096d27c10238bf9f6492ba6c65822dfb69d2bf74c41a8e88b7ddef", size = 218289, upload-time = "2026-02-12T14:52:20.134Z" }, + { url = "https://files.pythonhosted.org/packages/6d/17/0ee0d13685cefee6d6f2d47bb643ddad3c62387e2882139794e6a5f1288a/librt-0.8.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c25a131013eadd3c600686a0c0333eb2896483cbc7f65baa6a7ee761017aef9", size = 211508, upload-time = "2026-02-12T14:52:21.413Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a8/1714ef6e9325582e3727de3be27e4c1b2f428ea411d09f1396374180f130/librt-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:21b14464bee0b604d80a638cf1ee3148d84ca4cc163dcdcecb46060c1b3605e4", size = 219129, upload-time = "2026-02-12T14:52:22.61Z" }, + { url = "https://files.pythonhosted.org/packages/89/d3/2d9fe353edff91cdc0ece179348054a6fa61f3de992c44b9477cb973509b/librt-0.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:05a3dd3f116747f7e1a2b475ccdc6fb637fd4987126d109e03013a79d40bf9e6", size = 213126, upload-time = "2026-02-12T14:52:23.819Z" }, + { url = "https://files.pythonhosted.org/packages/ad/8e/9f5c60444880f6ad50e3ff7475e5529e787797e7f3ad5432241633733b92/librt-0.8.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fa37f99bff354ff191c6bcdffbc9d7cdd4fc37faccfc9be0ef3a4fd5613977da", size = 212279, upload-time = "2026-02-12T14:52:25.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/eb/d4a2cfa647da3022ae977f50d7eda1d91f70d7d1883cf958a4b6ef689eab/librt-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1566dbb9d1eb0987264c9b9460d212e809ba908d2f4a3999383a84d765f2f3f1", size = 234654, upload-time = "2026-02-12T14:52:26.204Z" }, + { url = "https://files.pythonhosted.org/packages/6a/31/26b978861c7983b036a3aea08bdbb2ec32bbaab1ad1d57c5e022be59afc1/librt-0.8.0-cp311-cp311-win32.whl", hash = "sha256:70defb797c4d5402166787a6b3c66dfb3fa7f93d118c0509ffafa35a392f4258", size = 54603, upload-time = "2026-02-12T14:52:27.342Z" }, + { url = "https://files.pythonhosted.org/packages/d0/78/f194ed7c48dacf875677e749c5d0d1d69a9daa7c994314a39466237fb1be/librt-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:db953b675079884ffda33d1dca7189fb961b6d372153750beb81880384300817", size = 61730, upload-time = "2026-02-12T14:52:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/97/ee/ad71095478d02137b6f49469dc808c595cfe89b50985f6b39c5345f0faab/librt-0.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:75d1a8cab20b2043f03f7aab730551e9e440adc034d776f15f6f8d582b0a5ad4", size = 52274, upload-time = "2026-02-12T14:52:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fb/53/f3bc0c4921adb0d4a5afa0656f2c0fbe20e18e3e0295e12985b9a5dc3f55/librt-0.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:17269dd2745dbe8e42475acb28e419ad92dfa38214224b1b01020b8cac70b645", size = 66511, upload-time = "2026-02-12T14:52:30.34Z" }, + { url = "https://files.pythonhosted.org/packages/89/4b/4c96357432007c25a1b5e363045373a6c39481e49f6ba05234bb59a839c1/librt-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f4617cef654fca552f00ce5ffdf4f4b68770f18950e4246ce94629b789b92467", size = 68628, upload-time = "2026-02-12T14:52:31.491Z" }, + { url = "https://files.pythonhosted.org/packages/47/16/52d75374d1012e8fc709216b5eaa25f471370e2a2331b8be00f18670a6c7/librt-0.8.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5cb11061a736a9db45e3c1293cfcb1e3caf205912dfa085734ba750f2197ff9a", size = 198941, upload-time = "2026-02-12T14:52:32.489Z" }, + { url = "https://files.pythonhosted.org/packages/fc/11/d5dd89e5a2228567b1228d8602d896736247424484db086eea6b8010bcba/librt-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4bb00bd71b448f16749909b08a0ff16f58b079e2261c2e1000f2bbb2a4f0a45", size = 210009, upload-time = "2026-02-12T14:52:33.634Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/fc1a92a77c3020ee08ce2dc48aed4b42ab7c30fb43ce488d388673b0f164/librt-0.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95a719a049f0eefaf1952673223cf00d442952273cbd20cf2ed7ec423a0ef58d", size = 224461, upload-time = "2026-02-12T14:52:34.868Z" }, + { url = "https://files.pythonhosted.org/packages/7f/98/eb923e8b028cece924c246104aa800cf72e02d023a8ad4ca87135b05a2fe/librt-0.8.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bd32add59b58fba3439d48d6f36ac695830388e3da3e92e4fc26d2d02670d19c", size = 217538, upload-time = "2026-02-12T14:52:36.078Z" }, + { url = "https://files.pythonhosted.org/packages/fd/67/24e80ab170674a1d8ee9f9a83081dca4635519dbd0473b8321deecddb5be/librt-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4f764b2424cb04524ff7a486b9c391e93f93dc1bd8305b2136d25e582e99aa2f", size = 225110, upload-time = "2026-02-12T14:52:37.301Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c7/6fbdcbd1a6e5243c7989c21d68ab967c153b391351174b4729e359d9977f/librt-0.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f04ca50e847abc486fa8f4107250566441e693779a5374ba211e96e238f298b9", size = 217758, upload-time = "2026-02-12T14:52:38.89Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bd/4d6b36669db086e3d747434430073e14def032dd58ad97959bf7e2d06c67/librt-0.8.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9ab3a3475a55b89b87ffd7e6665838e8458e0b596c22e0177e0f961434ec474a", size = 218384, upload-time = "2026-02-12T14:52:40.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/2d/afe966beb0a8f179b132f3e95c8dd90738a23e9ebdba10f89a3f192f9366/librt-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e36a8da17134ffc29373775d88c04832f9ecfab1880470661813e6c7991ef79", size = 241187, upload-time = "2026-02-12T14:52:43.55Z" }, + { url = "https://files.pythonhosted.org/packages/02/d0/6172ea4af2b538462785ab1a68e52d5c99cfb9866a7caf00fdf388299734/librt-0.8.0-cp312-cp312-win32.whl", hash = "sha256:4eb5e06ebcc668677ed6389164f52f13f71737fc8be471101fa8b4ce77baeb0c", size = 54914, upload-time = "2026-02-12T14:52:44.676Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cb/ceb6ed6175612a4337ad49fb01ef594712b934b4bc88ce8a63554832eb44/librt-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:0a33335eb59921e77c9acc05d0e654e4e32e45b014a4d61517897c11591094f8", size = 62020, upload-time = "2026-02-12T14:52:45.676Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7e/61701acbc67da74ce06ddc7ba9483e81c70f44236b2d00f6a4bfee1aacbf/librt-0.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:24a01c13a2a9bdad20997a4443ebe6e329df063d1978bbe2ebbf637878a46d1e", size = 52443, upload-time = "2026-02-12T14:52:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/6d/32/3edb0bcb4113a9c8bdcd1750663a54565d255027657a5df9d90f13ee07fa/librt-0.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7f820210e21e3a8bf8fde2ae3c3d10106d4de9ead28cbfdf6d0f0f41f5b12fa1", size = 66522, upload-time = "2026-02-12T14:52:48.219Z" }, + { url = "https://files.pythonhosted.org/packages/30/ab/e8c3d05e281f5d405ebdcc5bc8ab36df23e1a4b40ac9da8c3eb9928b72b9/librt-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4831c44b8919e75ca0dfb52052897c1ef59fdae19d3589893fbd068f1e41afbf", size = 68658, upload-time = "2026-02-12T14:52:50.351Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d3/74a206c47b7748bbc8c43942de3ed67de4c231156e148b4f9250869593df/librt-0.8.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:88c6e75540f1f10f5e0fc5e87b4b6c290f0e90d1db8c6734f670840494764af8", size = 199287, upload-time = "2026-02-12T14:52:51.938Z" }, + { url = "https://files.pythonhosted.org/packages/fa/29/ef98a9131cf12cb95771d24e4c411fda96c89dc78b09c2de4704877ebee4/librt-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9646178cd794704d722306c2c920c221abbf080fede3ba539d5afdec16c46dad", size = 210293, upload-time = "2026-02-12T14:52:53.128Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3e/89b4968cb08c53d4c2d8b02517081dfe4b9e07a959ec143d333d76899f6c/librt-0.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e1af31a710e17891d9adf0dbd9a5fcd94901a3922a96499abdbf7ce658f4e01", size = 224801, upload-time = "2026-02-12T14:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/6d/28/f38526d501f9513f8b48d78e6be4a241e15dd4b000056dc8b3f06ee9ce5d/librt-0.8.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:507e94f4bec00b2f590fbe55f48cd518a208e2474a3b90a60aa8f29136ddbada", size = 218090, upload-time = "2026-02-12T14:52:55.758Z" }, + { url = "https://files.pythonhosted.org/packages/02/ec/64e29887c5009c24dc9c397116c680caffc50286f62bd99c39e3875a2854/librt-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f1178e0de0c271231a660fbef9be6acdfa1d596803464706862bef6644cc1cae", size = 225483, upload-time = "2026-02-12T14:52:57.375Z" }, + { url = "https://files.pythonhosted.org/packages/ee/16/7850bdbc9f1a32d3feff2708d90c56fc0490b13f1012e438532781aa598c/librt-0.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:71fc517efc14f75c2f74b1f0a5d5eb4a8e06aa135c34d18eaf3522f4a53cd62d", size = 218226, upload-time = "2026-02-12T14:52:58.534Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4a/166bffc992d65ddefa7c47052010a87c059b44a458ebaf8f5eba384b0533/librt-0.8.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0583aef7e9a720dd40f26a2ad5a1bf2ccbb90059dac2b32ac516df232c701db3", size = 218755, upload-time = "2026-02-12T14:52:59.701Z" }, + { url = "https://files.pythonhosted.org/packages/da/5d/9aeee038bcc72a9cfaaee934463fe9280a73c5440d36bd3175069d2cb97b/librt-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5d0f76fc73480d42285c609c0ea74d79856c160fa828ff9aceab574ea4ecfd7b", size = 241617, upload-time = "2026-02-12T14:53:00.966Z" }, + { url = "https://files.pythonhosted.org/packages/64/ff/2bec6b0296b9d0402aa6ec8540aa19ebcb875d669c37800cb43d10d9c3a3/librt-0.8.0-cp313-cp313-win32.whl", hash = "sha256:e79dbc8f57de360f0ed987dc7de7be814b4803ef0e8fc6d3ff86e16798c99935", size = 54966, upload-time = "2026-02-12T14:53:02.042Z" }, + { url = "https://files.pythonhosted.org/packages/08/8d/bf44633b0182996b2c7ea69a03a5c529683fa1f6b8e45c03fe874ff40d56/librt-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:25b3e667cbfc9000c4740b282df599ebd91dbdcc1aa6785050e4c1d6be5329ab", size = 62000, upload-time = "2026-02-12T14:53:03.822Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fd/c6472b8e0eac0925001f75e366cf5500bcb975357a65ef1f6b5749389d3a/librt-0.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:e9a3a38eb4134ad33122a6d575e6324831f930a771d951a15ce232e0237412c2", size = 52496, upload-time = "2026-02-12T14:53:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/13/79ebfe30cd273d7c0ce37a5f14dc489c5fb8b722a008983db2cfd57270bb/librt-0.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:421765e8c6b18e64d21c8ead315708a56fc24f44075059702e421d164575fdda", size = 66078, upload-time = "2026-02-12T14:53:06.085Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8f/d11eca40b62a8d5e759239a80636386ef88adecb10d1a050b38cc0da9f9e/librt-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:48f84830a8f8ad7918afd743fd7c4eb558728bceab7b0e38fd5a5cf78206a556", size = 68309, upload-time = "2026-02-12T14:53:07.121Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b4/f12ee70a3596db40ff3c88ec9eaa4e323f3b92f77505b4d900746706ec6a/librt-0.8.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9f09d4884f882baa39a7e36bbf3eae124c4ca2a223efb91e567381d1c55c6b06", size = 196804, upload-time = "2026-02-12T14:53:08.164Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7e/70dbbdc0271fd626abe1671ad117bcd61a9a88cdc6a10ccfbfc703db1873/librt-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:693697133c3b32aa9b27f040e3691be210e9ac4d905061859a9ed519b1d5a376", size = 206915, upload-time = "2026-02-12T14:53:09.333Z" }, + { url = "https://files.pythonhosted.org/packages/79/13/6b9e05a635d4327608d06b3c1702166e3b3e78315846373446cf90d7b0bf/librt-0.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5512aae4648152abaf4d48b59890503fcbe86e85abc12fb9b096fe948bdd816", size = 221200, upload-time = "2026-02-12T14:53:10.68Z" }, + { url = "https://files.pythonhosted.org/packages/35/6c/e19a3ac53e9414de43a73d7507d2d766cd22d8ca763d29a4e072d628db42/librt-0.8.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:995d24caa6bbb34bcdd4a41df98ac6d1af637cfa8975cb0790e47d6623e70e3e", size = 214640, upload-time = "2026-02-12T14:53:12.342Z" }, + { url = "https://files.pythonhosted.org/packages/30/f0/23a78464788619e8c70f090cfd099cce4973eed142c4dccb99fc322283fd/librt-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b9aef96d7593584e31ef6ac1eb9775355b0099fee7651fae3a15bc8657b67b52", size = 221980, upload-time = "2026-02-12T14:53:13.603Z" }, + { url = "https://files.pythonhosted.org/packages/03/32/38e21420c5d7aa8a8bd2c7a7d5252ab174a5a8aaec8b5551968979b747bf/librt-0.8.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4f6e975377fbc4c9567cb33ea9ab826031b6c7ec0515bfae66a4fb110d40d6da", size = 215146, upload-time = "2026-02-12T14:53:14.8Z" }, + { url = "https://files.pythonhosted.org/packages/bb/00/bd9ecf38b1824c25240b3ad982fb62c80f0a969e6679091ba2b3afb2b510/librt-0.8.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:daae5e955764be8fd70a93e9e5133c75297f8bce1e802e1d3683b98f77e1c5ab", size = 215203, upload-time = "2026-02-12T14:53:16.087Z" }, + { url = "https://files.pythonhosted.org/packages/b9/60/7559bcc5279d37810b98d4a52616febd7b8eef04391714fd6bdf629598b1/librt-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7bd68cebf3131bb920d5984f75fe302d758db33264e44b45ad139385662d7bc3", size = 237937, upload-time = "2026-02-12T14:53:17.236Z" }, + { url = "https://files.pythonhosted.org/packages/41/cc/be3e7da88f1abbe2642672af1dc00a0bccece11ca60241b1883f3018d8d5/librt-0.8.0-cp314-cp314-win32.whl", hash = "sha256:1e6811cac1dcb27ca4c74e0ca4a5917a8e06db0d8408d30daee3a41724bfde7a", size = 50685, upload-time = "2026-02-12T14:53:18.888Z" }, + { url = "https://files.pythonhosted.org/packages/38/27/e381d0df182a8f61ef1f6025d8b138b3318cc9d18ad4d5f47c3bf7492523/librt-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:178707cda89d910c3b28bf5aa5f69d3d4734e0f6ae102f753ad79edef83a83c7", size = 57872, upload-time = "2026-02-12T14:53:19.942Z" }, + { url = "https://files.pythonhosted.org/packages/c5/0c/ca9dfdf00554a44dea7d555001248269a4bab569e1590a91391feb863fa4/librt-0.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3e8b77b5f54d0937b26512774916041756c9eb3e66f1031971e626eea49d0bf4", size = 48056, upload-time = "2026-02-12T14:53:21.473Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ed/6cc9c4ad24f90c8e782193c7b4a857408fd49540800613d1356c63567d7b/librt-0.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:789911e8fa40a2e82f41120c936b1965f3213c67f5a483fc5a41f5839a05dcbb", size = 68307, upload-time = "2026-02-12T14:53:22.498Z" }, + { url = "https://files.pythonhosted.org/packages/84/d8/0e94292c6b3e00b6eeea39dd44d5703d1ec29b6dafce7eea19dc8f1aedbd/librt-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2b37437e7e4ef5e15a297b36ba9e577f73e29564131d86dd75875705e97402b5", size = 70999, upload-time = "2026-02-12T14:53:23.603Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f4/6be1afcbdeedbdbbf54a7c9d73ad43e1bf36897cebf3978308cd64922e02/librt-0.8.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:671a6152edf3b924d98a5ed5e6982ec9cb30894085482acadce0975f031d4c5c", size = 220782, upload-time = "2026-02-12T14:53:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f0/8d/f306e8caa93cfaf5c6c9e0d940908d75dc6af4fd856baa5535c922ee02b1/librt-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8992ca186a1678107b0af3d0c9303d8c7305981b9914989b9788319ed4d89546", size = 235420, upload-time = "2026-02-12T14:53:27.047Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f2/65d86bd462e9c351326564ca805e8457442149f348496e25ccd94583ffa2/librt-0.8.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:001e5330093d887b8b9165823eca6c5c4db183fe4edea4fdc0680bbac5f46944", size = 246452, upload-time = "2026-02-12T14:53:28.341Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/39c88b503b4cb3fcbdeb3caa29672b6b44ebee8dcc8a54d49839ac280f3f/librt-0.8.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d920789eca7ef71df7f31fd547ec0d3002e04d77f30ba6881e08a630e7b2c30e", size = 238891, upload-time = "2026-02-12T14:53:29.625Z" }, + { url = "https://files.pythonhosted.org/packages/e3/c6/6c0d68190893d01b71b9569b07a1c811e280c0065a791249921c83dc0290/librt-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:82fb4602d1b3e303a58bfe6165992b5a78d823ec646445356c332cd5f5bbaa61", size = 250249, upload-time = "2026-02-12T14:53:30.93Z" }, + { url = "https://files.pythonhosted.org/packages/52/7a/f715ed9e039035d0ea637579c3c0155ab3709a7046bc408c0fb05d337121/librt-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4d3e38797eb482485b486898f89415a6ab163bc291476bd95712e42cf4383c05", size = 240642, upload-time = "2026-02-12T14:53:32.174Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3c/609000a333debf5992efe087edc6467c1fdbdddca5b610355569bbea9589/librt-0.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a905091a13e0884701226860836d0386b88c72ce5c2fdfba6618e14c72be9f25", size = 239621, upload-time = "2026-02-12T14:53:33.39Z" }, + { url = "https://files.pythonhosted.org/packages/b9/df/87b0673d5c395a8f34f38569c116c93142d4dc7e04af2510620772d6bd4f/librt-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:375eda7acfce1f15f5ed56cfc960669eefa1ec8732e3e9087c3c4c3f2066759c", size = 262986, upload-time = "2026-02-12T14:53:34.617Z" }, + { url = "https://files.pythonhosted.org/packages/09/7f/6bbbe9dcda649684773aaea78b87fff4d7e59550fbc2877faa83612087a3/librt-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:2ccdd20d9a72c562ffb73098ac411de351b53a6fbb3390903b2d33078ef90447", size = 51328, upload-time = "2026-02-12T14:53:36.15Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f3/e1981ab6fa9b41be0396648b5850267888a752d025313a9e929c4856208e/librt-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:25e82d920d4d62ad741592fcf8d0f3bda0e3fc388a184cb7d2f566c681c5f7b9", size = 58719, upload-time = "2026-02-12T14:53:37.183Z" }, + { url = "https://files.pythonhosted.org/packages/94/d1/433b3c06e78f23486fe4fdd19bc134657eb30997d2054b0dbf52bbf3382e/librt-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:92249938ab744a5890580d3cb2b22042f0dce71cdaa7c1369823df62bedf7cbc", size = 48753, upload-time = "2026-02-12T14:53:38.539Z" }, ] [[package]] @@ -1328,11 +1374,11 @@ wheels = [ [[package]] name = "markdown" -version = "3.10" +version = "3.10.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931, upload-time = "2025-11-03T19:51:15.007Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678, upload-time = "2025-11-03T19:51:13.887Z" }, + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, ] [[package]] @@ -1487,7 +1533,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.25.0" +version = "1.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1505,9 +1551,9 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d5/2d/649d80a0ecf6a1f82632ca44bec21c0461a9d9fc8934d38cb5b319f2db5e/mcp-1.25.0.tar.gz", hash = "sha256:56310361ebf0364e2d438e5b45f7668cbb124e158bb358333cd06e49e83a6802", size = 605387, upload-time = "2025-12-19T10:19:56.985Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/fc/6dc7659c2ae5ddf280477011f4213a74f806862856b796ef08f028e664bf/mcp-1.25.0-py3-none-any.whl", hash = "sha256:b37c38144a666add0862614cc79ec276e97d72aa8ca26d622818d4e278b9721a", size = 233076, upload-time = "2025-12-19T10:19:55.416Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, ] [[package]] @@ -1554,16 +1600,16 @@ wheels = [ [[package]] name = "mkdocs-autorefs" -version = "1.4.3" +version = "1.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "markupsafe" }, { name = "mkdocs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/51/fa/9124cd63d822e2bcbea1450ae68cdc3faf3655c69b455f3a7ed36ce6c628/mkdocs_autorefs-1.4.3.tar.gz", hash = "sha256:beee715b254455c4aa93b6ef3c67579c399ca092259cc41b7d9342573ff1fc75", size = 55425, upload-time = "2025-08-26T14:23:17.223Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/4d/7123b6fa2278000688ebd338e2a06d16870aaf9eceae6ba047ea05f92df1/mkdocs_autorefs-1.4.3-py3-none-any.whl", hash = "sha256:469d85eb3114801d08e9cc55d102b3ba65917a869b893403b8987b601cf55dc9", size = 25034, upload-time = "2025-08-26T14:23:15.906Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" }, ] [[package]] @@ -1582,7 +1628,7 @@ wheels = [ [[package]] name = "mkdocs-material" -version = "9.7.0" +version = "9.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, @@ -1597,9 +1643,9 @@ dependencies = [ { name = "pymdown-extensions" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9c/3b/111b84cd6ff28d9e955b5f799ef217a17bc1684ac346af333e6100e413cb/mkdocs_material-9.7.0.tar.gz", hash = "sha256:602b359844e906ee402b7ed9640340cf8a474420d02d8891451733b6b02314ec", size = 4094546, upload-time = "2025-11-11T08:49:09.73Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/e2/2ffc356cd72f1473d07c7719d82a8f2cbd261666828614ecb95b12169f41/mkdocs_material-9.7.1.tar.gz", hash = "sha256:89601b8f2c3e6c6ee0a918cc3566cb201d40bf37c3cd3c2067e26fadb8cce2b8", size = 4094392, upload-time = "2025-12-18T09:49:00.308Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/87/eefe8d5e764f4cf50ed91b943f8e8f96b5efd65489d8303b7a36e2e79834/mkdocs_material-9.7.0-py3-none-any.whl", hash = "sha256:da2866ea53601125ff5baa8aa06404c6e07af3c5ce3d5de95e3b52b80b442887", size = 9283770, upload-time = "2025-11-11T08:49:06.26Z" }, + { url = "https://files.pythonhosted.org/packages/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-py3-none-any.whl", hash = "sha256:3f6100937d7d731f87f1e3e3b021c97f7239666b9ba1151ab476cabb96c60d5c", size = 9297166, upload-time = "2025-12-18T09:48:56.664Z" }, ] [[package]] @@ -1613,7 +1659,7 @@ wheels = [ [[package]] name = "mkdocstrings" -version = "1.0.0" +version = "1.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, @@ -1623,23 +1669,23 @@ dependencies = [ { name = "mkdocs-autorefs" }, { name = "pymdown-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e5/13/10bbf9d56565fd91b91e6f5a8cd9b9d8a2b101c4e8ad6eeafa35a706301d/mkdocstrings-1.0.0.tar.gz", hash = "sha256:351a006dbb27aefce241ade110d3cd040c1145b7a3eb5fd5ac23f03ed67f401a", size = 101086, upload-time = "2025-11-27T15:39:40.534Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/62/0dfc5719514115bf1781f44b1d7f2a0923fcc01e9c5d7990e48a05c9ae5d/mkdocstrings-1.0.3.tar.gz", hash = "sha256:ab670f55040722b49bb45865b2e93b824450fb4aef638b00d7acb493a9020434", size = 100946, upload-time = "2026-02-07T14:31:40.973Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/fc/80aa31b79133634721cf7855d37b76ea49773599214896f2ff10be03de2a/mkdocstrings-1.0.0-py3-none-any.whl", hash = "sha256:4c50eb960bff6e05dfc631f6bc00dfabffbcb29c5ff25f676d64daae05ed82fa", size = 35135, upload-time = "2025-11-27T15:39:39.301Z" }, + { url = "https://files.pythonhosted.org/packages/04/41/1cf02e3df279d2dd846a1bf235a928254eba9006dd22b4a14caa71aed0f7/mkdocstrings-1.0.3-py3-none-any.whl", hash = "sha256:0d66d18430c2201dc7fe85134277382baaa15e6b30979f3f3bdbabd6dbdb6046", size = 35523, upload-time = "2026-02-07T14:31:39.27Z" }, ] [[package]] name = "mkdocstrings-python" -version = "2.0.1" +version = "2.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "griffe" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/24/75/d30af27a2906f00eb90143470272376d728521997800f5dce5b340ba35bc/mkdocstrings_python-2.0.1.tar.gz", hash = "sha256:843a562221e6a471fefdd4b45cc6c22d2607ccbad632879234fa9692e9cf7732", size = 199345, upload-time = "2025-12-03T14:26:11.755Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/84/78243847ad9d5c21d30a2842720425b17e880d99dfe824dee11d6b2149b4/mkdocstrings_python-2.0.2.tar.gz", hash = "sha256:4a32ccfc4b8d29639864698e81cfeb04137bce76bb9f3c251040f55d4b6e1ad8", size = 199124, upload-time = "2026-02-09T15:12:01.543Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/06/c5f8deba7d2cbdfa7967a716ae801aa9ca5f734b8f54fd473ef77a088dbe/mkdocstrings_python-2.0.1-py3-none-any.whl", hash = "sha256:66ecff45c5f8b71bf174e11d49afc845c2dfc7fc0ab17a86b6b337e0f24d8d90", size = 105055, upload-time = "2025-12-03T14:26:10.184Z" }, + { url = "https://files.pythonhosted.org/packages/f3/31/7ee938abbde2322e553a2cb5f604cdd1e4728e08bba39c7ee6fae9af840b/mkdocstrings_python-2.0.2-py3-none-any.whl", hash = "sha256:31241c0f43d85a69306d704d5725786015510ea3f3c4bdfdb5a5731d83cdc2b0", size = 104900, upload-time = "2026-02-09T15:12:00.166Z" }, ] [[package]] @@ -1653,41 +1699,41 @@ wheels = [ [[package]] name = "mypy" -version = "1.19.0" +version = "1.19.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "librt" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f9/b5/b58cdc25fadd424552804bf410855d52324183112aa004f0732c5f6324cf/mypy-1.19.0.tar.gz", hash = "sha256:f6b874ca77f733222641e5c46e4711648c4037ea13646fd0cdc814c2eaec2528", size = 3579025, upload-time = "2025-11-28T15:49:01.26Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/d2/010fb171ae5ac4a01cc34fbacd7544531e5ace95c35ca166dd8fd1b901d0/mypy-1.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a31e4c28e8ddb042c84c5e977e28a21195d086aaffaf08b016b78e19c9ef8106", size = 13010563, upload-time = "2025-11-28T15:48:23.975Z" }, - { url = "https://files.pythonhosted.org/packages/41/6b/63f095c9f1ce584fdeb595d663d49e0980c735a1d2004720ccec252c5d47/mypy-1.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34ec1ac66d31644f194b7c163d7f8b8434f1b49719d403a5d26c87fff7e913f7", size = 12077037, upload-time = "2025-11-28T15:47:51.582Z" }, - { url = "https://files.pythonhosted.org/packages/d7/83/6cb93d289038d809023ec20eb0b48bbb1d80af40511fa077da78af6ff7c7/mypy-1.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb64b0ba5980466a0f3f9990d1c582bcab8db12e29815ecb57f1408d99b4bff7", size = 12680255, upload-time = "2025-11-28T15:46:57.628Z" }, - { url = "https://files.pythonhosted.org/packages/99/db/d217815705987d2cbace2edd9100926196d6f85bcb9b5af05058d6e3c8ad/mypy-1.19.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:120cffe120cca5c23c03c77f84abc0c14c5d2e03736f6c312480020082f1994b", size = 13421472, upload-time = "2025-11-28T15:47:59.655Z" }, - { url = "https://files.pythonhosted.org/packages/4e/51/d2beaca7c497944b07594f3f8aad8d2f0e8fc53677059848ae5d6f4d193e/mypy-1.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7a500ab5c444268a70565e374fc803972bfd1f09545b13418a5174e29883dab7", size = 13651823, upload-time = "2025-11-28T15:45:29.318Z" }, - { url = "https://files.pythonhosted.org/packages/aa/d1/7883dcf7644db3b69490f37b51029e0870aac4a7ad34d09ceae709a3df44/mypy-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:c14a98bc63fd867530e8ec82f217dae29d0550c86e70debc9667fff1ec83284e", size = 10049077, upload-time = "2025-11-28T15:45:39.818Z" }, - { url = "https://files.pythonhosted.org/packages/11/7e/1afa8fb188b876abeaa14460dc4983f909aaacaa4bf5718c00b2c7e0b3d5/mypy-1.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0fb3115cb8fa7c5f887c8a8d81ccdcb94cff334684980d847e5a62e926910e1d", size = 13207728, upload-time = "2025-11-28T15:46:26.463Z" }, - { url = "https://files.pythonhosted.org/packages/b2/13/f103d04962bcbefb1644f5ccb235998b32c337d6c13145ea390b9da47f3e/mypy-1.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3e19e3b897562276bb331074d64c076dbdd3e79213f36eed4e592272dabd760", size = 12202945, upload-time = "2025-11-28T15:48:49.143Z" }, - { url = "https://files.pythonhosted.org/packages/e4/93/a86a5608f74a22284a8ccea8592f6e270b61f95b8588951110ad797c2ddd/mypy-1.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9d491295825182fba01b6ffe2c6fe4e5a49dbf4e2bb4d1217b6ced3b4797bc6", size = 12718673, upload-time = "2025-11-28T15:47:37.193Z" }, - { url = "https://files.pythonhosted.org/packages/3d/58/cf08fff9ced0423b858f2a7495001fda28dc058136818ee9dffc31534ea9/mypy-1.19.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6016c52ab209919b46169651b362068f632efcd5eb8ef9d1735f6f86da7853b2", size = 13608336, upload-time = "2025-11-28T15:48:32.625Z" }, - { url = "https://files.pythonhosted.org/packages/64/ed/9c509105c5a6d4b73bb08733102a3ea62c25bc02c51bca85e3134bf912d3/mypy-1.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f188dcf16483b3e59f9278c4ed939ec0254aa8a60e8fc100648d9ab5ee95a431", size = 13833174, upload-time = "2025-11-28T15:45:48.091Z" }, - { url = "https://files.pythonhosted.org/packages/cd/71/01939b66e35c6f8cb3e6fdf0b657f0fd24de2f8ba5e523625c8e72328208/mypy-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:0e3c3d1e1d62e678c339e7ade72746a9e0325de42cd2cccc51616c7b2ed1a018", size = 10112208, upload-time = "2025-11-28T15:46:41.702Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0d/a1357e6bb49e37ce26fcf7e3cc55679ce9f4ebee0cd8b6ee3a0e301a9210/mypy-1.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7686ed65dbabd24d20066f3115018d2dce030d8fa9db01aa9f0a59b6813e9f9e", size = 13191993, upload-time = "2025-11-28T15:47:22.336Z" }, - { url = "https://files.pythonhosted.org/packages/5d/75/8e5d492a879ec4490e6ba664b5154e48c46c85b5ac9785792a5ec6a4d58f/mypy-1.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4a985b2e32f23bead72e2fb4bbe5d6aceee176be471243bd831d5b2644672d", size = 12174411, upload-time = "2025-11-28T15:44:55.492Z" }, - { url = "https://files.pythonhosted.org/packages/71/31/ad5dcee9bfe226e8eaba777e9d9d251c292650130f0450a280aec3485370/mypy-1.19.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc51a5b864f73a3a182584b1ac75c404396a17eced54341629d8bdcb644a5bba", size = 12727751, upload-time = "2025-11-28T15:44:14.169Z" }, - { url = "https://files.pythonhosted.org/packages/77/06/b6b8994ce07405f6039701f4b66e9d23f499d0b41c6dd46ec28f96d57ec3/mypy-1.19.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37af5166f9475872034b56c5efdcf65ee25394e9e1d172907b84577120714364", size = 13593323, upload-time = "2025-11-28T15:46:34.699Z" }, - { url = "https://files.pythonhosted.org/packages/68/b1/126e274484cccdf099a8e328d4fda1c7bdb98a5e888fa6010b00e1bbf330/mypy-1.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:510c014b722308c9bd377993bcbf9a07d7e0692e5fa8fc70e639c1eb19fc6bee", size = 13818032, upload-time = "2025-11-28T15:46:18.286Z" }, - { url = "https://files.pythonhosted.org/packages/f8/56/53a8f70f562dfc466c766469133a8a4909f6c0012d83993143f2a9d48d2d/mypy-1.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:cabbee74f29aa9cd3b444ec2f1e4fa5a9d0d746ce7567a6a609e224429781f53", size = 10120644, upload-time = "2025-11-28T15:47:43.99Z" }, - { url = "https://files.pythonhosted.org/packages/b0/f4/7751f32f56916f7f8c229fe902cbdba3e4dd3f3ea9e8b872be97e7fc546d/mypy-1.19.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f2e36bed3c6d9b5f35d28b63ca4b727cb0228e480826ffc8953d1892ddc8999d", size = 13185236, upload-time = "2025-11-28T15:45:20.696Z" }, - { url = "https://files.pythonhosted.org/packages/35/31/871a9531f09e78e8d145032355890384f8a5b38c95a2c7732d226b93242e/mypy-1.19.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a18d8abdda14035c5718acb748faec09571432811af129bf0d9e7b2d6699bf18", size = 12213902, upload-time = "2025-11-28T15:46:10.117Z" }, - { url = "https://files.pythonhosted.org/packages/58/b8/af221910dd40eeefa2077a59107e611550167b9994693fc5926a0b0f87c0/mypy-1.19.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75e60aca3723a23511948539b0d7ed514dda194bc3755eae0bfc7a6b4887aa7", size = 12738600, upload-time = "2025-11-28T15:44:22.521Z" }, - { url = "https://files.pythonhosted.org/packages/11/9f/c39e89a3e319c1d9c734dedec1183b2cc3aefbab066ec611619002abb932/mypy-1.19.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f44f2ae3c58421ee05fe609160343c25f70e3967f6e32792b5a78006a9d850f", size = 13592639, upload-time = "2025-11-28T15:48:08.55Z" }, - { url = "https://files.pythonhosted.org/packages/97/6d/ffaf5f01f5e284d9033de1267e6c1b8f3783f2cf784465378a86122e884b/mypy-1.19.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63ea6a00e4bd6822adbfc75b02ab3653a17c02c4347f5bb0cf1d5b9df3a05835", size = 13799132, upload-time = "2025-11-28T15:47:06.032Z" }, - { url = "https://files.pythonhosted.org/packages/fe/b0/c33921e73aaa0106224e5a34822411bea38046188eb781637f5a5b07e269/mypy-1.19.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ad925b14a0bb99821ff6f734553294aa6a3440a8cb082fe1f5b84dfb662afb1", size = 10269832, upload-time = "2025-11-28T15:47:29.392Z" }, - { url = "https://files.pythonhosted.org/packages/09/0e/fe228ed5aeab470c6f4eb82481837fadb642a5aa95cc8215fd2214822c10/mypy-1.19.0-py3-none-any.whl", hash = "sha256:0c01c99d626380752e527d5ce8e69ffbba2046eb8a060db0329690849cf9b6f9", size = 2469714, upload-time = "2025-11-28T15:45:33.22Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, ] [[package]] @@ -1701,97 +1747,95 @@ wheels = [ [[package]] name = "nodeenv" -version = "1.9.1" +version = "1.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] [[package]] name = "numpy" -version = "2.3.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641, upload-time = "2025-11-16T22:49:19.336Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218", size = 12528324, upload-time = "2025-11-16T22:49:22.582Z" }, - { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d", size = 5356872, upload-time = "2025-11-16T22:49:25.408Z" }, - { url = "https://files.pythonhosted.org/packages/5c/bb/35ef04afd567f4c989c2060cde39211e4ac5357155c1833bcd1166055c61/numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5", size = 6893148, upload-time = "2025-11-16T22:49:27.549Z" }, - { url = "https://files.pythonhosted.org/packages/f2/2b/05bbeb06e2dff5eab512dfc678b1cc5ee94d8ac5956a0885c64b6b26252b/numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7", size = 14557282, upload-time = "2025-11-16T22:49:30.964Z" }, - { url = "https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4", size = 16897903, upload-time = "2025-11-16T22:49:34.191Z" }, - { url = "https://files.pythonhosted.org/packages/ac/14/085f4cf05fc3f1e8aa95e85404e984ffca9b2275a5dc2b1aae18a67538b8/numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e", size = 16341672, upload-time = "2025-11-16T22:49:37.2Z" }, - { url = "https://files.pythonhosted.org/packages/6f/3b/1f73994904142b2aa290449b3bb99772477b5fd94d787093e4f24f5af763/numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748", size = 18838896, upload-time = "2025-11-16T22:49:39.727Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b9/cf6649b2124f288309ffc353070792caf42ad69047dcc60da85ee85fea58/numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c", size = 6563608, upload-time = "2025-11-16T22:49:42.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c", size = 13078442, upload-time = "2025-11-16T22:49:43.99Z" }, - { url = "https://files.pythonhosted.org/packages/6d/a7/f99a41553d2da82a20a2f22e93c94f928e4490bb447c9ff3c4ff230581d3/numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa", size = 10458555, upload-time = "2025-11-16T22:49:47.092Z" }, - { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" }, - { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" }, - { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" }, - { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" }, - { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" }, - { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" }, - { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" }, - { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" }, - { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" }, - { url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" }, - { url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" }, - { url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" }, - { url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" }, - { url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" }, - { url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" }, - { url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" }, - { url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" }, - { url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" }, - { url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" }, - { url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" }, - { url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" }, - { url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" }, - { url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" }, - { url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" }, - { url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" }, - { url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" }, - { url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" }, - { url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" }, - { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" }, - { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" }, - { url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706, upload-time = "2025-11-16T22:51:19.558Z" }, - { url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507, upload-time = "2025-11-16T22:51:22.492Z" }, - { url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049, upload-time = "2025-11-16T22:51:25.171Z" }, - { url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603, upload-time = "2025-11-16T22:51:27Z" }, - { url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696, upload-time = "2025-11-16T22:51:29.402Z" }, - { url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350, upload-time = "2025-11-16T22:51:32.167Z" }, - { url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190, upload-time = "2025-11-16T22:51:35.403Z" }, - { url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749, upload-time = "2025-11-16T22:51:39.698Z" }, - { url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432, upload-time = "2025-11-16T22:51:42.476Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388, upload-time = "2025-11-16T22:51:45.275Z" }, - { url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651, upload-time = "2025-11-16T22:51:47.749Z" }, - { url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503, upload-time = "2025-11-16T22:51:50.443Z" }, - { url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612, upload-time = "2025-11-16T22:51:53.609Z" }, - { url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042, upload-time = "2025-11-16T22:51:56.213Z" }, - { url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502, upload-time = "2025-11-16T22:51:58.584Z" }, - { url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962, upload-time = "2025-11-16T22:52:01.698Z" }, - { url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054, upload-time = "2025-11-16T22:52:04.267Z" }, - { url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613, upload-time = "2025-11-16T22:52:08.651Z" }, - { url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147, upload-time = "2025-11-16T22:52:11.453Z" }, - { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" }, - { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" }, - { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" }, - { url = "https://files.pythonhosted.org/packages/c6/65/f9dea8e109371ade9c782b4e4756a82edf9d3366bca495d84d79859a0b79/numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310", size = 16910689, upload-time = "2025-11-16T22:52:23.247Z" }, - { url = "https://files.pythonhosted.org/packages/00/4f/edb00032a8fb92ec0a679d3830368355da91a69cab6f3e9c21b64d0bb986/numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c", size = 12457053, upload-time = "2025-11-16T22:52:26.367Z" }, - { url = "https://files.pythonhosted.org/packages/16/a4/e8a53b5abd500a63836a29ebe145fc1ab1f2eefe1cfe59276020373ae0aa/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18", size = 5285635, upload-time = "2025-11-16T22:52:29.266Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2f/37eeb9014d9c8b3e9c55bc599c68263ca44fdbc12a93e45a21d1d56df737/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff", size = 6801770, upload-time = "2025-11-16T22:52:31.421Z" }, - { url = "https://files.pythonhosted.org/packages/7d/e4/68d2f474df2cb671b2b6c2986a02e520671295647dad82484cde80ca427b/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb", size = 14391768, upload-time = "2025-11-16T22:52:33.593Z" }, - { url = "https://files.pythonhosted.org/packages/b8/50/94ccd8a2b141cb50651fddd4f6a48874acb3c91c8f0842b08a6afc4b0b21/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7", size = 16729263, upload-time = "2025-11-16T22:52:36.369Z" }, - { url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213, upload-time = "2025-11-16T22:52:39.38Z" }, +version = "2.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478, upload-time = "2026-01-31T23:10:25.623Z" }, + { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467, upload-time = "2026-01-31T23:10:28.186Z" }, + { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172, upload-time = "2026-01-31T23:10:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145, upload-time = "2026-01-31T23:10:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084, upload-time = "2026-01-31T23:10:34.502Z" }, + { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477, upload-time = "2026-01-31T23:10:37.075Z" }, + { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429, upload-time = "2026-01-31T23:10:39.704Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109, upload-time = "2026-01-31T23:10:41.924Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915, upload-time = "2026-01-31T23:10:45.26Z" }, + { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972, upload-time = "2026-01-31T23:10:47.021Z" }, + { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763, upload-time = "2026-01-31T23:10:50.087Z" }, + { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, + { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, + { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, + { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, + { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, + { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" }, + { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" }, + { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" }, + { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" }, + { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" }, + { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" }, + { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" }, + { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" }, + { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" }, + { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" }, + { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" }, + { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" }, + { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" }, + { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" }, + { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" }, + { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" }, + { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" }, + { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" }, + { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" }, + { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179, upload-time = "2026-01-31T23:12:53.5Z" }, + { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755, upload-time = "2026-01-31T23:12:55.933Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500, upload-time = "2026-01-31T23:12:58.671Z" }, + { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252, upload-time = "2026-01-31T23:13:00.518Z" }, + { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142, upload-time = "2026-01-31T23:13:02.219Z" }, + { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979, upload-time = "2026-01-31T23:13:04.62Z" }, + { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577, upload-time = "2026-01-31T23:13:07.08Z" }, ] [[package]] name = "openai" -version = "2.15.0" +version = "2.21.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1803,9 +1847,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/f4/4690ecb5d70023ce6bfcfeabfe717020f654bde59a775058ec6ac4692463/openai-2.15.0.tar.gz", hash = "sha256:42eb8cbb407d84770633f31bf727d4ffb4138711c670565a41663d9439174fba", size = 627383, upload-time = "2026-01-09T22:10:08.603Z" } +sdist = { url = "https://files.pythonhosted.org/packages/92/e5/3d197a0947a166649f566706d7a4c8f7fe38f1fa7b24c9bcffe4c7591d44/openai-2.21.0.tar.gz", hash = "sha256:81b48ce4b8bbb2cc3af02047ceb19561f7b1dc0d4e52d1de7f02abfd15aa59b7", size = 644374, upload-time = "2026-02-14T00:12:01.577Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/df/c306f7375d42bafb379934c2df4c2fa3964656c8c782bac75ee10c102818/openai-2.15.0-py3-none-any.whl", hash = "sha256:6ae23b932cd7230f7244e52954daa6602716d6b9bf235401a107af731baea6c3", size = 1067879, upload-time = "2026-01-09T22:10:06.446Z" }, + { url = "https://files.pythonhosted.org/packages/cc/56/0a89092a453bb2c676d66abee44f863e742b2110d4dbb1dbcca3f7e5fc33/openai-2.21.0-py3-none-any.whl", hash = "sha256:0bc1c775e5b1536c294eded39ee08f8407656537ccc71b1004104fe1602e267c", size = 1103065, upload-time = "2026-02-14T00:11:59.603Z" }, ] [[package]] @@ -1822,11 +1866,11 @@ wheels = [ [[package]] name = "packaging" -version = "25.0" +version = "26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] @@ -1840,65 +1884,71 @@ wheels = [ [[package]] name = "pandas" -version = "2.3.3" +version = "3.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, { name = "python-dateutil" }, - { name = "pytz" }, - { name = "tzdata" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, - { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, - { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, - { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, - { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, - { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, - { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, - { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, - { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, - { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, - { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, - { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, - { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, - { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, - { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, - { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, - { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, - { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, - { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, - { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, - { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, - { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, - { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, - { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, - { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, - { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, - { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, - { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, - { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, - { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, - { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, - { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, - { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, - { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, - { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, - { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, - { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/da/b1dc0481ab8d55d0f46e343cfe67d4551a0e14fcee52bd38ca1bd73258d8/pandas-3.0.0.tar.gz", hash = "sha256:0facf7e87d38f721f0af46fe70d97373a37701b1c09f7ed7aeeb292ade5c050f", size = 4633005, upload-time = "2026-01-21T15:52:04.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/1e/b184654a856e75e975a6ee95d6577b51c271cd92cb2b020c9378f53e0032/pandas-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d64ce01eb9cdca96a15266aa679ae50212ec52757c79204dbc7701a222401850", size = 10313247, upload-time = "2026-01-21T15:50:15.775Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5e/e04a547ad0f0183bf151fd7c7a477468e3b85ff2ad231c566389e6cc9587/pandas-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:613e13426069793aa1ec53bdcc3b86e8d32071daea138bbcf4fa959c9cdaa2e2", size = 9913131, upload-time = "2026-01-21T15:50:18.611Z" }, + { url = "https://files.pythonhosted.org/packages/a2/93/bb77bfa9fc2aba9f7204db807d5d3fb69832ed2854c60ba91b4c65ba9219/pandas-3.0.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0192fee1f1a8e743b464a6607858ee4b071deb0b118eb143d71c2a1d170996d5", size = 10741925, upload-time = "2026-01-21T15:50:21.058Z" }, + { url = "https://files.pythonhosted.org/packages/62/fb/89319812eb1d714bfc04b7f177895caeba8ab4a37ef6712db75ed786e2e0/pandas-3.0.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0b853319dec8d5e0c8b875374c078ef17f2269986a78168d9bd57e49bf650ae", size = 11245979, upload-time = "2026-01-21T15:50:23.413Z" }, + { url = "https://files.pythonhosted.org/packages/a9/63/684120486f541fc88da3862ed31165b3b3e12b6a1c7b93be4597bc84e26c/pandas-3.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:707a9a877a876c326ae2cb640fbdc4ef63b0a7b9e2ef55c6df9942dcee8e2af9", size = 11756337, upload-time = "2026-01-21T15:50:25.932Z" }, + { url = "https://files.pythonhosted.org/packages/39/92/7eb0ad232312b59aec61550c3c81ad0743898d10af5df7f80bc5e5065416/pandas-3.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:afd0aa3d0b5cda6e0b8ffc10dbcca3b09ef3cbcd3fe2b27364f85fdc04e1989d", size = 12325517, upload-time = "2026-01-21T15:50:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/51/27/bf9436dd0a4fc3130acec0828951c7ef96a0631969613a9a35744baf27f6/pandas-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:113b4cca2614ff7e5b9fee9b6f066618fe73c5a83e99d721ffc41217b2bf57dd", size = 9881576, upload-time = "2026-01-21T15:50:30.149Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2b/c618b871fce0159fd107516336e82891b404e3f340821853c2fc28c7830f/pandas-3.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c14837eba8e99a8da1527c0280bba29b0eb842f64aa94982c5e21227966e164b", size = 9140807, upload-time = "2026-01-21T15:50:32.308Z" }, + { url = "https://files.pythonhosted.org/packages/0b/38/db33686f4b5fa64d7af40d96361f6a4615b8c6c8f1b3d334eee46ae6160e/pandas-3.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9803b31f5039b3c3b10cc858c5e40054adb4b29b4d81cb2fd789f4121c8efbcd", size = 10334013, upload-time = "2026-01-21T15:50:34.771Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7b/9254310594e9774906bacdd4e732415e1f86ab7dbb4b377ef9ede58cd8ec/pandas-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14c2a4099cd38a1d18ff108168ea417909b2dea3bd1ebff2ccf28ddb6a74d740", size = 9874154, upload-time = "2026-01-21T15:50:36.67Z" }, + { url = "https://files.pythonhosted.org/packages/63/d4/726c5a67a13bc66643e66d2e9ff115cead482a44fc56991d0c4014f15aaf/pandas-3.0.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d257699b9a9960e6125686098d5714ac59d05222bef7a5e6af7a7fd87c650801", size = 10384433, upload-time = "2026-01-21T15:50:39.132Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2e/9211f09bedb04f9832122942de8b051804b31a39cfbad199a819bb88d9f3/pandas-3.0.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69780c98f286076dcafca38d8b8eee1676adf220199c0a39f0ecbf976b68151a", size = 10864519, upload-time = "2026-01-21T15:50:41.043Z" }, + { url = "https://files.pythonhosted.org/packages/00/8d/50858522cdc46ac88b9afdc3015e298959a70a08cd21e008a44e9520180c/pandas-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4a66384f017240f3858a4c8a7cf21b0591c3ac885cddb7758a589f0f71e87ebb", size = 11394124, upload-time = "2026-01-21T15:50:43.377Z" }, + { url = "https://files.pythonhosted.org/packages/86/3f/83b2577db02503cd93d8e95b0f794ad9d4be0ba7cb6c8bcdcac964a34a42/pandas-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be8c515c9bc33989d97b89db66ea0cececb0f6e3c2a87fcc8b69443a6923e95f", size = 11920444, upload-time = "2026-01-21T15:50:45.932Z" }, + { url = "https://files.pythonhosted.org/packages/64/2d/4f8a2f192ed12c90a0aab47f5557ece0e56b0370c49de9454a09de7381b2/pandas-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:a453aad8c4f4e9f166436994a33884442ea62aa8b27d007311e87521b97246e1", size = 9730970, upload-time = "2026-01-21T15:50:47.962Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/ff571be435cf1e643ca98d0945d76732c0b4e9c37191a89c8550b105eed1/pandas-3.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:da768007b5a33057f6d9053563d6b74dd6d029c337d93c6d0d22a763a5c2ecc0", size = 9041950, upload-time = "2026-01-21T15:50:50.422Z" }, + { url = "https://files.pythonhosted.org/packages/6f/fa/7f0ac4ca8877c57537aaff2a842f8760e630d8e824b730eb2e859ffe96ca/pandas-3.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b78d646249b9a2bc191040988c7bb524c92fa8534fb0898a0741d7e6f2ffafa6", size = 10307129, upload-time = "2026-01-21T15:50:52.877Z" }, + { url = "https://files.pythonhosted.org/packages/6f/11/28a221815dcea4c0c9414dfc845e34a84a6a7dabc6da3194498ed5ba4361/pandas-3.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bc9cba7b355cb4162442a88ce495e01cb605f17ac1e27d6596ac963504e0305f", size = 9850201, upload-time = "2026-01-21T15:50:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/ba/da/53bbc8c5363b7e5bd10f9ae59ab250fc7a382ea6ba08e4d06d8694370354/pandas-3.0.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c9a1a149aed3b6c9bf246033ff91e1b02d529546c5d6fb6b74a28fea0cf4c70", size = 10354031, upload-time = "2026-01-21T15:50:57.463Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a3/51e02ebc2a14974170d51e2410dfdab58870ea9bcd37cda15bd553d24dc4/pandas-3.0.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95683af6175d884ee89471842acfca29172a85031fccdabc35e50c0984470a0e", size = 10861165, upload-time = "2026-01-21T15:50:59.32Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fe/05a51e3cac11d161472b8297bd41723ea98013384dd6d76d115ce3482f9b/pandas-3.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1fbbb5a7288719e36b76b4f18d46ede46e7f916b6c8d9915b756b0a6c3f792b3", size = 11359359, upload-time = "2026-01-21T15:51:02.014Z" }, + { url = "https://files.pythonhosted.org/packages/ee/56/ba620583225f9b85a4d3e69c01df3e3870659cc525f67929b60e9f21dcd1/pandas-3.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e8b9808590fa364416b49b2a35c1f4cf2785a6c156935879e57f826df22038e", size = 11912907, upload-time = "2026-01-21T15:51:05.175Z" }, + { url = "https://files.pythonhosted.org/packages/c9/8c/c6638d9f67e45e07656b3826405c5cc5f57f6fd07c8b2572ade328c86e22/pandas-3.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:98212a38a709feb90ae658cb6227ea3657c22ba8157d4b8f913cd4c950de5e7e", size = 9732138, upload-time = "2026-01-21T15:51:07.569Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bf/bd1335c3bf1770b6d8fed2799993b11c4971af93bb1b729b9ebbc02ca2ec/pandas-3.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:177d9df10b3f43b70307a149d7ec49a1229a653f907aa60a48f1877d0e6be3be", size = 9033568, upload-time = "2026-01-21T15:51:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/8e/c6/f5e2171914d5e29b9171d495344097d54e3ffe41d2d85d8115baba4dc483/pandas-3.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2713810ad3806767b89ad3b7b69ba153e1c6ff6d9c20f9c2140379b2a98b6c98", size = 10741936, upload-time = "2026-01-21T15:51:11.693Z" }, + { url = "https://files.pythonhosted.org/packages/51/88/9a0164f99510a1acb9f548691f022c756c2314aad0d8330a24616c14c462/pandas-3.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:15d59f885ee5011daf8335dff47dcb8a912a27b4ad7826dc6cbe809fd145d327", size = 10393884, upload-time = "2026-01-21T15:51:14.197Z" }, + { url = "https://files.pythonhosted.org/packages/e0/53/b34d78084d88d8ae2b848591229da8826d1e65aacf00b3abe34023467648/pandas-3.0.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24e6547fb64d2c92665dd2adbfa4e85fa4fd70a9c070e7cfb03b629a0bbab5eb", size = 10310740, upload-time = "2026-01-21T15:51:16.093Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d3/bee792e7c3d6930b74468d990604325701412e55d7aaf47460a22311d1a5/pandas-3.0.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48ee04b90e2505c693d3f8e8f524dab8cb8aaf7ddcab52c92afa535e717c4812", size = 10700014, upload-time = "2026-01-21T15:51:18.818Z" }, + { url = "https://files.pythonhosted.org/packages/55/db/2570bc40fb13aaed1cbc3fbd725c3a60ee162477982123c3adc8971e7ac1/pandas-3.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66f72fb172959af42a459e27a8d8d2c7e311ff4c1f7db6deb3b643dbc382ae08", size = 11323737, upload-time = "2026-01-21T15:51:20.784Z" }, + { url = "https://files.pythonhosted.org/packages/bc/2e/297ac7f21c8181b62a4cccebad0a70caf679adf3ae5e83cb676194c8acc3/pandas-3.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4a4a400ca18230976724a5066f20878af785f36c6756e498e94c2a5e5d57779c", size = 11771558, upload-time = "2026-01-21T15:51:22.977Z" }, + { url = "https://files.pythonhosted.org/packages/0a/46/e1c6876d71c14332be70239acce9ad435975a80541086e5ffba2f249bcf6/pandas-3.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:940eebffe55528074341a5a36515f3e4c5e25e958ebbc764c9502cfc35ba3faa", size = 10473771, upload-time = "2026-01-21T15:51:25.285Z" }, + { url = "https://files.pythonhosted.org/packages/c0/db/0270ad9d13c344b7a36fa77f5f8344a46501abf413803e885d22864d10bf/pandas-3.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:597c08fb9fef0edf1e4fa2f9828dd27f3d78f9b8c9b4a748d435ffc55732310b", size = 10312075, upload-time = "2026-01-21T15:51:28.5Z" }, + { url = "https://files.pythonhosted.org/packages/09/9f/c176f5e9717f7c91becfe0f55a52ae445d3f7326b4a2cf355978c51b7913/pandas-3.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:447b2d68ac5edcbf94655fe909113a6dba6ef09ad7f9f60c80477825b6c489fe", size = 9900213, upload-time = "2026-01-21T15:51:30.955Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e7/63ad4cc10b257b143e0a5ebb04304ad806b4e1a61c5da25f55896d2ca0f4/pandas-3.0.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:debb95c77ff3ed3ba0d9aa20c3a2f19165cc7956362f9873fce1ba0a53819d70", size = 10428768, upload-time = "2026-01-21T15:51:33.018Z" }, + { url = "https://files.pythonhosted.org/packages/9e/0e/4e4c2d8210f20149fd2248ef3fff26623604922bd564d915f935a06dd63d/pandas-3.0.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fedabf175e7cd82b69b74c30adbaa616de301291a5231138d7242596fc296a8d", size = 10882954, upload-time = "2026-01-21T15:51:35.287Z" }, + { url = "https://files.pythonhosted.org/packages/c6/60/c9de8ac906ba1f4d2250f8a951abe5135b404227a55858a75ad26f84db47/pandas-3.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:412d1a89aab46889f3033a386912efcdfa0f1131c5705ff5b668dda88305e986", size = 11430293, upload-time = "2026-01-21T15:51:37.57Z" }, + { url = "https://files.pythonhosted.org/packages/a1/69/806e6637c70920e5787a6d6896fd707f8134c2c55cd761e7249a97b7dc5a/pandas-3.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e979d22316f9350c516479dd3a92252be2937a9531ed3a26ec324198a99cdd49", size = 11952452, upload-time = "2026-01-21T15:51:39.618Z" }, + { url = "https://files.pythonhosted.org/packages/cb/de/918621e46af55164c400ab0ef389c9d969ab85a43d59ad1207d4ddbe30a5/pandas-3.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:083b11415b9970b6e7888800c43c82e81a06cd6b06755d84804444f0007d6bb7", size = 9851081, upload-time = "2026-01-21T15:51:41.758Z" }, + { url = "https://files.pythonhosted.org/packages/91/a1/3562a18dd0bd8c73344bfa26ff90c53c72f827df119d6d6b1dacc84d13e3/pandas-3.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:5db1e62cb99e739fa78a28047e861b256d17f88463c76b8dafc7c1338086dca8", size = 9174610, upload-time = "2026-01-21T15:51:44.312Z" }, + { url = "https://files.pythonhosted.org/packages/ce/26/430d91257eaf366f1737d7a1c158677caaf6267f338ec74e3a1ec444111c/pandas-3.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:697b8f7d346c68274b1b93a170a70974cdc7d7354429894d5927c1effdcccd73", size = 10761999, upload-time = "2026-01-21T15:51:46.899Z" }, + { url = "https://files.pythonhosted.org/packages/ec/1a/954eb47736c2b7f7fe6a9d56b0cb6987773c00faa3c6451a43db4beb3254/pandas-3.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8cb3120f0d9467ed95e77f67a75e030b67545bcfa08964e349252d674171def2", size = 10410279, upload-time = "2026-01-21T15:51:48.89Z" }, + { url = "https://files.pythonhosted.org/packages/20/fc/b96f3a5a28b250cd1b366eb0108df2501c0f38314a00847242abab71bb3a/pandas-3.0.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33fd3e6baa72899746b820c31e4b9688c8e1b7864d7aec2de7ab5035c285277a", size = 10330198, upload-time = "2026-01-21T15:51:51.015Z" }, + { url = "https://files.pythonhosted.org/packages/90/b3/d0e2952f103b4fbef1ef22d0c2e314e74fc9064b51cee30890b5e3286ee6/pandas-3.0.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8942e333dc67ceda1095227ad0febb05a3b36535e520154085db632c40ad084", size = 10728513, upload-time = "2026-01-21T15:51:53.387Z" }, + { url = "https://files.pythonhosted.org/packages/76/81/832894f286df828993dc5fd61c63b231b0fb73377e99f6c6c369174cf97e/pandas-3.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:783ac35c4d0fe0effdb0d67161859078618b1b6587a1af15928137525217a721", size = 11345550, upload-time = "2026-01-21T15:51:55.329Z" }, + { url = "https://files.pythonhosted.org/packages/34/a0/ed160a00fb4f37d806406bc0a79a8b62fe67f29d00950f8d16203ff3409b/pandas-3.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:125eb901e233f155b268bbef9abd9afb5819db74f0e677e89a61b246228c71ac", size = 11799386, upload-time = "2026-01-21T15:51:57.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/c8/2ac00d7255252c5e3cf61b35ca92ca25704b0188f7454ca4aec08a33cece/pandas-3.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b86d113b6c109df3ce0ad5abbc259fe86a1bd4adfd4a31a89da42f84f65509bb", size = 10873041, upload-time = "2026-01-21T15:52:00.034Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3f/a80ac00acbc6b35166b42850e98a4f466e2c0d9c64054161ba9620f95680/pandas-3.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1c39eab3ad38f2d7a249095f0a3d8f8c22cc0f847e98ccf5bbe732b272e2d9fa", size = 9441003, upload-time = "2026-01-21T15:52:02.281Z" }, ] [[package]] name = "pathspec" -version = "0.12.1" +version = "1.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] [[package]] @@ -1990,11 +2040,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.5.0" +version = "4.9.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, + { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, ] [[package]] @@ -2008,7 +2058,7 @@ wheels = [ [[package]] name = "pre-commit" -version = "4.5.0" +version = "4.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, @@ -2017,9 +2067,9 @@ dependencies = [ { name = "pyyaml" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/9b/6a4ffb4ed980519da959e1cf3122fc6cb41211daa58dbae1c73c0e519a37/pre_commit-4.5.0.tar.gz", hash = "sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b", size = 198428, upload-time = "2025-11-22T21:02:42.304Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429, upload-time = "2025-11-22T21:02:40.836Z" }, + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, ] [[package]] @@ -2160,16 +2210,16 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.12.0" +version = "2.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/a1/ae859ffac5a3338a66b74c5e29e244fd3a3cc483c89feaf9f56c39898d75/pydantic_settings-2.13.0.tar.gz", hash = "sha256:95d875514610e8595672800a5c40b073e99e4aae467fa7c8f9c263061ea2e1fe", size = 222450, upload-time = "2026-02-15T12:11:23.476Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1a/dd1b9d7e627486cf8e7523d09b70010e05a4bc41414f4ae6ce184cf0afb6/pydantic_settings-2.13.0-py3-none-any.whl", hash = "sha256:d67b576fff39cd086b595441bf9c75d4193ca9c0ed643b90360694d0f1240246", size = 58429, upload-time = "2026-02-15T12:11:22.133Z" }, ] [[package]] @@ -2183,11 +2233,11 @@ wheels = [ [[package]] name = "pyjwt" -version = "2.10.1" +version = "2.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, + { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, ] [package.optional-dependencies] @@ -2197,30 +2247,30 @@ crypto = [ [[package]] name = "pymdown-extensions" -version = "10.17.2" +version = "10.21" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/6d/af5378dbdb379fddd9a277f8b9888c027db480cde70028669ebd009d642a/pymdown_extensions-10.17.2.tar.gz", hash = "sha256:26bb3d7688e651606260c90fb46409fbda70bf9fdc3623c7868643a1aeee4713", size = 847344, upload-time = "2025-11-26T15:43:57.004Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/63/06673d1eb6d8f83c0ea1f677d770e12565fb516928b4109c9e2055656a9e/pymdown_extensions-10.21.tar.gz", hash = "sha256:39f4a020f40773f6b2ff31d2cd2546c2c04d0a6498c31d9c688d2be07e1767d5", size = 853363, upload-time = "2026-02-15T20:44:06.748Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/78/b93cb80bd673bdc9f6ede63d8eb5b4646366953df15667eb3603be57a2b1/pymdown_extensions-10.17.2-py3-none-any.whl", hash = "sha256:bffae79a2e8b9e44aef0d813583a8fea63457b7a23643a43988055b7b79b4992", size = 266556, upload-time = "2025-11-26T15:43:55.162Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl", hash = "sha256:91b879f9f864d49794c2d9534372b10150e6141096c3908a455e45ca72ad9d3f", size = 268877, upload-time = "2026-02-15T20:44:05.464Z" }, ] [[package]] name = "pymupdf" -version = "1.26.7" +version = "1.27.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/48/d6/09b28f027b510838559f7748807192149c419b30cb90e6d5f0cf916dc9dc/pymupdf-1.26.7.tar.gz", hash = "sha256:71add8bdc8eb1aaa207c69a13400693f06ad9b927bea976f5d5ab9df0bb489c3", size = 84327033, upload-time = "2025-12-11T21:48:50.694Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/0c/40dda0cc4bd2220a2ef75f8c53dd7d8ed1e29681fcb3df75db6ee9677a7e/pymupdf-1.27.1.tar.gz", hash = "sha256:4afbde0769c336717a149ab0de3330dcb75378f795c1a8c5af55c1a628b17d55", size = 85303479, upload-time = "2026-02-12T08:29:17.682Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/35/cd74cea1787b2247702ef8522186bdef32e9cb30a099e6bb864627ef6045/pymupdf-1.26.7-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:07085718dfdae5ab83b05eb5eb397f863bcc538fe05135318a01ea353e7a1353", size = 23179369, upload-time = "2025-12-11T21:47:21.587Z" }, - { url = "https://files.pythonhosted.org/packages/72/74/448b6172927c829c6a3fba80078d7b0a016ebbe2c9ee528821f5ea21677a/pymupdf-1.26.7-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:31aa9c8377ea1eea02934b92f4dcf79fb2abba0bf41f8a46d64c3e31546a3c02", size = 22470101, upload-time = "2025-12-11T21:47:37.105Z" }, - { url = "https://files.pythonhosted.org/packages/65/e7/47af26f3ac76be7ac3dd4d6cc7ee105948a8355d774e5ca39857bf91c11c/pymupdf-1.26.7-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e419b609996434a14a80fa060adec72c434a1cca6a511ec54db9841bc5d51b3c", size = 23502486, upload-time = "2025-12-12T09:51:25.824Z" }, - { url = "https://files.pythonhosted.org/packages/2a/6b/3de1714d734ff949be1e90a22375d0598d3540b22ae73eb85c2d7d1f36a9/pymupdf-1.26.7-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:69dfc78f206a96e5b3ac22741263ebab945fdf51f0dbe7c5757c3511b23d9d72", size = 24115727, upload-time = "2025-12-11T21:47:51.274Z" }, - { url = "https://files.pythonhosted.org/packages/62/9b/f86224847949577a523be2207315ae0fd3155b5d909cd66c274d095349a3/pymupdf-1.26.7-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1d5106f46e1ca0d64d46bd51892372a4f82076bdc14a9678d33d630702abca36", size = 24324386, upload-time = "2025-12-12T14:58:45.483Z" }, - { url = "https://files.pythonhosted.org/packages/85/8e/a117d39092ca645fde8b903f4a941d9aa75b370a67b4f1f435f56393dc5a/pymupdf-1.26.7-cp310-abi3-win32.whl", hash = "sha256:7c9645b6f5452629c747690190350213d3e5bbdb6b2eca227d82702b327f6eee", size = 17203888, upload-time = "2025-12-12T13:59:57.613Z" }, - { url = "https://files.pythonhosted.org/packages/dd/c3/d0047678146c294469c33bae167c8ace337deafb736b0bf97b9bc481aa65/pymupdf-1.26.7-cp310-abi3-win_amd64.whl", hash = "sha256:425b1befe40d41b72eb0fe211711c7ae334db5eb60307e9dd09066ed060cceba", size = 18405952, upload-time = "2025-12-11T21:48:02.947Z" }, + { url = "https://files.pythonhosted.org/packages/13/19/fde6ea4712a904b65e8f41124a0e4233879b87a770fe6a8ce857964de6d5/pymupdf-1.27.1-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:bee9f95512f9556dbf2cacfd1413c61b29a55baa07fa7f8fc83d221d8419888a", size = 23986707, upload-time = "2026-02-11T15:03:24.025Z" }, + { url = "https://files.pythonhosted.org/packages/75/c2/070dff91ad3f1bc16fd6c6ceff23495601fcce4c92d28be534417596418a/pymupdf-1.27.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:3de95a0889395b0966fafd11b94980b7543a816e89dd1c218597a08543ac3415", size = 23263493, upload-time = "2026-02-11T15:03:45.528Z" }, + { url = "https://files.pythonhosted.org/packages/8e/db/937377f4b3e0fbf6273c17436a49f7db17df1a46b1be9e26653b6fafc0e1/pymupdf-1.27.1-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2c9d9353b840040cbc724341f4095fb7e2cc1a12a9147d0ec1a0a79f5d773147", size = 24317651, upload-time = "2026-02-11T22:33:38.967Z" }, + { url = "https://files.pythonhosted.org/packages/72/d5/c701cf2d0cdd6e5d6bca3ca9188d7f5d7ce3ae67dd1368d658cd4bae2707/pymupdf-1.27.1-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:aeaed76e72cbc061149a825ab0811c5f4752970c56591c2938c5042ec06b26e1", size = 24945742, upload-time = "2026-02-11T15:04:06.21Z" }, + { url = "https://files.pythonhosted.org/packages/2b/29/690202b38b93cf77b73a29c25a63a2b6f3fcb36b1f75006e50b8dee7c108/pymupdf-1.27.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4f1837554134fb45d390a44de8844b2ca9b6c901c82ccc90b340e3b7f3b126ca", size = 25167965, upload-time = "2026-02-11T22:36:35.478Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/f937e6aa606fd263c3a45d0ff0f0bbdbf3fb779933091fc0f6179513cc93/pymupdf-1.27.1-cp310-abi3-win32.whl", hash = "sha256:fa33b512d82c6c4852edadf57f22d5f27d16243bb33dac0fbe4eb0f281c5b17e", size = 18006253, upload-time = "2026-02-12T13:48:07.129Z" }, + { url = "https://files.pythonhosted.org/packages/3e/99/fe4a7752990bf65277718fffbead4478de9afd1c7288d7a6d643f79a6fa7/pymupdf-1.27.1-cp310-abi3-win_amd64.whl", hash = "sha256:4b6268dff3a9d713034eba5c2ffce0da37c62443578941ac5df433adcde57b2f", size = 19236703, upload-time = "2026-02-11T15:04:19.607Z" }, ] [[package]] @@ -2234,27 +2284,36 @@ wheels = [ [[package]] name = "pypdfium2" -version = "5.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/87/56782107fa242137b77ccddc30519bbb33e7a9eed9da9649d9db45db2c64/pypdfium2-5.1.0.tar.gz", hash = "sha256:46335ca30a1584b804a6824da84d2e846b4b954bdfc342d035b7bf15ed9a14e5", size = 270104, upload-time = "2025-11-23T13:36:52.589Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/d7/46ce255322cd29f0db3772667a0da3db8ed137e1e9b9aa306ac5691765b3/pypdfium2-5.1.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f3dde94d320d582d3c20255b600f1e7e03261bfdea139b7064b54126fc3db4e2", size = 2817789, upload-time = "2025-11-23T13:36:31.423Z" }, - { url = "https://files.pythonhosted.org/packages/19/a5/4ad3c1b336fdc2b7a88d835c56bcd64ce60d4a95d1a9eaafc44f853da582/pypdfium2-5.1.0-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:dee09b7a3ab1860a17decc97c179a5aaba5a74b2780d53c91daa18d742945892", size = 2940861, upload-time = "2025-11-23T13:36:33.519Z" }, - { url = "https://files.pythonhosted.org/packages/19/93/d13ca66d5e075d7e27736c51c15955cdd3266ac0a8327613c3c520d43693/pypdfium2-5.1.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1757d6470cbf5b8d1c825350df2ccd79fd0bfcf5753ff566fd02153a486014b1", size = 2980933, upload-time = "2025-11-23T13:36:35.283Z" }, - { url = "https://files.pythonhosted.org/packages/a2/7c/02744ef9e0363af08f9ed47c0e603ef8713e02d4a48492c76d5bf36f65c3/pypdfium2-5.1.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad18e95497423f88b33f2976cb78c27f0bd6ef4b4bf340c901f5f28a234c4f06", size = 2762960, upload-time = "2025-11-23T13:36:37.033Z" }, - { url = "https://files.pythonhosted.org/packages/89/26/f0abcfccb99b0a5c4451b70b0e72ccb7c27387931af01eae982870272202/pypdfium2-5.1.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2faee2f4fbd5bd33dd77c07d15ccaa6687562d883a54c4beb8329ebaee615b7d", size = 3060522, upload-time = "2025-11-23T13:36:38.835Z" }, - { url = "https://files.pythonhosted.org/packages/2f/74/92f508e71178aa85de32454762f84d6f9cef35c468caab3e0f1041dae464/pypdfium2-5.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d688372df169a9dad606c1e5ad34b6e0e6b820f1e0d540b4780711600a7bf8dd", size = 2995178, upload-time = "2025-11-23T13:36:40.319Z" }, - { url = "https://files.pythonhosted.org/packages/94/9f/91ca099ea64b24e19ef05da72e33d0ef0840e104d89cbdcb618da12629b5/pypdfium2-5.1.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:cfecd2b20f1c05027aaa2af6bfbcc2835b4c8f6455155b0dc2800ec6a2051965", size = 6321704, upload-time = "2025-11-23T13:36:42.177Z" }, - { url = "https://files.pythonhosted.org/packages/e0/4b/5628cfda9f534b3acc1e2cf50f9e9582cd9cfd86cf2ce718da229de6e709/pypdfium2-5.1.0-py3-none-musllinux_1_1_i686.whl", hash = "sha256:5698de8e6d662f1b2cdff5cb62e6f0ee79ffaaa13e282251854cbc64cf712449", size = 6329892, upload-time = "2025-11-23T13:36:43.757Z" }, - { url = "https://files.pythonhosted.org/packages/c5/25/5d2db765f8f82129d75ea2883ed26af3d1a64d8daaa20a11005ac681e2c3/pypdfium2-5.1.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd73093fbb1710ea1164cdf27583363e1b663b8cc22d555c84af0ee1af50c7", size = 6409889, upload-time = "2025-11-23T13:36:45.387Z" }, - { url = "https://files.pythonhosted.org/packages/89/d3/135ed8ca46044cd5005cd104ead13bea417777afa65d7af5a710eb68d340/pypdfium2-5.1.0-py3-none-win32.whl", hash = "sha256:11d319cd2e5f71cdc3d68e8a79142b559a0edbcc16fe31d4036fcfc45f0e9ed8", size = 2991546, upload-time = "2025-11-23T13:36:47.373Z" }, - { url = "https://files.pythonhosted.org/packages/52/8f/884a1b2fd7c747a98e9b4c95097c08b39d042a88837ac72f2945a7f6162c/pypdfium2-5.1.0-py3-none-win_amd64.whl", hash = "sha256:4725f347a8c9ff011a7035d8267ee25912ab1b946034ba0b57f3cca89de8847a", size = 3100176, upload-time = "2025-11-23T13:36:49.234Z" }, - { url = "https://files.pythonhosted.org/packages/d7/5c/72448636ea0ccd44878f77bb5d59a2c967a54eec806ee2e0d894ef0d2434/pypdfium2-5.1.0-py3-none-win_arm64.whl", hash = "sha256:47c5593f7eb6ae0f1e5a940d712d733ede580f09ca91de6c3f89611848695c0f", size = 2941500, upload-time = "2025-11-23T13:36:50.69Z" }, +version = "5.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/23/b3979a1d4f536fabce02e3d9f332e8aeeed064d9df9391f2a77160f4ab36/pypdfium2-5.4.0.tar.gz", hash = "sha256:7219e55048fb3999fc8adcaea467088507207df4676ff9e521a3ae15a67d99c4", size = 269136, upload-time = "2026-02-08T16:54:08.383Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/c0/3d707bff5e973272b5412556d19e8c6889ce859a235465f0049cc8d35bc3/pypdfium2-5.4.0-py3-none-android_23_arm64_v8a.whl", hash = "sha256:8bc51a12a8c8eabbdbd7499d3e5ec47bcf56ba18e07b52bdd07d321cc1252c90", size = 2759769, upload-time = "2026-02-08T16:53:32.985Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6b/306cafcb0b18d5fab41687d9ed76eabea86a9ff78bc568bee1bfa34e526d/pypdfium2-5.4.0-py3-none-android_23_armeabi_v7a.whl", hash = "sha256:a414ef5b685824cc6c7acbe19b7dbc735de2023cf473321a8ebfe8d7f5d8a41f", size = 2301913, upload-time = "2026-02-08T16:53:35.026Z" }, + { url = "https://files.pythonhosted.org/packages/7a/37/3d737c7eb84fb22939ab0a643aa0183dbc0745c309e962b4d61eeff8211b/pypdfium2-5.4.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0e83657db8da5971434ff5683bf3faa007ee1f3a56b61f245b8aa5b60442c23a", size = 2814181, upload-time = "2026-02-08T16:53:36.481Z" }, + { url = "https://files.pythonhosted.org/packages/96/d7/0895737ec3d95ad607ade42e98fa8868b91e35b1170ec39b8c1b5fdb124c/pypdfium2-5.4.0-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:e42b1d14db642e96bb3a57167f620b4247e9c843d22b9fb569b16a7c35a18f47", size = 2943476, upload-time = "2026-02-08T16:53:37.992Z" }, + { url = "https://files.pythonhosted.org/packages/9a/53/f8ab449997d3efa52737b8e6c494f1c3f09dc0642161fadc934f16a57cf0/pypdfium2-5.4.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0698c9a002f839127e74ec0185147e08b64e47a1e6caeaee95df434c05b26e8c", size = 2976675, upload-time = "2026-02-08T16:53:39.923Z" }, + { url = "https://files.pythonhosted.org/packages/c6/28/b8a4d4c1557019101bb722c88ba532ec9c14640117ab1c272c80774d83d7/pypdfium2-5.4.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:22e9d4c73fc48b18b022977ea6fe78df43adf95440e1135020ed35fea9595017", size = 2762396, upload-time = "2026-02-08T16:53:41.958Z" }, + { url = "https://files.pythonhosted.org/packages/0b/4a/6c765f6e0b69d792e2d4c7ef2359301896c82df265d60f9a56e87618ec50/pypdfium2-5.4.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f0619f8a8ae3eb71b2cdc1fbd2a8f5d43f0fc6bee66d1b3aac2c9c23e44a3bf", size = 3068559, upload-time = "2026-02-08T16:53:43.974Z" }, + { url = "https://files.pythonhosted.org/packages/1c/17/4464e4ab6dd98ac3783c10eb799d8da49cb551a769c987eb9c6ba72a5ccf/pypdfium2-5.4.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50124415d815c41de8ce7e21cee5450f74f6f1240a140573bb71ccac804d5e5f", size = 3419384, upload-time = "2026-02-08T16:53:46.041Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/fa315a2ab353b41501b7088be72dc6cf8ad2bd4f1ebdfdb90c41b7f29155/pypdfium2-5.4.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce482d76e5447e745d761307401eaa366616ca44032b86cf7fbe6be918ade64e", size = 2998123, upload-time = "2026-02-08T16:53:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/a171d313d54a028d9437dea2c5d07fc9e1592f4daf5c39cbf514fca75242/pypdfium2-5.4.0-py3-none-manylinux_2_27_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16b9c6b07f3dbe7eda209bf7aaf131ca9614e1dae527e9764180dd58bcbaf411", size = 3673594, upload-time = "2026-02-08T16:53:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c0/60416f011f7e5a4ca29f40ae94907f34975239f3c6dd7fcb51f99e110f3b/pypdfium2-5.4.0-py3-none-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3b08d48b7cca3b51aefaad7855bc0e9e251432a6eef1356d532ff438be84855e", size = 2965025, upload-time = "2026-02-08T16:53:50.553Z" }, + { url = "https://files.pythonhosted.org/packages/75/e2/8e36144b5e933c707b6aeab7dc6638eee8208697925b48b5b78ef68fb52a/pypdfium2-5.4.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0a1526e2a2bde7f2f13bec0f471d9fd475f7bbac2c0c860d48c35af8394d5931", size = 4130551, upload-time = "2026-02-08T16:53:52.71Z" }, + { url = "https://files.pythonhosted.org/packages/a0/64/8cda96259a8fdecd457f5d14a9d650315d7bdf496f96055d1d55900b3881/pypdfium2-5.4.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:40cea0bceb1e60a71b3855e2b04d175d2199b7da06212bb80f0c78067d065810", size = 3746587, upload-time = "2026-02-08T16:53:54.219Z" }, + { url = "https://files.pythonhosted.org/packages/33/6b/7764491269f188a922bd6b254359d718899fc3092c90f0f68c2f6e451921/pypdfium2-5.4.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7a116f8fbeae7aa3a18ff2d1fa331ac647831cc16b589d4fbbbb66d64ecc8793", size = 4336703, upload-time = "2026-02-08T16:53:56.18Z" }, + { url = "https://files.pythonhosted.org/packages/87/b0/2484bd3c20ead51ecea2082deaf94a3e91bad709fa14f049ca7fb598dc9a/pypdfium2-5.4.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:55c7fc894718db5fa2981d46dee45fe3a4fcd60d26f5095ad8f7779600fa8b6f", size = 4375051, upload-time = "2026-02-08T16:53:57.804Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ac/5f0536be885c3cadc09422de0324a193a21c165488a574029d9d2db92ecb/pypdfium2-5.4.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:dfc1c0c7e6e7ba258ebb338aaf664eb933bff1854cda76e4ee530886ea39b31a", size = 3928935, upload-time = "2026-02-08T16:53:59.265Z" }, + { url = "https://files.pythonhosted.org/packages/13/b9/693b665df0939555491bece0777cafda1270e208734e925006de313abb5b/pypdfium2-5.4.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:4c0a48ede7180f804c029c509c2b6ea0c66813a3fde9eb9afc390183f947164d", size = 4997642, upload-time = "2026-02-08T16:54:00.809Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ea/ba585acdfbefe309ee2fe5ebfeb097e36abe1d33c2a5108828c493c070bb/pypdfium2-5.4.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dea22d15c44a275702fd95ad664ba6eaa3c493d53d58b4d69272a04bdfb0df70", size = 4179914, upload-time = "2026-02-08T16:54:02.264Z" }, + { url = "https://files.pythonhosted.org/packages/97/47/238383e89081a0ed1ca2bf4ef44f7e512fa0c72ffc51adc7df83bfcfd9b9/pypdfium2-5.4.0-py3-none-win32.whl", hash = "sha256:35c643827ed0f4dae9cedf3caf836f94cba5b31bd2c115b80a7c85f004636de9", size = 2995844, upload-time = "2026-02-08T16:54:03.692Z" }, + { url = "https://files.pythonhosted.org/packages/08/37/f1338a0600c6c6e31759f8f80d7ab20aa0bc43b11594da67091300e051d4/pypdfium2-5.4.0-py3-none-win_amd64.whl", hash = "sha256:f9d9ce3c6901294d6984004d4a797dea110f8248b1bde33a823d25b45d3c2685", size = 3104198, upload-time = "2026-02-08T16:54:05.304Z" }, + { url = "https://files.pythonhosted.org/packages/65/17/18ad82f070da18ab970928f730fbd44d9b05aafcb52a2ebb6470eaae53f9/pypdfium2-5.4.0-py3-none-win_arm64.whl", hash = "sha256:2b78ea216fb92e7709b61c46241ebf2cc0c60cf18ad2fb4633af665d7b4e21e6", size = 2938727, upload-time = "2026-02-08T16:54:06.814Z" }, ] [[package]] name = "pytest" -version = "9.0.1" +version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -2263,9 +2322,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] [[package]] @@ -2333,15 +2392,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/a4/2f2def0378b44f913d2d6cb3bc5b1a15267b363937ab1cb9afb07ce2313c/python_toon-0.1.3-py3-none-any.whl", hash = "sha256:a27b0ee4a729e730d1037d0a63eb8b344b3e5a26e3dc9a173067b6c31a868ee6", size = 21797, upload-time = "2025-11-04T09:12:19.443Z" }, ] -[[package]] -name = "pytz" -version = "2025.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, -] - [[package]] name = "pywin32" version = "311" @@ -2472,15 +2522,15 @@ wheels = [ [[package]] name = "rich" -version = "14.2.0" +version = "14.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, + { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" }, ] [[package]] @@ -2593,112 +2643,111 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ed/d9/f7a0c4b3a2bf2556cd5d99b05372c29980249ef71e8e32669ba77428c82c/ruff-0.14.8.tar.gz", hash = "sha256:774ed0dd87d6ce925e3b8496feb3a00ac564bea52b9feb551ecd17e0a23d1eed", size = 5765385, upload-time = "2025-12-04T15:06:17.669Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/b8/9537b52010134b1d2b72870cc3f92d5fb759394094741b09ceccae183fbe/ruff-0.14.8-py3-none-linux_armv6l.whl", hash = "sha256:ec071e9c82eca417f6111fd39f7043acb53cd3fde9b1f95bbed745962e345afb", size = 13441540, upload-time = "2025-12-04T15:06:14.896Z" }, - { url = "https://files.pythonhosted.org/packages/24/00/99031684efb025829713682012b6dd37279b1f695ed1b01725f85fd94b38/ruff-0.14.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8cdb162a7159f4ca36ce980a18c43d8f036966e7f73f866ac8f493b75e0c27e9", size = 13669384, upload-time = "2025-12-04T15:06:51.809Z" }, - { url = "https://files.pythonhosted.org/packages/72/64/3eb5949169fc19c50c04f28ece2c189d3b6edd57e5b533649dae6ca484fe/ruff-0.14.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e2fcbefe91f9fad0916850edf0854530c15bd1926b6b779de47e9ab619ea38f", size = 12806917, upload-time = "2025-12-04T15:06:08.925Z" }, - { url = "https://files.pythonhosted.org/packages/c4/08/5250babb0b1b11910f470370ec0cbc67470231f7cdc033cee57d4976f941/ruff-0.14.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9d70721066a296f45786ec31916dc287b44040f553da21564de0ab4d45a869b", size = 13256112, upload-time = "2025-12-04T15:06:23.498Z" }, - { url = "https://files.pythonhosted.org/packages/78/4c/6c588e97a8e8c2d4b522c31a579e1df2b4d003eddfbe23d1f262b1a431ff/ruff-0.14.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c87e09b3cd9d126fc67a9ecd3b5b1d3ded2b9c7fce3f16e315346b9d05cfb52", size = 13227559, upload-time = "2025-12-04T15:06:33.432Z" }, - { url = "https://files.pythonhosted.org/packages/23/ce/5f78cea13eda8eceac71b5f6fa6e9223df9b87bb2c1891c166d1f0dce9f1/ruff-0.14.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d62cb310c4fbcb9ee4ac023fe17f984ae1e12b8a4a02e3d21489f9a2a5f730c", size = 13896379, upload-time = "2025-12-04T15:06:02.687Z" }, - { url = "https://files.pythonhosted.org/packages/cf/79/13de4517c4dadce9218a20035b21212a4c180e009507731f0d3b3f5df85a/ruff-0.14.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1af35c2d62633d4da0521178e8a2641c636d2a7153da0bac1b30cfd4ccd91344", size = 15372786, upload-time = "2025-12-04T15:06:29.828Z" }, - { url = "https://files.pythonhosted.org/packages/00/06/33df72b3bb42be8a1c3815fd4fae83fa2945fc725a25d87ba3e42d1cc108/ruff-0.14.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25add4575ffecc53d60eed3f24b1e934493631b48ebbc6ebaf9d8517924aca4b", size = 14990029, upload-time = "2025-12-04T15:06:36.812Z" }, - { url = "https://files.pythonhosted.org/packages/64/61/0f34927bd90925880394de0e081ce1afab66d7b3525336f5771dcf0cb46c/ruff-0.14.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c943d847b7f02f7db4201a0600ea7d244d8a404fbb639b439e987edcf2baf9a", size = 14407037, upload-time = "2025-12-04T15:06:39.979Z" }, - { url = "https://files.pythonhosted.org/packages/96/bc/058fe0aefc0fbf0d19614cb6d1a3e2c048f7dc77ca64957f33b12cfdc5ef/ruff-0.14.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb6e8bf7b4f627548daa1b69283dac5a296bfe9ce856703b03130732e20ddfe2", size = 14102390, upload-time = "2025-12-04T15:06:46.372Z" }, - { url = "https://files.pythonhosted.org/packages/af/a4/e4f77b02b804546f4c17e8b37a524c27012dd6ff05855d2243b49a7d3cb9/ruff-0.14.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7aaf2974f378e6b01d1e257c6948207aec6a9b5ba53fab23d0182efb887a0e4a", size = 14230793, upload-time = "2025-12-04T15:06:20.497Z" }, - { url = "https://files.pythonhosted.org/packages/3f/52/bb8c02373f79552e8d087cedaffad76b8892033d2876c2498a2582f09dcf/ruff-0.14.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e5758ca513c43ad8a4ef13f0f081f80f08008f410790f3611a21a92421ab045b", size = 13160039, upload-time = "2025-12-04T15:06:49.06Z" }, - { url = "https://files.pythonhosted.org/packages/1f/ad/b69d6962e477842e25c0b11622548df746290cc6d76f9e0f4ed7456c2c31/ruff-0.14.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f74f7ba163b6e85a8d81a590363bf71618847e5078d90827749bfda1d88c9cdf", size = 13205158, upload-time = "2025-12-04T15:06:54.574Z" }, - { url = "https://files.pythonhosted.org/packages/06/63/54f23da1315c0b3dfc1bc03fbc34e10378918a20c0b0f086418734e57e74/ruff-0.14.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eed28f6fafcc9591994c42254f5a5c5ca40e69a30721d2ab18bb0bb3baac3ab6", size = 13469550, upload-time = "2025-12-04T15:05:59.209Z" }, - { url = "https://files.pythonhosted.org/packages/70/7d/a4d7b1961e4903bc37fffb7ddcfaa7beb250f67d97cfd1ee1d5cddb1ec90/ruff-0.14.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:21d48fa744c9d1cb8d71eb0a740c4dd02751a5de9db9a730a8ef75ca34cf138e", size = 14211332, upload-time = "2025-12-04T15:06:06.027Z" }, - { url = "https://files.pythonhosted.org/packages/5d/93/2a5063341fa17054e5c86582136e9895db773e3c2ffb770dde50a09f35f0/ruff-0.14.8-py3-none-win32.whl", hash = "sha256:15f04cb45c051159baebb0f0037f404f1dc2f15a927418f29730f411a79bc4e7", size = 13151890, upload-time = "2025-12-04T15:06:11.668Z" }, - { url = "https://files.pythonhosted.org/packages/02/1c/65c61a0859c0add13a3e1cbb6024b42de587456a43006ca2d4fd3d1618fe/ruff-0.14.8-py3-none-win_amd64.whl", hash = "sha256:9eeb0b24242b5bbff3011409a739929f497f3fb5fe3b5698aba5e77e8c833097", size = 14537826, upload-time = "2025-12-04T15:06:26.409Z" }, - { url = "https://files.pythonhosted.org/packages/6d/63/8b41cea3afd7f58eb64ac9251668ee0073789a3bc9ac6f816c8c6fef986d/ruff-0.14.8-py3-none-win_arm64.whl", hash = "sha256:965a582c93c63fe715fd3e3f8aa37c4b776777203d8e1d8aa3cc0c14424a4b99", size = 13634522, upload-time = "2025-12-04T15:06:43.212Z" }, +version = "0.15.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" }, + { url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" }, + { url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" }, + { url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" }, + { url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" }, + { url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" }, + { url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" }, + { url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" }, + { url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" }, + { url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" }, + { url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" }, ] [[package]] name = "scipy" -version = "1.16.3" +version = "1.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/5f/6f37d7439de1455ce9c5a556b8d1db0979f03a796c030bafdf08d35b7bf9/scipy-1.16.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:40be6cf99e68b6c4321e9f8782e7d5ff8265af28ef2cd56e9c9b2638fa08ad97", size = 36630881, upload-time = "2025-10-28T17:31:47.104Z" }, - { url = "https://files.pythonhosted.org/packages/7c/89/d70e9f628749b7e4db2aa4cd89735502ff3f08f7b9b27d2e799485987cd9/scipy-1.16.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:8be1ca9170fcb6223cc7c27f4305d680ded114a1567c0bd2bfcbf947d1b17511", size = 28941012, upload-time = "2025-10-28T17:31:53.411Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a8/0e7a9a6872a923505dbdf6bb93451edcac120363131c19013044a1e7cb0c/scipy-1.16.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bea0a62734d20d67608660f69dcda23e7f90fb4ca20974ab80b6ed40df87a005", size = 20931935, upload-time = "2025-10-28T17:31:57.361Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c7/020fb72bd79ad798e4dbe53938543ecb96b3a9ac3fe274b7189e23e27353/scipy-1.16.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:2a207a6ce9c24f1951241f4693ede2d393f59c07abc159b2cb2be980820e01fb", size = 23534466, upload-time = "2025-10-28T17:32:01.875Z" }, - { url = "https://files.pythonhosted.org/packages/be/a0/668c4609ce6dbf2f948e167836ccaf897f95fb63fa231c87da7558a374cd/scipy-1.16.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:532fb5ad6a87e9e9cd9c959b106b73145a03f04c7d57ea3e6f6bb60b86ab0876", size = 33593618, upload-time = "2025-10-28T17:32:06.902Z" }, - { url = "https://files.pythonhosted.org/packages/ca/6e/8942461cf2636cdae083e3eb72622a7fbbfa5cf559c7d13ab250a5dbdc01/scipy-1.16.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0151a0749efeaaab78711c78422d413c583b8cdd2011a3c1d6c794938ee9fdb2", size = 35899798, upload-time = "2025-10-28T17:32:12.665Z" }, - { url = "https://files.pythonhosted.org/packages/79/e8/d0f33590364cdbd67f28ce79368b373889faa4ee959588beddf6daef9abe/scipy-1.16.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7180967113560cca57418a7bc719e30366b47959dd845a93206fbed693c867e", size = 36226154, upload-time = "2025-10-28T17:32:17.961Z" }, - { url = "https://files.pythonhosted.org/packages/39/c1/1903de608c0c924a1749c590064e65810f8046e437aba6be365abc4f7557/scipy-1.16.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:deb3841c925eeddb6afc1e4e4a45e418d19ec7b87c5df177695224078e8ec733", size = 38878540, upload-time = "2025-10-28T17:32:23.907Z" }, - { url = "https://files.pythonhosted.org/packages/f1/d0/22ec7036ba0b0a35bccb7f25ab407382ed34af0b111475eb301c16f8a2e5/scipy-1.16.3-cp311-cp311-win_amd64.whl", hash = "sha256:53c3844d527213631e886621df5695d35e4f6a75f620dca412bcd292f6b87d78", size = 38722107, upload-time = "2025-10-28T17:32:29.921Z" }, - { url = "https://files.pythonhosted.org/packages/7b/60/8a00e5a524bb3bf8898db1650d350f50e6cffb9d7a491c561dc9826c7515/scipy-1.16.3-cp311-cp311-win_arm64.whl", hash = "sha256:9452781bd879b14b6f055b26643703551320aa8d79ae064a71df55c00286a184", size = 25506272, upload-time = "2025-10-28T17:32:34.577Z" }, - { url = "https://files.pythonhosted.org/packages/40/41/5bf55c3f386b1643812f3a5674edf74b26184378ef0f3e7c7a09a7e2ca7f/scipy-1.16.3-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81fc5827606858cf71446a5e98715ba0e11f0dbc83d71c7409d05486592a45d6", size = 36659043, upload-time = "2025-10-28T17:32:40.285Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0f/65582071948cfc45d43e9870bf7ca5f0e0684e165d7c9ef4e50d783073eb/scipy-1.16.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:c97176013d404c7346bf57874eaac5187d969293bf40497140b0a2b2b7482e07", size = 28898986, upload-time = "2025-10-28T17:32:45.325Z" }, - { url = "https://files.pythonhosted.org/packages/96/5e/36bf3f0ac298187d1ceadde9051177d6a4fe4d507e8f59067dc9dd39e650/scipy-1.16.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2b71d93c8a9936046866acebc915e2af2e292b883ed6e2cbe5c34beb094b82d9", size = 20889814, upload-time = "2025-10-28T17:32:49.277Z" }, - { url = "https://files.pythonhosted.org/packages/80/35/178d9d0c35394d5d5211bbff7ac4f2986c5488b59506fef9e1de13ea28d3/scipy-1.16.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3d4a07a8e785d80289dfe66b7c27d8634a773020742ec7187b85ccc4b0e7b686", size = 23565795, upload-time = "2025-10-28T17:32:53.337Z" }, - { url = "https://files.pythonhosted.org/packages/fa/46/d1146ff536d034d02f83c8afc3c4bab2eddb634624d6529a8512f3afc9da/scipy-1.16.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0553371015692a898e1aa858fed67a3576c34edefa6b7ebdb4e9dde49ce5c203", size = 33349476, upload-time = "2025-10-28T17:32:58.353Z" }, - { url = "https://files.pythonhosted.org/packages/79/2e/415119c9ab3e62249e18c2b082c07aff907a273741b3f8160414b0e9193c/scipy-1.16.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72d1717fd3b5e6ec747327ce9bda32d5463f472c9dce9f54499e81fbd50245a1", size = 35676692, upload-time = "2025-10-28T17:33:03.88Z" }, - { url = "https://files.pythonhosted.org/packages/27/82/df26e44da78bf8d2aeaf7566082260cfa15955a5a6e96e6a29935b64132f/scipy-1.16.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fb2472e72e24d1530debe6ae078db70fb1605350c88a3d14bc401d6306dbffe", size = 36019345, upload-time = "2025-10-28T17:33:09.773Z" }, - { url = "https://files.pythonhosted.org/packages/82/31/006cbb4b648ba379a95c87262c2855cd0d09453e500937f78b30f02fa1cd/scipy-1.16.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5192722cffe15f9329a3948c4b1db789fbb1f05c97899187dcf009b283aea70", size = 38678975, upload-time = "2025-10-28T17:33:15.809Z" }, - { url = "https://files.pythonhosted.org/packages/c2/7f/acbd28c97e990b421af7d6d6cd416358c9c293fc958b8529e0bd5d2a2a19/scipy-1.16.3-cp312-cp312-win_amd64.whl", hash = "sha256:56edc65510d1331dae01ef9b658d428e33ed48b4f77b1d51caf479a0253f96dc", size = 38555926, upload-time = "2025-10-28T17:33:21.388Z" }, - { url = "https://files.pythonhosted.org/packages/ce/69/c5c7807fd007dad4f48e0a5f2153038dc96e8725d3345b9ee31b2b7bed46/scipy-1.16.3-cp312-cp312-win_arm64.whl", hash = "sha256:a8a26c78ef223d3e30920ef759e25625a0ecdd0d60e5a8818b7513c3e5384cf2", size = 25463014, upload-time = "2025-10-28T17:33:25.975Z" }, - { url = "https://files.pythonhosted.org/packages/72/f1/57e8327ab1508272029e27eeef34f2302ffc156b69e7e233e906c2a5c379/scipy-1.16.3-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:d2ec56337675e61b312179a1ad124f5f570c00f920cc75e1000025451b88241c", size = 36617856, upload-time = "2025-10-28T17:33:31.375Z" }, - { url = "https://files.pythonhosted.org/packages/44/13/7e63cfba8a7452eb756306aa2fd9b37a29a323b672b964b4fdeded9a3f21/scipy-1.16.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:16b8bc35a4cc24db80a0ec836a9286d0e31b2503cb2fd7ff7fb0e0374a97081d", size = 28874306, upload-time = "2025-10-28T17:33:36.516Z" }, - { url = "https://files.pythonhosted.org/packages/15/65/3a9400efd0228a176e6ec3454b1fa998fbbb5a8defa1672c3f65706987db/scipy-1.16.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:5803c5fadd29de0cf27fa08ccbfe7a9e5d741bf63e4ab1085437266f12460ff9", size = 20865371, upload-time = "2025-10-28T17:33:42.094Z" }, - { url = "https://files.pythonhosted.org/packages/33/d7/eda09adf009a9fb81827194d4dd02d2e4bc752cef16737cc4ef065234031/scipy-1.16.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:b81c27fc41954319a943d43b20e07c40bdcd3ff7cf013f4fb86286faefe546c4", size = 23524877, upload-time = "2025-10-28T17:33:48.483Z" }, - { url = "https://files.pythonhosted.org/packages/7d/6b/3f911e1ebc364cb81320223a3422aab7d26c9c7973109a9cd0f27c64c6c0/scipy-1.16.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0c3b4dd3d9b08dbce0f3440032c52e9e2ab9f96ade2d3943313dfe51a7056959", size = 33342103, upload-time = "2025-10-28T17:33:56.495Z" }, - { url = "https://files.pythonhosted.org/packages/21/f6/4bfb5695d8941e5c570a04d9fcd0d36bce7511b7d78e6e75c8f9791f82d0/scipy-1.16.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7dc1360c06535ea6116a2220f760ae572db9f661aba2d88074fe30ec2aa1ff88", size = 35697297, upload-time = "2025-10-28T17:34:04.722Z" }, - { url = "https://files.pythonhosted.org/packages/04/e1/6496dadbc80d8d896ff72511ecfe2316b50313bfc3ebf07a3f580f08bd8c/scipy-1.16.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:663b8d66a8748051c3ee9c96465fb417509315b99c71550fda2591d7dd634234", size = 36021756, upload-time = "2025-10-28T17:34:13.482Z" }, - { url = "https://files.pythonhosted.org/packages/fe/bd/a8c7799e0136b987bda3e1b23d155bcb31aec68a4a472554df5f0937eef7/scipy-1.16.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eab43fae33a0c39006a88096cd7b4f4ef545ea0447d250d5ac18202d40b6611d", size = 38696566, upload-time = "2025-10-28T17:34:22.384Z" }, - { url = "https://files.pythonhosted.org/packages/cd/01/1204382461fcbfeb05b6161b594f4007e78b6eba9b375382f79153172b4d/scipy-1.16.3-cp313-cp313-win_amd64.whl", hash = "sha256:062246acacbe9f8210de8e751b16fc37458213f124bef161a5a02c7a39284304", size = 38529877, upload-time = "2025-10-28T17:35:51.076Z" }, - { url = "https://files.pythonhosted.org/packages/7f/14/9d9fbcaa1260a94f4bb5b64ba9213ceb5d03cd88841fe9fd1ffd47a45b73/scipy-1.16.3-cp313-cp313-win_arm64.whl", hash = "sha256:50a3dbf286dbc7d84f176f9a1574c705f277cb6565069f88f60db9eafdbe3ee2", size = 25455366, upload-time = "2025-10-28T17:35:59.014Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a3/9ec205bd49f42d45d77f1730dbad9ccf146244c1647605cf834b3a8c4f36/scipy-1.16.3-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:fb4b29f4cf8cc5a8d628bc8d8e26d12d7278cd1f219f22698a378c3d67db5e4b", size = 37027931, upload-time = "2025-10-28T17:34:31.451Z" }, - { url = "https://files.pythonhosted.org/packages/25/06/ca9fd1f3a4589cbd825b1447e5db3a8ebb969c1eaf22c8579bd286f51b6d/scipy-1.16.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:8d09d72dc92742988b0e7750bddb8060b0c7079606c0d24a8cc8e9c9c11f9079", size = 29400081, upload-time = "2025-10-28T17:34:39.087Z" }, - { url = "https://files.pythonhosted.org/packages/6a/56/933e68210d92657d93fb0e381683bc0e53a965048d7358ff5fbf9e6a1b17/scipy-1.16.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:03192a35e661470197556de24e7cb1330d84b35b94ead65c46ad6f16f6b28f2a", size = 21391244, upload-time = "2025-10-28T17:34:45.234Z" }, - { url = "https://files.pythonhosted.org/packages/a8/7e/779845db03dc1418e215726329674b40576879b91814568757ff0014ad65/scipy-1.16.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:57d01cb6f85e34f0946b33caa66e892aae072b64b034183f3d87c4025802a119", size = 23929753, upload-time = "2025-10-28T17:34:51.793Z" }, - { url = "https://files.pythonhosted.org/packages/4c/4b/f756cf8161d5365dcdef9e5f460ab226c068211030a175d2fc7f3f41ca64/scipy-1.16.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:96491a6a54e995f00a28a3c3badfff58fd093bf26cd5fb34a2188c8c756a3a2c", size = 33496912, upload-time = "2025-10-28T17:34:59.8Z" }, - { url = "https://files.pythonhosted.org/packages/09/b5/222b1e49a58668f23839ca1542a6322bb095ab8d6590d4f71723869a6c2c/scipy-1.16.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cd13e354df9938598af2be05822c323e97132d5e6306b83a3b4ee6724c6e522e", size = 35802371, upload-time = "2025-10-28T17:35:08.173Z" }, - { url = "https://files.pythonhosted.org/packages/c1/8d/5964ef68bb31829bde27611f8c9deeac13764589fe74a75390242b64ca44/scipy-1.16.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63d3cdacb8a824a295191a723ee5e4ea7768ca5ca5f2838532d9f2e2b3ce2135", size = 36190477, upload-time = "2025-10-28T17:35:16.7Z" }, - { url = "https://files.pythonhosted.org/packages/ab/f2/b31d75cb9b5fa4dd39a0a931ee9b33e7f6f36f23be5ef560bf72e0f92f32/scipy-1.16.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e7efa2681ea410b10dde31a52b18b0154d66f2485328830e45fdf183af5aefc6", size = 38796678, upload-time = "2025-10-28T17:35:26.354Z" }, - { url = "https://files.pythonhosted.org/packages/b4/1e/b3723d8ff64ab548c38d87055483714fefe6ee20e0189b62352b5e015bb1/scipy-1.16.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2d1ae2cf0c350e7705168ff2429962a89ad90c2d49d1dd300686d8b2a5af22fc", size = 38640178, upload-time = "2025-10-28T17:35:35.304Z" }, - { url = "https://files.pythonhosted.org/packages/8e/f3/d854ff38789aca9b0cc23008d607ced9de4f7ab14fa1ca4329f86b3758ca/scipy-1.16.3-cp313-cp313t-win_arm64.whl", hash = "sha256:0c623a54f7b79dd88ef56da19bc2873afec9673a48f3b85b18e4d402bdd29a5a", size = 25803246, upload-time = "2025-10-28T17:35:42.155Z" }, - { url = "https://files.pythonhosted.org/packages/99/f6/99b10fd70f2d864c1e29a28bbcaa0c6340f9d8518396542d9ea3b4aaae15/scipy-1.16.3-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:875555ce62743e1d54f06cdf22c1e0bc47b91130ac40fe5d783b6dfa114beeb6", size = 36606469, upload-time = "2025-10-28T17:36:08.741Z" }, - { url = "https://files.pythonhosted.org/packages/4d/74/043b54f2319f48ea940dd025779fa28ee360e6b95acb7cd188fad4391c6b/scipy-1.16.3-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:bb61878c18a470021fb515a843dc7a76961a8daceaaaa8bad1332f1bf4b54657", size = 28872043, upload-time = "2025-10-28T17:36:16.599Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e1/24b7e50cc1c4ee6ffbcb1f27fe9f4c8b40e7911675f6d2d20955f41c6348/scipy-1.16.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2622206f5559784fa5c4b53a950c3c7c1cf3e84ca1b9c4b6c03f062f289ca26", size = 20862952, upload-time = "2025-10-28T17:36:22.966Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3a/3e8c01a4d742b730df368e063787c6808597ccb38636ed821d10b39ca51b/scipy-1.16.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7f68154688c515cdb541a31ef8eb66d8cd1050605be9dcd74199cbd22ac739bc", size = 23508512, upload-time = "2025-10-28T17:36:29.731Z" }, - { url = "https://files.pythonhosted.org/packages/1f/60/c45a12b98ad591536bfe5330cb3cfe1850d7570259303563b1721564d458/scipy-1.16.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3c820ddb80029fe9f43d61b81d8b488d3ef8ca010d15122b152db77dc94c22", size = 33413639, upload-time = "2025-10-28T17:36:37.982Z" }, - { url = "https://files.pythonhosted.org/packages/71/bc/35957d88645476307e4839712642896689df442f3e53b0fa016ecf8a3357/scipy-1.16.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d3837938ae715fc0fe3c39c0202de3a8853aff22ca66781ddc2ade7554b7e2cc", size = 35704729, upload-time = "2025-10-28T17:36:46.547Z" }, - { url = "https://files.pythonhosted.org/packages/3b/15/89105e659041b1ca11c386e9995aefacd513a78493656e57789f9d9eab61/scipy-1.16.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aadd23f98f9cb069b3bd64ddc900c4d277778242e961751f77a8cb5c4b946fb0", size = 36086251, upload-time = "2025-10-28T17:36:55.161Z" }, - { url = "https://files.pythonhosted.org/packages/1a/87/c0ea673ac9c6cc50b3da2196d860273bc7389aa69b64efa8493bdd25b093/scipy-1.16.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b7c5f1bda1354d6a19bc6af73a649f8285ca63ac6b52e64e658a5a11d4d69800", size = 38716681, upload-time = "2025-10-28T17:37:04.1Z" }, - { url = "https://files.pythonhosted.org/packages/91/06/837893227b043fb9b0d13e4bd7586982d8136cb249ffb3492930dab905b8/scipy-1.16.3-cp314-cp314-win_amd64.whl", hash = "sha256:e5d42a9472e7579e473879a1990327830493a7047506d58d73fc429b84c1d49d", size = 39358423, upload-time = "2025-10-28T17:38:20.005Z" }, - { url = "https://files.pythonhosted.org/packages/95/03/28bce0355e4d34a7c034727505a02d19548549e190bedd13a721e35380b7/scipy-1.16.3-cp314-cp314-win_arm64.whl", hash = "sha256:6020470b9d00245926f2d5bb93b119ca0340f0d564eb6fbaad843eaebf9d690f", size = 26135027, upload-time = "2025-10-28T17:38:24.966Z" }, - { url = "https://files.pythonhosted.org/packages/b2/6f/69f1e2b682efe9de8fe9f91040f0cd32f13cfccba690512ba4c582b0bc29/scipy-1.16.3-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:e1d27cbcb4602680a49d787d90664fa4974063ac9d4134813332a8c53dbe667c", size = 37028379, upload-time = "2025-10-28T17:37:14.061Z" }, - { url = "https://files.pythonhosted.org/packages/7c/2d/e826f31624a5ebbab1cd93d30fd74349914753076ed0593e1d56a98c4fb4/scipy-1.16.3-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:9b9c9c07b6d56a35777a1b4cc8966118fb16cfd8daf6743867d17d36cfad2d40", size = 29400052, upload-time = "2025-10-28T17:37:21.709Z" }, - { url = "https://files.pythonhosted.org/packages/69/27/d24feb80155f41fd1f156bf144e7e049b4e2b9dd06261a242905e3bc7a03/scipy-1.16.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:3a4c460301fb2cffb7f88528f30b3127742cff583603aa7dc964a52c463b385d", size = 21391183, upload-time = "2025-10-28T17:37:29.559Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d3/1b229e433074c5738a24277eca520a2319aac7465eea7310ea6ae0e98ae2/scipy-1.16.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f667a4542cc8917af1db06366d3f78a5c8e83badd56409f94d1eac8d8d9133fa", size = 23930174, upload-time = "2025-10-28T17:37:36.306Z" }, - { url = "https://files.pythonhosted.org/packages/16/9d/d9e148b0ec680c0f042581a2be79a28a7ab66c0c4946697f9e7553ead337/scipy-1.16.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f379b54b77a597aa7ee5e697df0d66903e41b9c85a6dd7946159e356319158e8", size = 33497852, upload-time = "2025-10-28T17:37:42.228Z" }, - { url = "https://files.pythonhosted.org/packages/2f/22/4e5f7561e4f98b7bea63cf3fd7934bff1e3182e9f1626b089a679914d5c8/scipy-1.16.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4aff59800a3b7f786b70bfd6ab551001cb553244988d7d6b8299cb1ea653b353", size = 35798595, upload-time = "2025-10-28T17:37:48.102Z" }, - { url = "https://files.pythonhosted.org/packages/83/42/6644d714c179429fc7196857866f219fef25238319b650bb32dde7bf7a48/scipy-1.16.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:da7763f55885045036fabcebd80144b757d3db06ab0861415d1c3b7c69042146", size = 36186269, upload-time = "2025-10-28T17:37:53.72Z" }, - { url = "https://files.pythonhosted.org/packages/ac/70/64b4d7ca92f9cf2e6fc6aaa2eecf80bb9b6b985043a9583f32f8177ea122/scipy-1.16.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa6eea95283b2b8079b821dc11f50a17d0571c92b43e2b5b12764dc5f9b285d", size = 38802779, upload-time = "2025-10-28T17:37:59.393Z" }, - { url = "https://files.pythonhosted.org/packages/61/82/8d0e39f62764cce5ffd5284131e109f07cf8955aef9ab8ed4e3aa5e30539/scipy-1.16.3-cp314-cp314t-win_amd64.whl", hash = "sha256:d9f48cafc7ce94cf9b15c6bffdc443a81a27bf7075cf2dcd5c8b40f85d10c4e7", size = 39471128, upload-time = "2025-10-28T17:38:05.259Z" }, - { url = "https://files.pythonhosted.org/packages/64/47/a494741db7280eae6dc033510c319e34d42dd41b7ac0c7ead39354d1a2b5/scipy-1.16.3-cp314-cp314t-win_arm64.whl", hash = "sha256:21d9d6b197227a12dcbf9633320a4e34c6b0e51c57268df255a0942983bac562", size = 26464127, upload-time = "2025-10-28T17:38:11.34Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/56/3e/9cca699f3486ce6bc12ff46dc2031f1ec8eb9ccc9a320fdaf925f1417426/scipy-1.17.0.tar.gz", hash = "sha256:2591060c8e648d8b96439e111ac41fd8342fdeff1876be2e19dea3fe8930454e", size = 30396830, upload-time = "2026-01-10T21:34:23.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/4b/c89c131aa87cad2b77a54eb0fb94d633a842420fa7e919dc2f922037c3d8/scipy-1.17.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:2abd71643797bd8a106dff97894ff7869eeeb0af0f7a5ce02e4227c6a2e9d6fd", size = 31381316, upload-time = "2026-01-10T21:24:33.42Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5f/a6b38f79a07d74989224d5f11b55267714707582908a5f1ae854cf9a9b84/scipy-1.17.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:ef28d815f4d2686503e5f4f00edc387ae58dfd7a2f42e348bb53359538f01558", size = 27966760, upload-time = "2026-01-10T21:24:38.911Z" }, + { url = "https://files.pythonhosted.org/packages/c1/20/095ad24e031ee8ed3c5975954d816b8e7e2abd731e04f8be573de8740885/scipy-1.17.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:272a9f16d6bb4667e8b50d25d71eddcc2158a214df1b566319298de0939d2ab7", size = 20138701, upload-time = "2026-01-10T21:24:43.249Z" }, + { url = "https://files.pythonhosted.org/packages/89/11/4aad2b3858d0337756f3323f8960755704e530b27eb2a94386c970c32cbe/scipy-1.17.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:7204fddcbec2fe6598f1c5fdf027e9f259106d05202a959a9f1aecf036adc9f6", size = 22480574, upload-time = "2026-01-10T21:24:47.266Z" }, + { url = "https://files.pythonhosted.org/packages/85/bd/f5af70c28c6da2227e510875cadf64879855193a687fb19951f0f44cfd6b/scipy-1.17.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc02c37a5639ee67d8fb646ffded6d793c06c5622d36b35cfa8fe5ececb8f042", size = 32862414, upload-time = "2026-01-10T21:24:52.566Z" }, + { url = "https://files.pythonhosted.org/packages/ef/df/df1457c4df3826e908879fe3d76bc5b6e60aae45f4ee42539512438cfd5d/scipy-1.17.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dac97a27520d66c12a34fd90a4fe65f43766c18c0d6e1c0a80f114d2260080e4", size = 35112380, upload-time = "2026-01-10T21:24:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/5f/bb/88e2c16bd1dd4de19d80d7c5e238387182993c2fb13b4b8111e3927ad422/scipy-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb7446a39b3ae0fe8f416a9a3fdc6fba3f11c634f680f16a239c5187bc487c0", size = 34922676, upload-time = "2026-01-10T21:25:04.287Z" }, + { url = "https://files.pythonhosted.org/packages/02/ba/5120242cc735f71fc002cff0303d536af4405eb265f7c60742851e7ccfe9/scipy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:474da16199f6af66601a01546144922ce402cb17362e07d82f5a6cf8f963e449", size = 37507599, upload-time = "2026-01-10T21:25:09.851Z" }, + { url = "https://files.pythonhosted.org/packages/52/c8/08629657ac6c0da198487ce8cd3de78e02cfde42b7f34117d56a3fe249dc/scipy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:255c0da161bd7b32a6c898e7891509e8a9289f0b1c6c7d96142ee0d2b114c2ea", size = 36380284, upload-time = "2026-01-10T21:25:15.632Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4a/465f96d42c6f33ad324a40049dfd63269891db9324aa66c4a1c108c6f994/scipy-1.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:85b0ac3ad17fa3be50abd7e69d583d98792d7edc08367e01445a1e2076005379", size = 24370427, upload-time = "2026-01-10T21:25:20.514Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/7241a63e73ba5a516f1930ac8d5b44cbbfabd35ac73a2d08ca206df007c4/scipy-1.17.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:0d5018a57c24cb1dd828bcf51d7b10e65986d549f52ef5adb6b4d1ded3e32a57", size = 31364580, upload-time = "2026-01-10T21:25:25.717Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1d/5057f812d4f6adc91a20a2d6f2ebcdb517fdbc87ae3acc5633c9b97c8ba5/scipy-1.17.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:88c22af9e5d5a4f9e027e26772cc7b5922fab8bcc839edb3ae33de404feebd9e", size = 27969012, upload-time = "2026-01-10T21:25:30.921Z" }, + { url = "https://files.pythonhosted.org/packages/e3/21/f6ec556c1e3b6ec4e088da667d9987bb77cc3ab3026511f427dc8451187d/scipy-1.17.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f3cd947f20fe17013d401b64e857c6b2da83cae567adbb75b9dcba865abc66d8", size = 20140691, upload-time = "2026-01-10T21:25:34.802Z" }, + { url = "https://files.pythonhosted.org/packages/7a/fe/5e5ad04784964ba964a96f16c8d4676aa1b51357199014dce58ab7ec5670/scipy-1.17.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e8c0b331c2c1f531eb51f1b4fc9ba709521a712cce58f1aa627bc007421a5306", size = 22463015, upload-time = "2026-01-10T21:25:39.277Z" }, + { url = "https://files.pythonhosted.org/packages/4a/69/7c347e857224fcaf32a34a05183b9d8a7aca25f8f2d10b8a698b8388561a/scipy-1.17.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5194c445d0a1c7a6c1a4a4681b6b7c71baad98ff66d96b949097e7513c9d6742", size = 32724197, upload-time = "2026-01-10T21:25:44.084Z" }, + { url = "https://files.pythonhosted.org/packages/d1/fe/66d73b76d378ba8cc2fe605920c0c75092e3a65ae746e1e767d9d020a75a/scipy-1.17.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9eeb9b5f5997f75507814ed9d298ab23f62cf79f5a3ef90031b1ee2506abdb5b", size = 35009148, upload-time = "2026-01-10T21:25:50.591Z" }, + { url = "https://files.pythonhosted.org/packages/af/07/07dec27d9dc41c18d8c43c69e9e413431d20c53a0339c388bcf72f353c4b/scipy-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:40052543f7bbe921df4408f46003d6f01c6af109b9e2c8a66dd1cf6cf57f7d5d", size = 34798766, upload-time = "2026-01-10T21:25:59.41Z" }, + { url = "https://files.pythonhosted.org/packages/81/61/0470810c8a093cdacd4ba7504b8a218fd49ca070d79eca23a615f5d9a0b0/scipy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0cf46c8013fec9d3694dc572f0b54100c28405d55d3e2cb15e2895b25057996e", size = 37405953, upload-time = "2026-01-10T21:26:07.75Z" }, + { url = "https://files.pythonhosted.org/packages/92/ce/672ed546f96d5d41ae78c4b9b02006cedd0b3d6f2bf5bb76ea455c320c28/scipy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:0937a0b0d8d593a198cededd4c439a0ea216a3f36653901ea1f3e4be949056f8", size = 36328121, upload-time = "2026-01-10T21:26:16.509Z" }, + { url = "https://files.pythonhosted.org/packages/9d/21/38165845392cae67b61843a52c6455d47d0cc2a40dd495c89f4362944654/scipy-1.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:f603d8a5518c7426414d1d8f82e253e454471de682ce5e39c29adb0df1efb86b", size = 24314368, upload-time = "2026-01-10T21:26:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/0c/51/3468fdfd49387ddefee1636f5cf6d03ce603b75205bf439bbf0e62069bfd/scipy-1.17.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:65ec32f3d32dfc48c72df4291345dae4f048749bc8d5203ee0a3f347f96c5ce6", size = 31344101, upload-time = "2026-01-10T21:26:30.25Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/9406aec58268d437636069419e6977af953d1e246df941d42d3720b7277b/scipy-1.17.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:1f9586a58039d7229ce77b52f8472c972448cded5736eaf102d5658bbac4c269", size = 27950385, upload-time = "2026-01-10T21:26:36.801Z" }, + { url = "https://files.pythonhosted.org/packages/4f/98/e7342709e17afdfd1b26b56ae499ef4939b45a23a00e471dfb5375eea205/scipy-1.17.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9fad7d3578c877d606b1150135c2639e9de9cecd3705caa37b66862977cc3e72", size = 20122115, upload-time = "2026-01-10T21:26:42.107Z" }, + { url = "https://files.pythonhosted.org/packages/fd/0e/9eeeb5357a64fd157cbe0302c213517c541cc16b8486d82de251f3c68ede/scipy-1.17.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:423ca1f6584fc03936972b5f7c06961670dbba9f234e71676a7c7ccf938a0d61", size = 22442402, upload-time = "2026-01-10T21:26:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c9/10/be13397a0e434f98e0c79552b2b584ae5bb1c8b2be95db421533bbca5369/scipy-1.17.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe508b5690e9eaaa9467fc047f833af58f1152ae51a0d0aed67aa5801f4dd7d6", size = 32696338, upload-time = "2026-01-10T21:26:55.521Z" }, + { url = "https://files.pythonhosted.org/packages/63/1e/12fbf2a3bb240161651c94bb5cdd0eae5d4e8cc6eaeceb74ab07b12a753d/scipy-1.17.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6680f2dfd4f6182e7d6db161344537da644d1cf85cf293f015c60a17ecf08752", size = 34977201, upload-time = "2026-01-10T21:27:03.501Z" }, + { url = "https://files.pythonhosted.org/packages/19/5b/1a63923e23ccd20bd32156d7dd708af5bbde410daa993aa2500c847ab2d2/scipy-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eec3842ec9ac9de5917899b277428886042a93db0b227ebbe3a333b64ec7643d", size = 34777384, upload-time = "2026-01-10T21:27:11.423Z" }, + { url = "https://files.pythonhosted.org/packages/39/22/b5da95d74edcf81e540e467202a988c50fef41bd2011f46e05f72ba07df6/scipy-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d7425fcafbc09a03731e1bc05581f5fad988e48c6a861f441b7ab729a49a55ea", size = 37379586, upload-time = "2026-01-10T21:27:20.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b6/8ac583d6da79e7b9e520579f03007cb006f063642afd6b2eeb16b890bf93/scipy-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:87b411e42b425b84777718cc41516b8a7e0795abfa8e8e1d573bf0ef014f0812", size = 36287211, upload-time = "2026-01-10T21:28:43.122Z" }, + { url = "https://files.pythonhosted.org/packages/55/fb/7db19e0b3e52f882b420417644ec81dd57eeef1bd1705b6f689d8ff93541/scipy-1.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:357ca001c6e37601066092e7c89cca2f1ce74e2a520ca78d063a6d2201101df2", size = 24312646, upload-time = "2026-01-10T21:28:49.893Z" }, + { url = "https://files.pythonhosted.org/packages/20/b6/7feaa252c21cc7aff335c6c55e1b90ab3e3306da3f048109b8b639b94648/scipy-1.17.0-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:ec0827aa4d36cb79ff1b81de898e948a51ac0b9b1c43e4a372c0508c38c0f9a3", size = 31693194, upload-time = "2026-01-10T21:27:27.454Z" }, + { url = "https://files.pythonhosted.org/packages/76/bb/bbb392005abce039fb7e672cb78ac7d158700e826b0515cab6b5b60c26fb/scipy-1.17.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:819fc26862b4b3c73a60d486dbb919202f3d6d98c87cf20c223511429f2d1a97", size = 28365415, upload-time = "2026-01-10T21:27:34.26Z" }, + { url = "https://files.pythonhosted.org/packages/37/da/9d33196ecc99fba16a409c691ed464a3a283ac454a34a13a3a57c0d66f3a/scipy-1.17.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:363ad4ae2853d88ebcde3ae6ec46ccca903ea9835ee8ba543f12f575e7b07e4e", size = 20537232, upload-time = "2026-01-10T21:27:40.306Z" }, + { url = "https://files.pythonhosted.org/packages/56/9d/f4b184f6ddb28e9a5caea36a6f98e8ecd2a524f9127354087ce780885d83/scipy-1.17.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:979c3a0ff8e5ba254d45d59ebd38cde48fce4f10b5125c680c7a4bfe177aab07", size = 22791051, upload-time = "2026-01-10T21:27:46.539Z" }, + { url = "https://files.pythonhosted.org/packages/9b/9d/025cccdd738a72140efc582b1641d0dd4caf2e86c3fb127568dc80444e6e/scipy-1.17.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:130d12926ae34399d157de777472bf82e9061c60cc081372b3118edacafe1d00", size = 32815098, upload-time = "2026-01-10T21:27:54.389Z" }, + { url = "https://files.pythonhosted.org/packages/48/5f/09b879619f8bca15ce392bfc1894bd9c54377e01d1b3f2f3b595a1b4d945/scipy-1.17.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e886000eb4919eae3a44f035e63f0fd8b651234117e8f6f29bad1cd26e7bc45", size = 35031342, upload-time = "2026-01-10T21:28:03.012Z" }, + { url = "https://files.pythonhosted.org/packages/f2/9a/f0f0a9f0aa079d2f106555b984ff0fbb11a837df280f04f71f056ea9c6e4/scipy-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:13c4096ac6bc31d706018f06a49abe0485f96499deb82066b94d19b02f664209", size = 34893199, upload-time = "2026-01-10T21:28:10.832Z" }, + { url = "https://files.pythonhosted.org/packages/90/b8/4f0f5cf0c5ea4d7548424e6533e6b17d164f34a6e2fb2e43ffebb6697b06/scipy-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cacbaddd91fcffde703934897c5cd2c7cb0371fac195d383f4e1f1c5d3f3bd04", size = 37438061, upload-time = "2026-01-10T21:28:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/f9/cc/2bd59140ed3b2fa2882fb15da0a9cb1b5a6443d67cfd0d98d4cec83a57ec/scipy-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:edce1a1cf66298cccdc48a1bdf8fb10a3bf58e8b58d6c3883dd1530e103f87c0", size = 36328593, upload-time = "2026-01-10T21:28:28.007Z" }, + { url = "https://files.pythonhosted.org/packages/13/1b/c87cc44a0d2c7aaf0f003aef2904c3d097b422a96c7e7c07f5efd9073c1b/scipy-1.17.0-cp313-cp313t-win_arm64.whl", hash = "sha256:30509da9dbec1c2ed8f168b8d8aa853bc6723fede1dbc23c7d43a56f5ab72a67", size = 24625083, upload-time = "2026-01-10T21:28:35.188Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2d/51006cd369b8e7879e1c630999a19d1fbf6f8b5ed3e33374f29dc87e53b3/scipy-1.17.0-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:c17514d11b78be8f7e6331b983a65a7f5ca1fd037b95e27b280921fe5606286a", size = 31346803, upload-time = "2026-01-10T21:28:57.24Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2e/2349458c3ce445f53a6c93d4386b1c4c5c0c540917304c01222ff95ff317/scipy-1.17.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:4e00562e519c09da34c31685f6acc3aa384d4d50604db0f245c14e1b4488bfa2", size = 27967182, upload-time = "2026-01-10T21:29:04.107Z" }, + { url = "https://files.pythonhosted.org/packages/5e/7c/df525fbfa77b878d1cfe625249529514dc02f4fd5f45f0f6295676a76528/scipy-1.17.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f7df7941d71314e60a481e02d5ebcb3f0185b8d799c70d03d8258f6c80f3d467", size = 20139125, upload-time = "2026-01-10T21:29:10.179Z" }, + { url = "https://files.pythonhosted.org/packages/33/11/fcf9d43a7ed1234d31765ec643b0515a85a30b58eddccc5d5a4d12b5f194/scipy-1.17.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:aabf057c632798832f071a8dde013c2e26284043934f53b00489f1773b33527e", size = 22443554, upload-time = "2026-01-10T21:29:15.888Z" }, + { url = "https://files.pythonhosted.org/packages/80/5c/ea5d239cda2dd3d31399424967a24d556cf409fbea7b5b21412b0fd0a44f/scipy-1.17.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a38c3337e00be6fd8a95b4ed66b5d988bac4ec888fd922c2ea9fe5fb1603dd67", size = 32757834, upload-time = "2026-01-10T21:29:23.406Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7e/8c917cc573310e5dc91cbeead76f1b600d3fb17cf0969db02c9cf92e3cfa/scipy-1.17.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00fb5f8ec8398ad90215008d8b6009c9db9fa924fd4c7d6be307c6f945f9cd73", size = 34995775, upload-time = "2026-01-10T21:29:31.915Z" }, + { url = "https://files.pythonhosted.org/packages/c5/43/176c0c3c07b3f7df324e7cdd933d3e2c4898ca202b090bd5ba122f9fe270/scipy-1.17.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f2a4942b0f5f7c23c7cd641a0ca1955e2ae83dedcff537e3a0259096635e186b", size = 34841240, upload-time = "2026-01-10T21:29:39.995Z" }, + { url = "https://files.pythonhosted.org/packages/44/8c/d1f5f4b491160592e7f084d997de53a8e896a3ac01cd07e59f43ca222744/scipy-1.17.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf133ced83889583156566d2bdf7a07ff89228fe0c0cb727f777de92092ec6b", size = 37394463, upload-time = "2026-01-10T21:29:48.723Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ec/42a6657f8d2d087e750e9a5dde0b481fd135657f09eaf1cf5688bb23c338/scipy-1.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:3625c631a7acd7cfd929e4e31d2582cf00f42fcf06011f59281271746d77e061", size = 37053015, upload-time = "2026-01-10T21:30:51.418Z" }, + { url = "https://files.pythonhosted.org/packages/27/58/6b89a6afd132787d89a362d443a7bddd511b8f41336a1ae47f9e4f000dc4/scipy-1.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:9244608d27eafe02b20558523ba57f15c689357c85bdcfe920b1828750aa26eb", size = 24951312, upload-time = "2026-01-10T21:30:56.771Z" }, + { url = "https://files.pythonhosted.org/packages/e9/01/f58916b9d9ae0112b86d7c3b10b9e685625ce6e8248df139d0fcb17f7397/scipy-1.17.0-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:2b531f57e09c946f56ad0b4a3b2abee778789097871fc541e267d2eca081cff1", size = 31706502, upload-time = "2026-01-10T21:29:56.326Z" }, + { url = "https://files.pythonhosted.org/packages/59/8e/2912a87f94a7d1f8b38aabc0faf74b82d3b6c9e22be991c49979f0eceed8/scipy-1.17.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:13e861634a2c480bd237deb69333ac79ea1941b94568d4b0efa5db5e263d4fd1", size = 28380854, upload-time = "2026-01-10T21:30:01.554Z" }, + { url = "https://files.pythonhosted.org/packages/bd/1c/874137a52dddab7d5d595c1887089a2125d27d0601fce8c0026a24a92a0b/scipy-1.17.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:eb2651271135154aa24f6481cbae5cc8af1f0dd46e6533fb7b56aa9727b6a232", size = 20552752, upload-time = "2026-01-10T21:30:05.93Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f0/7518d171cb735f6400f4576cf70f756d5b419a07fe1867da34e2c2c9c11b/scipy-1.17.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:c5e8647f60679790c2f5c76be17e2e9247dc6b98ad0d3b065861e082c56e078d", size = 22803972, upload-time = "2026-01-10T21:30:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/7c/74/3498563a2c619e8a3ebb4d75457486c249b19b5b04a30600dfd9af06bea5/scipy-1.17.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fb10d17e649e1446410895639f3385fd2bf4c3c7dfc9bea937bddcbc3d7b9ba", size = 32829770, upload-time = "2026-01-10T21:30:16.359Z" }, + { url = "https://files.pythonhosted.org/packages/48/d1/7b50cedd8c6c9d6f706b4b36fa8544d829c712a75e370f763b318e9638c1/scipy-1.17.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8547e7c57f932e7354a2319fab613981cde910631979f74c9b542bb167a8b9db", size = 35051093, upload-time = "2026-01-10T21:30:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/e2/82/a2d684dfddb87ba1b3ea325df7c3293496ee9accb3a19abe9429bce94755/scipy-1.17.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33af70d040e8af9d5e7a38b5ed3b772adddd281e3062ff23fec49e49681c38cf", size = 34909905, upload-time = "2026-01-10T21:30:28.704Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5e/e565bd73991d42023eb82bb99e51c5b3d9e2c588ca9d4b3e2cc1d3ca62a6/scipy-1.17.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb55bb97d00f8b7ab95cb64f873eb0bf54d9446264d9f3609130381233483f", size = 37457743, upload-time = "2026-01-10T21:30:34.819Z" }, + { url = "https://files.pythonhosted.org/packages/58/a8/a66a75c3d8f1fb2b83f66007d6455a06a6f6cf5618c3dc35bc9b69dd096e/scipy-1.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1ff269abf702f6c7e67a4b7aad981d42871a11b9dd83c58d2d2ea624efbd1088", size = 37098574, upload-time = "2026-01-10T21:30:40.782Z" }, + { url = "https://files.pythonhosted.org/packages/56/a5/df8f46ef7da168f1bc52cd86e09a9de5c6f19cc1da04454d51b7d4f43408/scipy-1.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:031121914e295d9791319a1875444d55079885bbae5bdc9c5e0f2ee5f09d34ff", size = 25246266, upload-time = "2026-01-10T21:30:45.923Z" }, ] [[package]] name = "sentry-sdk" -version = "2.48.0" +version = "2.53.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/f0/0e9dc590513d5e742d7799e2038df3a05167cba084c6ca4f3cdd75b55164/sentry_sdk-2.48.0.tar.gz", hash = "sha256:5213190977ff7fdff8a58b722fb807f8d5524a80488626ebeda1b5676c0c1473", size = 384828, upload-time = "2025-12-16T14:55:41.722Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/06/66c8b705179bc54087845f28fd1b72f83751b6e9a195628e2e9af9926505/sentry_sdk-2.53.0.tar.gz", hash = "sha256:6520ef2c4acd823f28efc55e43eb6ce2e6d9f954a95a3aa96b6fd14871e92b77", size = 412369, upload-time = "2026-02-16T11:11:14.743Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/19/8d77f9992e5cbfcaa9133c3bf63b4fbbb051248802e1e803fed5c552fbb2/sentry_sdk-2.48.0-py2.py3-none-any.whl", hash = "sha256:6b12ac256769d41825d9b7518444e57fa35b5642df4c7c5e322af4d2c8721172", size = 414555, upload-time = "2025-12-16T14:55:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/2fdf854bc3b9c7f55219678f812600a20a138af2dd847d99004994eada8f/sentry_sdk-2.53.0-py2.py3-none-any.whl", hash = "sha256:46e1ed8d84355ae54406c924f6b290c3d61f4048625989a723fd622aab838899", size = 437908, upload-time = "2026-02-16T11:11:13.227Z" }, ] [[package]] @@ -2849,78 +2898,83 @@ wheels = [ [[package]] name = "tomli" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, - { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, - { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, - { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, - { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, - { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, - { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, - { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, - { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, - { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, - { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, - { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, - { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, - { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, - { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, - { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, - { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, - { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, - { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, - { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, - { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, - { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, - { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, - { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, - { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, - { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, - { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, - { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, - { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, - { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, - { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, - { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, - { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, - { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, - { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, - { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, - { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, - { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, - { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, ] [[package]] name = "tqdm" -version = "4.67.1" +version = "4.67.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, ] [[package]] name = "typer" -version = "0.21.1" +version = "0.24.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "annotated-doc" }, { name = "click" }, { name = "rich" }, { name = "shellingham" }, - { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b6/3e681d3b6bb22647509bdbfdd18055d5adc0dce5c5585359fa46ff805fdc/typer-0.24.0.tar.gz", hash = "sha256:f9373dc4eff901350694f519f783c29b6d7a110fc0dcc11b1d7e353b85ca6504", size = 118380, upload-time = "2026-02-16T22:08:48.496Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" }, + { url = "https://files.pythonhosted.org/packages/85/d0/4da85c2a45054bb661993c93524138ace4956cb075a7ae0c9d1deadc331b/typer-0.24.0-py3-none-any.whl", hash = "sha256:5fc435a9c8356f6160ed6e85a6301fdd6e3d8b2851da502050d1f92c5e9eddc8", size = 56441, upload-time = "2026-02-16T22:08:47.535Z" }, ] [[package]] @@ -2946,11 +3000,11 @@ wheels = [ [[package]] name = "tzdata" -version = "2025.2" +version = "2025.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] [[package]] @@ -2964,29 +3018,29 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.40.0" +version = "0.41.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click" }, - { name = "h11" }, + { name = "click", marker = "sys_platform != 'emscripten'" }, + { name = "h11", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" }, ] [[package]] name = "virtualenv" -version = "20.36.1" +version = "20.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c1/ef/d9d4ce633df789bf3430bd81fb0d8b9d9465dfc1d1f0deb3fb62cd80f5c2/virtualenv-20.37.0.tar.gz", hash = "sha256:6f7e2064ed470aa7418874e70b6369d53b66bcd9e9fd5389763e96b6c94ccb7c", size = 5864710, upload-time = "2026-02-16T16:17:59.42Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, + { url = "https://files.pythonhosted.org/packages/42/4b/6cf85b485be7ec29db837ec2a1d8cd68bc1147b1abf23d8636c5bd65b3cc/virtualenv-20.37.0-py3-none-any.whl", hash = "sha256:5d3951c32d57232ae3569d4de4cc256c439e045135ebf43518131175d9be435d", size = 5837480, upload-time = "2026-02-16T16:17:57.341Z" }, ] [[package]] @@ -3018,46 +3072,46 @@ wheels = [ [[package]] name = "xlwings" -version = "0.33.16" +version = "0.33.20" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "appscript", marker = "sys_platform == 'darwin'" }, { name = "psutil", marker = "sys_platform == 'darwin'" }, { name = "pywin32", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/e3/4b4ff2248476b4a9cc71c7ebafdba642dafdf6c0559cf86fe44f73a5c6d0/xlwings-0.33.16.tar.gz", hash = "sha256:3ed1b93199d2cb4509ad9a3d4bab38ce483a8eaa39d38ffd034b7edac3241b51", size = 15705501, upload-time = "2025-10-10T16:13:34.197Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/59/d9f9ae712acd273396f6e58ccfbdc116f9deba269fbead95bf5760221384/xlwings-0.33.16-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d20e4a32b0dd76571ace70d03093ab933704f488c6db2f888550c9c435844817", size = 1418140, upload-time = "2025-10-10T16:13:48.709Z" }, - { url = "https://files.pythonhosted.org/packages/23/c5/a3c564294057b39de260e4b03f25d44f3c033ddcb78a0ecef576a3d98720/xlwings-0.33.16-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b2e145a06fd44755963c9ec3fd2a9d75c144f9024789264fb8a39bc125dd0af8", size = 1403121, upload-time = "2025-10-10T16:13:49.87Z" }, - { url = "https://files.pythonhosted.org/packages/31/cd/3ca7b20c96e8af552349e803aa620b4ffdeb0d4eee560b3f14c3e2b8a90d/xlwings-0.33.16-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65ac0f404f076a780b346239dc1a9efd4a2a6e45ca62f15d9a08b220a6de8d53", size = 1460325, upload-time = "2025-10-10T16:13:51.498Z" }, - { url = "https://files.pythonhosted.org/packages/4c/df/3a60d9ffe47f3178ca538678485e2ea69cbd442914f7a6340673c4bfeb7f/xlwings-0.33.16-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb8146b24798198497c96df99c70d00c8d7b9c845d747162c438b91e192aa01d", size = 1464812, upload-time = "2025-10-10T16:13:53.045Z" }, - { url = "https://files.pythonhosted.org/packages/23/0a/5b4f66c26a212f6cfd715fa2658d3ce7902499214ea638300e0984add9ce/xlwings-0.33.16-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbddd19a5fc80021ca6ff9cc8139b29164372457179f566e1b73b2b2bad4388b", size = 1465855, upload-time = "2025-10-10T16:13:54.465Z" }, - { url = "https://files.pythonhosted.org/packages/78/65/e00386d26091bce82ca85f03954d9c5250e9d9b8cb5738cf61468a428d99/xlwings-0.33.16-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:933624904fda923dfd6368d207c3b2c8668788d9a24264b3a608147b97463df9", size = 1641694, upload-time = "2025-10-10T16:13:55.892Z" }, - { url = "https://files.pythonhosted.org/packages/44/91/4b45453407bffa15ed8b6111f9122076663df7d579d048f0e1b396cfc772/xlwings-0.33.16-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:970ce94624a6e75a66ed57c8fb1ba2e50cbf4309e56563d0d3b96c03f40a5fcb", size = 1636945, upload-time = "2025-10-10T16:13:57.391Z" }, - { url = "https://files.pythonhosted.org/packages/6e/35/e2190c93bd615e059da270cefabdeecd2e417bc601e59d761e7adfb00d40/xlwings-0.33.16-cp311-cp311-win_amd64.whl", hash = "sha256:4459a39586d8001bf1e6d14d502c19eac52af1362cd3ac12d6c9d988d370f804", size = 1631025, upload-time = "2025-10-10T16:13:58.467Z" }, - { url = "https://files.pythonhosted.org/packages/16/bc/1d024c1f5b0487da5ee4fa3f71fc9e8140ff4ca6ed95f24d310b0bc428c1/xlwings-0.33.16-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3332ea06de1f993c72101251948afd12e44f68d9738c85934a0d7f899743ce60", size = 1415246, upload-time = "2025-10-10T16:14:00.037Z" }, - { url = "https://files.pythonhosted.org/packages/28/90/4652e69dec171a0570e0ebf0dab1bcf637c7f74f4c81c3b5b9339344aacc/xlwings-0.33.16-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2c4bb59522fe6b2e6106b460d4dea8d5ab06e7d3599e0dbe7582c10c257c5ddb", size = 1399303, upload-time = "2025-10-10T16:14:01.704Z" }, - { url = "https://files.pythonhosted.org/packages/a6/af/02e48e007a6a2213cc775bf699feac46cd4a5f8abbef5ca9c52fac0b01f1/xlwings-0.33.16-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97aaa937438a58fb166192b09338a6b58bb9612167fd25b3afcde7ba3edf1664", size = 1459522, upload-time = "2025-10-10T16:14:02.835Z" }, - { url = "https://files.pythonhosted.org/packages/0d/e1/273d99eb6cf6da026ac58aff3feaa288591a949b6bb5563d908e204722cf/xlwings-0.33.16-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d705df6a731d17436bf2fe419e85b0eccb74877de977fe320cb31ad18097b4cc", size = 1463864, upload-time = "2025-10-10T16:14:04.568Z" }, - { url = "https://files.pythonhosted.org/packages/6b/7e/2aa1a2daba72512bc13483f424070db0649b6720d30f8527db8efb33c94e/xlwings-0.33.16-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4602f0a5fc2416f954c3b389ed732f1f5f3e4f6a7f35f1a5032b618323c85ba", size = 1465310, upload-time = "2025-10-10T16:14:05.716Z" }, - { url = "https://files.pythonhosted.org/packages/55/6c/3579de03d723378854228a79d94c31a43656bc2d7c4b205fd86a4f7505d1/xlwings-0.33.16-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f2ecb6b820ae9657e017bffd57c46676bb2afd98377931d2deb526e758e83eaa", size = 1641107, upload-time = "2025-10-10T16:14:07.318Z" }, - { url = "https://files.pythonhosted.org/packages/e5/f3/46f752a363b23454d6d285ce6132d22bbe125199979e9aaedbc744897cae/xlwings-0.33.16-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5992255711b11ce2130571c8c77babf5b99bdc01b88aba967e3c3983fdd0ec67", size = 1636267, upload-time = "2025-10-10T16:14:08.646Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e2/5b944e96312490ccb03450ca19d8f3337b8e8c6b7a1b8947e9fff7fe87a4/xlwings-0.33.16-cp312-cp312-win_amd64.whl", hash = "sha256:28bbbd2bda7e9dc71d8a566a6306708676758973d00c4d0bccd79c33ab74a914", size = 1630930, upload-time = "2025-10-10T16:14:10.253Z" }, - { url = "https://files.pythonhosted.org/packages/86/08/e8cd25beae629dfe201bc2f52fba3128d6ad2ad05ca10e6f0a675d5ea353/xlwings-0.33.16-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a1fa32063ea8e567409242ba11e483759210af6de93d025ecdbed45695b241fe", size = 1415631, upload-time = "2025-10-10T16:14:11.815Z" }, - { url = "https://files.pythonhosted.org/packages/8f/d3/10e0f425bc695941d95c7f47e1707dd56f99ce79f6d29e7b27fda1178dca/xlwings-0.33.16-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6a79f54916c42514ba9904154b217d8bfa98c25f0c5969353af23623f7c5d4c4", size = 1400140, upload-time = "2025-10-10T16:14:13.531Z" }, - { url = "https://files.pythonhosted.org/packages/92/15/6ae6328e64a0b181a07a78423d8c3942fc5cc9261cf27f1f37449ce203a4/xlwings-0.33.16-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ee2c3c0ad5c4277a1b5709c3f4ca2e2d1c06df1415adc231ec1854ba9d87a93", size = 1459909, upload-time = "2025-10-10T16:14:15.674Z" }, - { url = "https://files.pythonhosted.org/packages/d7/f4/e6ae7ea7ddfc53a9ff90d7f050d5236f091292200db988a7f6fdff0977ad/xlwings-0.33.16-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1fb07f710b04538a9207a882a32d0151c4f3a8e7bf1218aeed76abda2a8d499", size = 1463285, upload-time = "2025-10-10T16:14:17.284Z" }, - { url = "https://files.pythonhosted.org/packages/07/38/4d46d1e5a7a0d60d84360e9bfbd62b28f6ca913009262aac9b59c3c18705/xlwings-0.33.16-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c6bc520d726f6eabfbbaceeb4dc8412fc906742377ab0f77b326ec8b95b39a", size = 1465546, upload-time = "2025-10-10T16:14:18.473Z" }, - { url = "https://files.pythonhosted.org/packages/66/58/ca06a6fd9e13e72a7ec05bd6b56f39dd2843c57594133f42363e31850d6f/xlwings-0.33.16-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:8ff6940a9f2d2b9b6876b4f4780403bc9c9bb1614ed6e3e29c913058a1b9b05e", size = 1641584, upload-time = "2025-10-10T16:14:19.613Z" }, - { url = "https://files.pythonhosted.org/packages/d5/6d/ed12c1bca3bbeb2a26a918c52b376dd39a647c5c6443bbf3bf351846c90d/xlwings-0.33.16-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3fa4768d74915df3e000737d26ad0889c8586c194996c68360909d8214743c18", size = 1636547, upload-time = "2025-10-10T16:14:21.3Z" }, - { url = "https://files.pythonhosted.org/packages/00/ee/29356034743afe63b90af1d8d6f605233454166496ff58ec6133f51921e4/xlwings-0.33.16-cp313-cp313-win_amd64.whl", hash = "sha256:fd4c6da24b4032a65917a35575126892c0319c2d607542a8816f0f9dac02c97f", size = 1630855, upload-time = "2025-10-10T16:14:22.52Z" }, - { url = "https://files.pythonhosted.org/packages/d2/31/2ff0120422f320d3840c3cff5cf6813e9507378414f2ceab3345b4686f00/xlwings-0.33.16-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc86d0180abb69d23531840ce51da8d15e597c5ad33aaa54c02f35f5e7991439", size = 1414773, upload-time = "2025-10-10T16:14:23.775Z" }, - { url = "https://files.pythonhosted.org/packages/fd/df/cb1e04bbef5b63adbe1e28556c90dc4faa5f5a00cc29ce2b1ad955f85a26/xlwings-0.33.16-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:160625fa6d3c54b17a5afa642a0dd0364c67919e0d82d38a2bc9df8f1019c959", size = 1399805, upload-time = "2025-10-10T16:14:24.967Z" }, - { url = "https://files.pythonhosted.org/packages/fe/36/b91f459f7a45bb3030f684abc85e970ad627acb7960a194cb874d45fc9ae/xlwings-0.33.16-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33ef84134f821b0597351961ed6c6abf3c97cf7f604beb0733478ba1a55ebb47", size = 1460195, upload-time = "2025-10-10T16:14:26.64Z" }, - { url = "https://files.pythonhosted.org/packages/c4/84/c55a6eb5459ace6668ef55e4b042e32e0dbc6c605cceff707609c9903043/xlwings-0.33.16-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4283246f8553e776cabd149088f001b5f45315ff375f2d0d3c2fcd7bd7a98195", size = 1463185, upload-time = "2025-10-10T16:14:28.333Z" }, - { url = "https://files.pythonhosted.org/packages/3f/92/128580317b29b179f0ffb3e8c12528d24d351671e55eb24d1e97d6f63cc6/xlwings-0.33.16-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bfcecec3b20802e3e976f63054d3a019c2a7c23d640d014e51d2cf4f907edf3", size = 1464759, upload-time = "2025-10-10T16:14:29.544Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f1/08d30773fbcfdff60632b01db3eee3311da9383cd8d473d4b013679d197f/xlwings-0.33.16-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6a0fc127d4c95d8452024f2f4a4589be4d76b80a8e32b7fa86576d9a6aa735af", size = 1641645, upload-time = "2025-10-10T16:14:30.671Z" }, - { url = "https://files.pythonhosted.org/packages/b7/a9/3c41849f6199e845c2af0971202ed99c4aba24243adcec3b385a18a471a8/xlwings-0.33.16-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:da19b3becfa34eacb6e0d6786bd6bfb17da45b28c7f29cba8b7dce292ca552d8", size = 1636452, upload-time = "2025-10-10T16:14:31.998Z" }, - { url = "https://files.pythonhosted.org/packages/3c/d3/6303d45234dade44b2fbf24f73cdf33485b121bc1641aefad7976d2652da/xlwings-0.33.16-cp314-cp314-win_amd64.whl", hash = "sha256:ce41bbc78997a2214dfd3722081ccd7b345f1af0d7437f3eb8e426accb39e7e4", size = 1653329, upload-time = "2025-10-10T16:14:33.326Z" }, - { url = "https://files.pythonhosted.org/packages/f2/1f/2cfb6b8145a6594d69aae76d9213e7d709c763c251e45a03d76df43f1f7e/xlwings-0.33.16-py3-none-any.whl", hash = "sha256:2adc68b55dedcf835b461bb60dc730c59329267b5361cb2f74d0a0dafc86906c", size = 732638, upload-time = "2025-10-10T16:14:34.511Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/9d/bb/c63719509af8dff2ec20a809e87bef90f35661488d6484dac306d1d7b560/xlwings-0.33.20.tar.gz", hash = "sha256:52c85b693745cd0cc9fd847609dae976cb12ea1e9fa2065b91cf52ce2d7a559d", size = 15761897, upload-time = "2026-01-21T14:41:03.709Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/80/a59e72e7b3fd38bca31b107471722fc3c85415314bef70ba2ceb50c234bf/xlwings-0.33.20-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:388b124db5928ead1bac43b828b06ebb25a26959a1daaadc57cc518640a93cd1", size = 1388819, upload-time = "2026-01-21T14:41:14.419Z" }, + { url = "https://files.pythonhosted.org/packages/8d/aa/24da7578c615f95f025bbac8fc9b851399211e377f33409183bd60ef772c/xlwings-0.33.20-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2dcb228a463def04fa07107527fc1a1116a1d6e515621355977cdb77bec3f7f9", size = 1371654, upload-time = "2026-01-21T14:41:15.554Z" }, + { url = "https://files.pythonhosted.org/packages/86/77/5001f724f84e0b728fa7ac7b0256205b73394f6993d3bb2ee3a4c25e7b79/xlwings-0.33.20-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ae857e3a59cfea0eb05f8400be17a76387dd7fb3ea4fd9f9325c52f140e1e55", size = 1430557, upload-time = "2026-01-21T14:41:16.737Z" }, + { url = "https://files.pythonhosted.org/packages/88/87/39798513a6d9a44b451613ae8961b718d1c6a033884262cf77f9f59e33aa/xlwings-0.33.20-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7911b6bb5f24a2df0229390f37ea835480a508a60933de051b50585d3a725ef", size = 1433243, upload-time = "2026-01-21T14:41:17.82Z" }, + { url = "https://files.pythonhosted.org/packages/10/d1/be9425cedaa84fe9eaadca61878bf03c7127798f059545718143f8b54dce/xlwings-0.33.20-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54ec450ef6dc7704f316e44cd6149102c41ea776ce0863494a5b37b0948b42b1", size = 1440029, upload-time = "2026-01-21T14:41:19.041Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c9/92193b7cf2ca4f43c6ca8640e53b796ee3db0ccc4a7bb135712736079e63/xlwings-0.33.20-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9dfd1fa445e9be73ea30d932a18ce877df3e3b339de8af59540c56ecc6872d5b", size = 1611773, upload-time = "2026-01-21T14:41:20.102Z" }, + { url = "https://files.pythonhosted.org/packages/ad/33/3e02d50e8e05f52cbf288bd261cfa36e0c43800195d9a0cda715705cea86/xlwings-0.33.20-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dac7e245776d4e47f22c43d1a5a1e9486a6ca86ae6a769540b2f5cccaa5e730a", size = 1645100, upload-time = "2026-01-21T14:41:21.463Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f8/34e506fdb03a128bf94f4e6a4ef782ffd08daef0309a9fc6a8e1d3e1f743/xlwings-0.33.20-cp311-cp311-win_amd64.whl", hash = "sha256:f9c0b47fea3607ea1d18ed964953401f1e569ef1a1e8778189a86050d7a4098a", size = 1603114, upload-time = "2026-01-21T14:41:23.147Z" }, + { url = "https://files.pythonhosted.org/packages/e4/bf/ca7671307d147e4c5bb47e84d1f8ebb27d05e7dca45cb2f066f1937cb97f/xlwings-0.33.20-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d8e7029aede59821ec5e963188fc1691e1ed55d3586b05bf91d6c49e64418b49", size = 1385489, upload-time = "2026-01-21T14:41:24.268Z" }, + { url = "https://files.pythonhosted.org/packages/60/b7/a6870daba05685f6957a23a8b33d385a18c1b4f36193d483626fe057a519/xlwings-0.33.20-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e1499bbd945bfc7e23895bdf2d400c74c5333f76f3e6fd23b6bf4bb2d8501dd3", size = 1366744, upload-time = "2026-01-21T14:41:25.427Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f9/95983e3cdd1fd4e0cceedf7166fdfddf1a538867b2e8936c497b86b491ad/xlwings-0.33.20-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aea36b509b86f28c57a4208e72bca30110952c11e0cd6e11610421f85a0c5156", size = 1429398, upload-time = "2026-01-21T14:41:26.641Z" }, + { url = "https://files.pythonhosted.org/packages/02/18/ead395017c25bd9fa055a3ae1e86c835f25bb1b861031a283b0a8abd44cc/xlwings-0.33.20-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8776e701b4ef761dab1563c17b18a009267ce0e285421e099c1f36d52d6a53ff", size = 1432144, upload-time = "2026-01-21T14:41:27.857Z" }, + { url = "https://files.pythonhosted.org/packages/1b/73/89c0f44a1d1fb8d928baac616b8d881fc691170d0149ee00e8a173c0b38a/xlwings-0.33.20-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad352cb2e1a55fdf20ce9d90433f88d727c6b362b8b452e290aee70899060448", size = 1439442, upload-time = "2026-01-21T14:41:28.938Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a6/1e7197c9f78708ce53ba0ed74a19e9dbb845fa73cb5747b3ab5d152384d3/xlwings-0.33.20-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:08ef59d3217ee28ce36d3f122b5b034fd97c167dac31855bab30ebc565b51346", size = 1610359, upload-time = "2026-01-21T14:41:30.532Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ca/7b3dd94712dce8f6cc7509be69ec4e0fd1bd22ca8009a3d8116ac6d1685b/xlwings-0.33.20-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:706b0e9f21ca8bf228391b78ee8d9007287391ba499205c5790ff6b051d4dbe8", size = 1642755, upload-time = "2026-01-21T14:41:32.471Z" }, + { url = "https://files.pythonhosted.org/packages/06/71/325886875c5cafac2ea15561c891a73987e9e8490282dc3f619edca58584/xlwings-0.33.20-cp312-cp312-win_amd64.whl", hash = "sha256:a08040c9bddf7a4ebc1d51fc84138307b58c7553099b50e3d2d7a3b7c6c591fc", size = 1602722, upload-time = "2026-01-21T14:41:33.565Z" }, + { url = "https://files.pythonhosted.org/packages/9e/15/d47bbe7e869f0b60a377540baad70e7874a9ecb9bc027b2616a4bab7b457/xlwings-0.33.20-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:4f77e348957f4db5dc89eaa234c7167e590b76352d499a70a9dcfcdff03c9ebf", size = 1385103, upload-time = "2026-01-21T14:41:35.199Z" }, + { url = "https://files.pythonhosted.org/packages/91/4b/8ec8de217e0d487e4cab97e02b35c6b24d3e0852f55b41d92bad966f6d71/xlwings-0.33.20-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9af3e0988ce1ab2922197c693e5047749c27e6415338a4927a24458dc94324ee", size = 1366295, upload-time = "2026-01-21T14:41:36.295Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ee/afa5067e84f27e998df9fa37cda71f216a047d565500718933799f3a13a4/xlwings-0.33.20-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fa67abf4ddfaa7a135fa534974dbf77b2f28f2528b78db67615c90ec878eaf4", size = 1428944, upload-time = "2026-01-21T14:41:37.837Z" }, + { url = "https://files.pythonhosted.org/packages/6a/50/77d80f98bd4fb7d55bea8d9a12f29954dea9bced64461e3922aa2b79be9a/xlwings-0.33.20-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:09592c09afcffe2435de2d548a3a611b48d6ab68669808e0b2b1fd37afc0c8e7", size = 1432203, upload-time = "2026-01-21T14:41:39.476Z" }, + { url = "https://files.pythonhosted.org/packages/a2/53/9caba36ba0bb924ae8d075fe6ff1b003af460b2fca4a3240a4d8b38462c7/xlwings-0.33.20-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:918f417a41a7ed09054584bc1f61c57bbaaae11636b9450a1219c0afd628e373", size = 1439025, upload-time = "2026-01-21T14:41:40.771Z" }, + { url = "https://files.pythonhosted.org/packages/d0/5e/3b7e4289d66397f61be6a0651f5c810b76a9eccfc4e93db8a32caf3bf390/xlwings-0.33.20-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:73f9cbe16405f54ed8d43eb565459f5c36a14c386dc2f8595a0e6d20ad22f447", size = 1610290, upload-time = "2026-01-21T14:41:41.86Z" }, + { url = "https://files.pythonhosted.org/packages/1e/79/cb2ee8383279a1eb80baf0432af95c2a143a3bbb6054339f0623287decc4/xlwings-0.33.20-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ab2273fa9a4e786ad0e57b3f2ecbc9e8ef3bddbdcb439708af62bdd002d33acf", size = 1644221, upload-time = "2026-01-21T14:41:42.978Z" }, + { url = "https://files.pythonhosted.org/packages/94/b5/e030e5deee30aa4e3ddbb0fdee3d47f087bbb4e647850dc0b9147f02eb8a/xlwings-0.33.20-cp313-cp313-win_amd64.whl", hash = "sha256:62dd5666ee68460416163ded2934e9439a3db1361987870ace5fb26ed8bdf614", size = 1602685, upload-time = "2026-01-21T14:41:44.232Z" }, + { url = "https://files.pythonhosted.org/packages/3e/da/c4f9ccecedef9e98642a588ac7903d9143942717dfbee0e3da2b3dee7a98/xlwings-0.33.20-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:527e820ce6c37068c53212d611b3f6fa56a9f71dfd535e16f64fd860a3615080", size = 1385244, upload-time = "2026-01-21T14:41:45.319Z" }, + { url = "https://files.pythonhosted.org/packages/81/68/c4a14e9148ef42f9383b8c6aead50d1d428200ec4b5f1eb0d0306997fc29/xlwings-0.33.20-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:af88d064bfe81d767d32312d7b81afb2e6b3565fee624c0a5e2a3454383a7d9e", size = 1365918, upload-time = "2026-01-21T14:41:46.361Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e7/779aec907b0a919a71a2e59fa9b7e89e7c74694a9e359f0136b1a122b53e/xlwings-0.33.20-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdb1bb6b5478af103c6fb6471370e7e0a5d70a00f672b313ba68a609f0faff27", size = 1429005, upload-time = "2026-01-21T14:41:47.483Z" }, + { url = "https://files.pythonhosted.org/packages/02/4c/6b8d42e085c907d344e9091e0cbb891a775de190a0db288d8ca367d03332/xlwings-0.33.20-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79f9377798c537c437e04f158f635b078becfe881bba2a538e59d01448342f9b", size = 1431915, upload-time = "2026-01-21T14:41:48.881Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c2/ae98cf66427862dfe52e56668e51f872e5e7baee8ec0d8222fd747f6726f/xlwings-0.33.20-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca22286d62b3db3a4cb6a8626a024b680869bc84896a47c997c60069c51e49ef", size = 1439084, upload-time = "2026-01-21T14:41:49.988Z" }, + { url = "https://files.pythonhosted.org/packages/ac/1f/1a8d6cfc33da413fa2bec104ceed01d88c5d33edd71ca32fc726604e22ca/xlwings-0.33.20-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:15f7821bb364f86cb3db2dec34a64b6e12b3ddf1ea3323b4baadc6122b0ec978", size = 1610152, upload-time = "2026-01-21T14:41:51.371Z" }, + { url = "https://files.pythonhosted.org/packages/99/56/2bb4d8972e3e1fbc9361f29595a22afc163627fbdfba56450c523a74ea86/xlwings-0.33.20-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:1db4bd88277fd5a4b5a1effc1fa8649a2cb7dc633f4eba03d800ae60896c53df", size = 1642859, upload-time = "2026-01-21T14:41:52.45Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5b/ac61220baea87a00e1a5471eab5280726bb048e92817557d714fa6627f2e/xlwings-0.33.20-cp314-cp314-win_amd64.whl", hash = "sha256:4c5443b92a3aa9c2bd6cc1b93f48a7036d50b1f673475f758bcd0ab52e83db0d", size = 1625767, upload-time = "2026-01-21T14:41:53.619Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e3/7253471b65023376819415729d5bfa85d2dc572e60ef1beeb927fe7326df/xlwings-0.33.20-py3-none-any.whl", hash = "sha256:47be279873519ee7d501a2daadca87b3a80c72b57bf6fca55b5891d4bb293957", size = 710681, upload-time = "2026-01-21T14:41:54.706Z" }, ] From 66bf289d1226c6f9411bd2a1479ef09629017199 Mon Sep 17 00:00:00 2001 From: harumiWeb Date: Tue, 17 Feb 2026 19:51:28 +0900 Subject: [PATCH 23/25] fix: use StrEnum for fallback reason enum --- src/exstruct/errors.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/exstruct/errors.py b/src/exstruct/errors.py index c291b13..7fba81f 100644 --- a/src/exstruct/errors.py +++ b/src/exstruct/errors.py @@ -2,7 +2,7 @@ from __future__ import annotations -from enum import Enum +from enum import StrEnum class ExstructError(Exception): @@ -37,7 +37,7 @@ class PrintAreaError(ExstructError, ValueError): """Raised when print-area specific processing fails (also a ValueError for compatibility).""" -class FallbackReason(str, Enum): +class FallbackReason(StrEnum): """Reason codes for extraction fallbacks.""" LIGHT_MODE = "light_mode" From a278d878d5a6609ea56fdc48a7ecff9ab8121898 Mon Sep 17 00:00:00 2001 From: harumiWeb Date: Tue, 17 Feb 2026 21:05:08 +0900 Subject: [PATCH 24/25] feat: Update patch operation documentation and improve test assertions --- .bandit | 2 ++ src/exstruct/mcp/patch_runner.py | 4 ++-- tests/com/test_shapes_extraction.py | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 .bandit diff --git a/.bandit b/.bandit new file mode 100644 index 0000000..953c879 --- /dev/null +++ b/.bandit @@ -0,0 +1,2 @@ +[bandit] +exclude_dirs = tests,benchmark diff --git a/src/exstruct/mcp/patch_runner.py b/src/exstruct/mcp/patch_runner.py index 3472e99..e952373 100644 --- a/src/exstruct/mcp/patch_runner.py +++ b/src/exstruct/mcp/patch_runner.py @@ -124,8 +124,8 @@ class PatchOp(BaseModel): - ``add_sheet``: Add a new worksheet. Requires ``sheet`` (new sheet name). No ``cell``/``value``/``formula``. - ``set_range_values``: Set values for a rectangular range. Requires ``sheet``, ``range`` (e.g. ``A1:C3``), ``values`` (2D list matching range shape). - ``fill_formula``: Fill a formula across a single row or column. Requires ``sheet``, ``range``, ``base_cell``, ``formula``. - - ``set_value_if``: Conditionally set value. Requires ``sheet``, ``cell``, ``expected``, ``value``. Skips if current value != expected. - - ``set_formula_if``: Conditionally set formula. Requires ``sheet``, ``cell``, ``expected``, ``formula``. Skips if current value != expected. + - ``set_value_if``: Conditionally set value. Requires ``sheet``, ``cell``, ``value``. ``expected`` is optional; ``null`` matches an empty cell. Skips if current value != expected. + - ``set_formula_if``: Conditionally set formula. Requires ``sheet``, ``cell``, ``formula``. ``expected`` is optional; ``null`` matches an empty cell. Skips if current value != expected. """ op: PatchOpType = Field( diff --git a/tests/com/test_shapes_extraction.py b/tests/com/test_shapes_extraction.py index 1f0b78d..9283acf 100644 --- a/tests/com/test_shapes_extraction.py +++ b/tests/com/test_shapes_extraction.py @@ -63,7 +63,7 @@ def _make_workbook_with_shapes(path: Path) -> None: except Exception: # In some environments connector wiring may fail; tests will # simply not find connected shapes in that case. - pass + connector = None wb.save(str(path)) finally: @@ -128,7 +128,7 @@ def test_line_direction(tmp_path: Path) -> None: def test_connector_connections(tmp_path: Path) -> None: - """Connectorの接続先IDが抽出結果のshape IDに整合することを確認する。""" + """Verify connector begin/end IDs match emitted shape IDs.""" path = tmp_path / "connectors.xlsx" _make_workbook_with_shapes(path) From bbb2510d4d9d8f4c5e602ac04b01ef0a05843643 Mon Sep 17 00:00:00 2001 From: harumiWeb Date: Tue, 17 Feb 2026 21:19:53 +0900 Subject: [PATCH 25/25] fix: handle app termination gracefully in test_shapes_extraction --- .codacy.yml | 1 + tests/com/test_shapes_extraction.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.codacy.yml b/.codacy.yml index da332f7..1f1b4b8 100644 --- a/.codacy.yml +++ b/.codacy.yml @@ -1,2 +1,3 @@ exclude_paths: - "benchmark/**" + - "tests/**" diff --git a/tests/com/test_shapes_extraction.py b/tests/com/test_shapes_extraction.py index 9283acf..6d9199c 100644 --- a/tests/com/test_shapes_extraction.py +++ b/tests/com/test_shapes_extraction.py @@ -25,7 +25,7 @@ def _excel_app() -> Iterator[xw.App]: try: app.kill() except Exception: - pass + app = None def _make_workbook_with_shapes(path: Path) -> None: