diff --git a/.claude/hooks/check_sync.sh b/.claude/hooks/check_sync.sh deleted file mode 100644 index 3d05aa3..0000000 --- a/.claude/hooks/check_sync.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env bash -# SessionStart hook: ローカルブランチが origin と同期しているかをチェックし, -# JSON で systemMessage (UI 表示) と additionalContext (モデルに注入) を出力する. -# 読み取り専用 (git fetch のみ). 失敗してもセッション開始は阻害しない. -# -# GUIDE_03「直列運用」「main を常に動作可能」を補助する目的で, -# 古い main から作業を始めるミスをセッション開始時に可視化する. - -# JSON 整形は Python に任せる (jq 非依存. 既存 hook も Python を利用). -# python3 / python / py の差は run_python.sh が吸収する (見つからなければ非ブロックで失敗). -emit() { - local msg="$1" - PYTHONIOENCODING=utf-8 bash "$(dirname "$0")/run_python.sh" - "$msg" <<'PY' -import json, sys -msg = sys.argv[1] -print(json.dumps({ - "systemMessage": msg, - "hookSpecificOutput": { - "hookEventName": "SessionStart", - "additionalContext": msg, - }, -}, ensure_ascii=False)) -PY -} - -# git リポジトリ外なら静かに終了 -git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0 - -# origin が未設定なら静かにスキップ (個人 clone・ローカル実験用途) -git remote get-url origin >/dev/null 2>&1 || exit 0 - -# fetch (5 秒で打ち切り). 失敗 = オフライン等. 警告だけ出して継続. -if ! timeout 5 git fetch --quiet origin 2>/dev/null; then - emit "[sync-check] origin への fetch に失敗 — オフラインの可能性" - exit 0 -fi - -BRANCH=$(git branch --show-current) -# detached HEAD は判定できないので静かに終了 -[ -z "$BRANCH" ] && exit 0 - -LOCAL=$(git rev-parse HEAD 2>/dev/null) || exit 0 -REMOTE=$(git rev-parse "@{u}" 2>/dev/null) -if [ -z "$REMOTE" ]; then - emit "[sync-check] $BRANCH は upstream 未設定" - exit 0 -fi -BASE=$(git merge-base HEAD "@{u}") - -DIRTY="" -[ -n "$(git status --porcelain)" ] && DIRTY=" / 未コミット変更あり" - -if [ "$LOCAL" = "$REMOTE" ]; then - emit "[sync-check] ✓ $BRANCH は origin と同期${DIRTY}" -elif [ "$LOCAL" = "$BASE" ]; then - AHEAD=$(git rev-list --count "HEAD..@{u}") - emit "[sync-check] ⚠ $BRANCH は origin より ${AHEAD} コミット遅れ — \`git pull\` 推奨${DIRTY}" -elif [ "$REMOTE" = "$BASE" ]; then - AHEAD=$(git rev-list --count "@{u}..HEAD") - emit "[sync-check] ℹ $BRANCH は origin より ${AHEAD} コミット先行${DIRTY}" -else - AHEAD=$(git rev-list --count "@{u}..HEAD") - BEHIND=$(git rev-list --count "HEAD..@{u}") - emit "[sync-check] ⚠ $BRANCH は origin と分岐 (先行 ${AHEAD} / 遅れ ${BEHIND}) — rebase/merge が必要${DIRTY}" -fi diff --git a/.claude/project-mode b/.claude/project-mode new file mode 100644 index 0000000..ceeec84 --- /dev/null +++ b/.claude/project-mode @@ -0,0 +1 @@ +solo diff --git a/.claude/repo-access.json b/.claude/repo-access.json new file mode 100644 index 0000000..5369745 --- /dev/null +++ b/.claude/repo-access.json @@ -0,0 +1,7 @@ +{ + "deny_read": [ + "C:/Users/Papin/clinical-data", + "data", + "exports" + ] +} diff --git a/.claude/rules/coding-style.md b/.claude/rules/coding-style.md new file mode 100644 index 0000000..2a8c52d --- /dev/null +++ b/.claude/rules/coding-style.md @@ -0,0 +1,116 @@ +--- +paths: ["src/**", "migrations/**", "datasets/**"] +--- + +# コーディング規約 (Coding Standards) + +Python コード(`src/**`)・マイグレーション(`migrations/**`)・データセットの定義(`datasets/**`)を編集するときは本ルールに従うこと. + +## 基本方針 (Basic Policy) + +- ruff を唯一の lint / format ツールとして使用する.設定は `pyproject.toml` の `[tool.ruff]` に集約する +- 標準(PEP 8)から逸脱しない範囲で ruff の自動修正に従う +- 別ツール(black・isort・mypy 等)は導入しない(ruff で完結させる) +- 使う側(TIASJudgeModel 等)と同じ規約にそろえる(サブモジュールとして同じ作業の中で編集されるため) + +## 適用 ruff ルール (Ruff Rules) + +`pyproject.toml` の `[tool.ruff.lint]` で `select = ["E", "F", "W", "I", "B", "UP", "N"]` を選択する.行長は 100 文字とする.実行コマンドは [ENV_04 開発コマンド](../../docs/02_ENV/ENV_04_開発コマンド.md)を参照する. + +| ルール | 内容 | +| --- | --- | +| E・W (pycodestyle) | PEP 8 形式 | +| F (pyflakes) | 未使用変数・未定義シンボル等 | +| I (isort) | import 文の並び順 | +| B (flake8-bugbear) | バグになりやすいパターン | +| UP (pyupgrade) | Python 3.12 の書き方への置き換え | +| N (pep8-naming) | 命名 | + +## 臨床データを扱うコードの規則 (Clinical Data Rules) + +本プロジェクトの最重要規則.[PLAN_01 要件定義書](../../docs/03_PLAN/PLAN_01_要件定義書.md)の制約と [SPEC_03「出力の規則」](../../docs/04_SPEC/SPEC_03_データセットと受け渡し.md)に対応する. + +- **データの場所をコードに書かない**.目録・ストア・取り込み元・計算サーバの場所は設定ファイル(`config.py` が読む)から受け取る.テスト用の既定値もコードに置かない +- **CLI の出力・ログ・例外メッセージに出すのは,件数・研究用 ID・SHA-256・種類名・所見の値・設定で付けた名前だけ**.実際の患者 ID・対応表の中身・画像の中身は出さない.元のファイル名(`source_name`)は既定では出さない +- **対応表(`identity.sqlite`)を開くのは取り込みの処理だけ**にする.他のモジュールから対応表を参照しない +- **原本(取り込み元)に書き込まない**.取り込み元は読み取り専用で開き,ストア・目録にだけ書く +- **ストアのファイルを書き換えない・消さない**.ストアへの書き込みは「同じ名前が無ければコピーし,コピー後にハッシュを照合する」だけにする +- `print` ではなく `logging` を使う.ただし CLI の結果表示(件数の表等)は標準出力に書いてよい + +## 目録(SQLite)の規則 (Catalog Rules) + +- SQL は `catalog/` の中にだけ書く.他のモジュールは `catalog/` の関数を呼ぶ +- **値は必ずプレースホルダ(`?`・`:name`)で渡す**.f-string・`%`・`+` で SQL に値を埋め込まない +- 接続ごとに `PRAGMA foreign_keys = ON` を有効にする +- 複数の表にまたがる登録・版の作成は 1 つのトランザクションで行う(`with conn:`).途中で失敗したら何も残さない +- **凍結した版(`dataset_versions`・`dataset_members`・`dataset_member_annotations`)と所見(`annotations`)を UPDATE・DELETE しない**.所見の訂正は新しい行の追加と `superseded_by` の設定だけで行う +- 日時は UTC の ISO 8601(例: `2026-10-02T09:00:00Z`)で保存する + +## マイグレーション (Migrations) + +- スキーマの変更は `migrations/NNNN_<内容>.sql`(4 桁の連番)を追加して行う.**適用済みのマイグレーションファイルは編集しない** +- 1 つのファイルは 1 つのトランザクションで適用できる内容にする +- 種類(`kinds`)の追加もマイグレーションで行い,同時に `src/tiasdata/kinds/` に値の型を追加する +- 表・列を変えたら [SPEC_02 データモデル](../../docs/04_SPEC/SPEC_02_データモデル.md)を先に更新する + +## 型ヒント (Type Hints) + +- **公開関数・クラス**(`tiasdata` から使う側に見えるもの): 引数と戻り値に型ヒント必須 +- **内部ヘルパ・テストコード**: 任意.書ける所は書く +- 目録の行・manifest の行・設定は pydantic のモデルで表す(辞書のまま受け渡さない) +- 静的型チェック(mypy / pyright)は当面導入しない + +## 公開 API の互換 (Public API Compatibility) + +- `tiasdata` の公開 API(`load`・`open_bundle`・`Bundle` 等)と `manifest.csv` の列は使う側が依存する.**名前・意味を変える・消す場合は,PR に使う側への影響を書き,SPEC_03 を更新する** +- 列や引数の追加(既存の使い方を壊さないもの)は自由に行ってよい + +## docstring + +- **公開関数・クラス**: docstring 必須.Google スタイル(`Args:` / `Returns:` / `Raises:`).日本語可 +- 例外を送出する公開関数は `Raises:` に書く + +```python +def load(name: str, version: int) -> Bundle: + """データセットの版を指定してバンドルを開く. + + Args: + name: データセット名(例: `tongue_color`). + version: 版(1 以上). + + Returns: + 開いたバンドル. + + Raises: + ValueError: バンドルが見つからない,または manifest の SHA-256 が一致しない. + """ +``` + +## 命名規則 (Naming) + +| 対象 | スタイル | 例 | +| --- | --- | --- | +| モジュール・パッケージ | snake_case | `bundle.py`・`tiasdata` | +| 関数・変数 | snake_case | `freeze_version`・`manifest_sha256` | +| クラス | PascalCase | `Bundle`・`CatalogConfig` | +| 定数 | UPPER_SNAKE_CASE | `SCHEMA_VERSION` | +| プライベート | 接頭辞 `_` | `_copy_to_store` | +| 種類名(`kinds.kind`)・データセット名 | snake_case の英小文字 | `tongue_crop_masked`・`tongue_color` | + +## コメント (Comments) + +- **WHY を書く**: 何をしているかは識別子で表し,コメントは「なぜこの実装か」「制約」「出典」に限る +- **TODO** は `# TODO(rintoHasegawa): 内容` の形で書く + +## CLI 引数 (CLI Arguments) + +- `argparse` 標準を使う.ヘルプ文は日本語可 +- 短縮形より長い形を優先する(`--version`・`--to`) +- **データのパスを引数に取らない**.取り込み元・受け渡し先は設定ファイルで付けた名前で指定する(SSH のコマンドにデータの場所が現れないようにするため) + +## 参考 (References) + +- [PEP 8 — Style Guide for Python Code](https://peps.python.org/pep-0008/) +- [Google Python Style Guide — Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) +- [ruff documentation](https://docs.astral.sh/ruff/) +- [SQLite — Foreign Key Support](https://www.sqlite.org/foreignkeys.html) diff --git a/.claude/rules/error-handling.md b/.claude/rules/error-handling.md new file mode 100644 index 0000000..6161aec --- /dev/null +++ b/.claude/rules/error-handling.md @@ -0,0 +1,55 @@ +--- +paths: ["src/**"] +--- + +# エラーハンドリング方針 (Error Handling) + +例外を raise / 捕捉するコード(`src/**`)を書くときは本ルールに従うこと.コーディング全般の規約は `.claude/rules/coding-style.md` を参照する. + +## 例外の使い分け (Exception Types) + +| 状況 | 送出する例外 | +| --- | --- | +| 利用者の入力(設定ファイル・データセットの定義・取り込み元の構成・バンドル等)の内容・形式の問題 | `ValueError` に統一して raise | +| 内部論理エラー(到達しないはずの分岐・不変条件違反等) | `RuntimeError` | +| pydantic を使うバリデーション | `pydantic.ValidationError` をそのまま伝播(握りつぶさない) | +| SQLite の制約違反(`sqlite3.IntegrityError`) | 意味のある `ValueError` に変換して再送出する(`from` で元の例外を残す) | + +## データの問題は止める (Fail Fast on Data) + +目録は再現性の根拠になるため,**静かな除外・静かな上書きをしない**. + +- 取り込み元の想定外の構成・未知の所見の値・ハッシュの不一致・参照切れは raise する +- 「とりあえず `try` で囲んで `continue`」は禁止.登録件数が静かに変わり,版の中身の解釈を誤る +- 例外: `tiasdata check`・`tiasdata stats` は現状把握のためのコマンドなので,問題を数えて最後に報告し,終了コードを 0 以外にする(止めずに全体を見る) + +## 個人情報を出さない (No PII in Messages) + +例外メッセージ・ログに,実際の患者 ID・対応表の中身・元のファイル名を含めない.位置は研究用 ID・SHA-256・件数・取り込み元の名前で示す. + +```python +raise ValueError(f"取り込み元 '{source}' のフォルダ {folder_index} 番目: 想定外の拡張子のファイルが {count} 件ある") +``` + +- 元のファイル名が無いと原因を特定できない場合だけ,CLI に `--show-source-names` のような明示の指定があるときに限り表示する + +## CLI の終了コード (Exit Codes) + +| 終了コード | 意味 | +| --- | --- | +| 0 | 成功 | +| 1 | 入力・データの問題(`ValueError`).メッセージを標準エラーに 1 行で出す(スタックトレースは出さない) | +| 2 | 引数の誤り(`argparse` の既定) | +| それ以外の例外 | スタックトレースを出して異常終了する(バグとして扱う) | + +## 方針 (Policy) + +- **最小限主義**: 独自例外クラスの階層は導入しない.上表の標準例外で表現できる範囲に留める +- **メッセージは日本語可**: 原因と対処が分かる具体的な内容にする +- **握りつぶさない**: `except:` で例外を無言で捨てない +- **境界で検証する**: 入力の検証は入口(設定の読み込み・取り込み元の走査・定義の読み込み・バンドルの読み込み)で行い,内部は検証済みの入力が来る前提で書く + +## 参考 (References) + +- [Python 公式ドキュメント — 組み込み例外](https://docs.python.org/3/library/exceptions.html) +- [Pydantic — Error Handling](https://docs.pydantic.dev/latest/errors/errors/) diff --git a/.claude/rules/lab-pc-operation.md b/.claude/rules/lab-pc-operation.md new file mode 100644 index 0000000..42e8895 --- /dev/null +++ b/.claude/rules/lab-pc-operation.md @@ -0,0 +1,23 @@ +# 研究室 PC の操作ルール (Lab PC Operation) + +研究室 PC(`ssh labpc`)や計算サーバに SSH でコマンドを送るときは,常に本ルールに従うこと.運用の背景は [ENV_02「研究室 PC の操作方針」](../../docs/02_ENV/ENV_02_環境構築手順.md). + +## 禁止 (Prohibited) + +- **データの中身を表示しない**: `type`・`Get-Content`・`cat`・`more`・`sqlite3` 等で,取り込み元・ストア・目録・対応表・バンドル(`C:\Users\Papin\clinical-data\` 配下,サーバの `~/data/` 配下)を読まない.フック(`.claude/repo-access.json`)は SSH の先の読み取りを止められないため,ここで守る +- **データの一覧を表示しない**: `dir`・`ls`・`Get-ChildItem` でデータのフォルダの中身(ファイル名)を表示しない.件数が必要なら `tiasdata stats`・`tiasdata check` を使う(CLI が無い段階で件数が必要なら,ファイル名を出さずに数だけを返すコマンドにする) +- **研究室 PC で編集・コミットしない**: 研究室 PC のリポジトリは `git pull` と `uv sync --no-dev` だけにする +- **原本・ストア・目録を手で消さない・動かさない**: 削除・移動が必要な場合は,実行するコマンドを提示してユーザーに依頼する + +## 許可 (Allowed) + +- `tiasdata` CLI の実行(出力は [SPEC_03「出力の規則」](../../docs/04_SPEC/SPEC_03_データセットと受け渡し.md)で個人情報を含まないことが保証されている) +- ツールの有無・版の確認(`where`・`--version`),`git pull`・`uv sync --no-dev` +- ツールの導入は,コマンド・入る場所を示してユーザーの承認を得てから行う + +## 書き方 (Conventions) + +- 非対話で実行する(`ssh -n labpc "..."`).パスワード入力・GUI が必要な操作は SSH で行わず,ユーザーに依頼する +- 研究室 PC の uv は絶対パス(`C:\Users\Papin\.local\bin\uv.exe`)で呼ぶ +- 出力が文字化けしたら手元で `iconv -f cp932 -t utf-8` を通す +- コマンドにデータのパスを書かない(`tiasdata` は設定ファイルで付けた名前で指定する) diff --git a/.claude/rules/project-structure.md b/.claude/rules/project-structure.md new file mode 100644 index 0000000..037db8b --- /dev/null +++ b/.claude/rules/project-structure.md @@ -0,0 +1,39 @@ +--- +paths: ["src/**", "tests/**", "migrations/**", "datasets/**"] +--- + +# プロジェクト構成ルール (Project Structure) + +ソース・テスト・マイグレーション・データセットの定義を追加・移動するときは本ルールに従うこと.設計の背景は [SPEC_01 アーキテクチャ設計](../../docs/04_SPEC/SPEC_01_アーキテクチャ設計.md)を参照する(本ルールは配置規則のみを定める). + +## ディレクトリ構成 (Layout) + +```text +src/tiasdata/ +├── __init__.py # 公開 API(load・open_bundle・Bundle・Catalog)を再 export する +├── config.py # 設定ファイルの読み込み・検証 +├── catalog/ # 目録(SQLite)の接続・マイグレーションの適用・各表の操作(SQL はここだけ) +├── store.py # ストア(ハッシュ・コピー・照合) +├── kinds/ # ファイルの種類・所見の種類の値の型 +├── importers/ # 取り込み元の形ごとの読み取り(1 つの形 = 1 モジュール) +├── datasets.py # データセットの定義の評価・版の作成 +├── bundle.py # バンドルの書き出し・読み込み・検証 +├── deliver.py # 計算サーバへの受け渡し(scp・ssh) +└── cli.py # CLI エントリポイント + +migrations/ # 目録のスキーマ変更(NNNN_<内容>.sql) +datasets/ # データセットの定義(<名前>.toml) +tests/ # src/tiasdata/ の構造をミラーする +``` + +## 配置ルール (Placement Rules) + +- **1 モジュール 1 責務**: 新しい機能は責務に合うモジュールに置く.大きくなったらサブパッケージに分ける(既存モジュールを肥大化させない) +- **依存方向**: `cli` → `datasets`・`bundle`・`deliver`・`importers` → `catalog`・`store`・`kinds`・`config` の一方向を保つ.下位から上位を import しない +- **バンドルの読み込みは目録に依存しない**: `bundle.py` の読み込み側(`load`・`open_bundle`)は `catalog`・`store`・`config` を import しない(目録の無い計算サーバで使うため.書き出し側は別関数に分ける) +- **公開 API は `__init__.py` で export したものだけ**: 使う側は `tiasdata.<名前>` で使う.内部モジュールを直接 import させない +- **取り込み元を増やすとき**: `importers/` にモジュールを追加し,設定ファイルの `importer` の名前から選べるようにする.既存の importer に分岐を足さない +- **種類を増やすとき**: `migrations/` に `kinds` の行を追加する SQL と,`kinds/` に値の型を追加する([SPEC_02「拡張の例」](../../docs/04_SPEC/SPEC_02_データモデル.md)) +- **副作用は縁に寄せる**: ファイルのコピー・SSH・DB への書き込みは `store`・`deliver`・`catalog` に閉じ込め,定義の評価・manifest の行の組み立て・照合の判定は入力 → 出力の純関数として書く(テスト容易性のため) +- **データを置かない**: 臨床データ・目録・対応表・バンドルをリポジトリの配下に置かない(`.gitignore` は保険.[ENV_02「臨床データの取り扱い」](../../docs/02_ENV/ENV_02_環境構築手順.md)) +- **テストの配置**: `tests/` は `src/tiasdata/` の構造をミラーする(詳細は `.claude/rules/testing.md`) diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md new file mode 100644 index 0000000..a6b53da --- /dev/null +++ b/.claude/rules/testing.md @@ -0,0 +1,62 @@ +--- +paths: ["tests/**", "**/test_*.py", "**/conftest.py"] +--- + +# テスト方針 (Test Policy) + +テストコード(`tests/**`・`test_*.py`・`conftest.py`)を追加・編集するときは本ルールに従うこと. + +## 基本方針 (Basic Policy) + +- フレームワーク: **pytest** +- 配置: `tests/` 配下に `src/tiasdata/` の構造をミラーする +- カバレッジ目標: **数値目標なし**.代わりに「下記の必須対象は必ず単体テストを書く」を徹底する +- **臨床データを使うテストは書かない**.テストはダミーデータ(`tmp_path` に作る小さい画像・設定ファイル・目録・ストア)だけで完結させる.手元 PC でも研究室 PC でも同じテストが通ること +- 研究室 PC の実環境での確認は,テストではなく `tiasdata check`・`tiasdata stats` を SSH 経由で実行して行う([SPEC_03「CLI」](../../docs/04_SPEC/SPEC_03_データセットと受け渡し.md)) +- 実行コマンドは [ENV_04 開発コマンド](../../docs/02_ENV/ENV_04_開発コマンド.md)を参照する + +## ファイル・関数命名 (Naming) + +- ファイル: `test_<対象モジュール>.py` +- 関数: `test_<振る舞い>`(例: `test_freeze_keeps_past_version_unchanged`) +- クラス(任意): `Test<クラス名>` で振る舞いごとにまとめてよい + +## 必ずテストを書く対象 (Mandatory) + +1. **版の凍結**: データ・所見を追加・訂正した後も,過去の版から同じ構成・同じ `manifest.csv`(SHA-256 一致)が得られること.構成が同じなら新しい版を作らないこと.**最重要**(破れると再現性が無意味になる) +2. **ストア**: 同じ中身は 1 つにまとまること.コピー後にハッシュを照合すること.既存のファイルを上書きしないこと.原本(取り込み元)を変更しないこと +3. **取り込み**: 2 回実行しても結果が同じであること.取り込み元の想定外の構成(未知のフォルダ・空のフォルダ・想定外の拡張子)で停止すること.途中で失敗したら目録に何も残らないこと +4. **所見の検証と訂正**: 未知の値・無効な組み合わせ(例: `light_red` 以外で `normal`)を拒否すること.訂正しても古い行が残り,過去の版が古い値を返すこと +5. **バンドル**: 書き出したバンドルを `open_bundle` で読めること.`manifest.csv` の SHA-256 の不一致・ファイルの欠け・中身の改変を検出すること.バンドルの読み込みが目録なしで動くこと +6. **出力の規則**: CLI の出力・ログ・例外メッセージに,対応表の患者 ID・元のファイル名(既定)が現れないこと(ダミーの患者 ID・ファイル名を仕込んで確認する) +7. **設定ファイル**: 未知のキー・必須キーの欠落が `ValidationError` になること +8. **マイグレーション**: 空の DB に全マイグレーションを適用でき,`PRAGMA user_version` が最新になること.再適用しても壊れないこと + +## テストを必須としない対象 (Optional) + +- **CLI のエントリポイント**: 主要な流れ(init → import → freeze → export)を統合テスト 1 本でカバーする +- **受け渡し(`deliver`)の SSH 部分**: コマンドの組み立てだけを検証する(下記) + +## モック方針 (Mocking) + +- **SSH・scp はモックする**.テストが研究室 PC・計算サーバに接続してはならない(組み立てたコマンド・引数を検証する) +- **ファイル I/O・SQLite はモックしない**.`tmp_path` に実ファイル・実 DB を作る +- 日時に依存するテストは日時を引数で渡すか固定する + +## /implement パイプラインでのテスト (Pipeline) + +1. **Phase 1(実装 / coder)**: 機能実装 → 人間が動作確認 +2. **Phase 2(テスト / tester)**: 本方針に従いテストを追加 +3. **Phase 3(リファクタ / refactorer)**: テストを safety net として品質改善 +4. **Phase 4**: ドキュメント更新 + +Phase 2 でテストが書きにくいと感じたら設計が悪いシグナルである.Phase 3 で副作用と純関数を分け直す. + +## テスト失敗時の運用 (On Failure) + +- 仕様変更による失敗: 先に SPEC を更新してからテストを書き直す +- バグ起因の失敗: 修正してから再実行する + +## 参考 (References) + +- [pytest documentation](https://docs.pytest.org/) diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..bd93da3 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,26 @@ +# 依存パッケージの定期更新 PR(Dependabot version updates) +# 脆弱性検出(Dependabot alerts / security updates)はリポジトリ設定で有効化済み(本ファイルとは独立に動く) +version: 2 +updates: + - package-ecosystem: "uv" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + commit-message: + prefix: "[update]" + groups: + minor-and-patch: + update-types: + - "minor" + - "patch" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + commit-message: + prefix: "[update]" + groups: + actions: + patterns: + - "*" diff --git a/.gitignore b/.gitignore index 7f9cbb5..cfdb5c3 100644 --- a/.gitignore +++ b/.gitignore @@ -25,9 +25,31 @@ # tests/test_hooks.py が一時的に作る作業ディレクトリ(テストが強制終了された場合の取り残し対策) .claude/hooks/tests/_restrict_sandbox_*/ -# -- プロジェクト立ち上げ時に有効化する(.claude/rules/git-conventions.md 参照) -- -# .env -# .env.* -# node_modules/ -# build/ -# dist/ +# -- プロジェクト固有(TIASDataHub) -- +# 環境変数・ローカル設定 +.env +.env.* + +# Python(uv) +.venv/ +build/ +dist/ +*.egg-info/ +.pytest_cache/ +.ruff_cache/ + +# 臨床データ・目録をリポジトリに入れない(PLAN_01「制約」). +# 本来はリポジトリの外に置くが,誤って配下に置いた場合の保険 +/data/ +*.sqlite +*.sqlite3 +*.db +*.db-journal +*.db-wal +*.db-shm + +# CLI が書き出す manifest 等の出力(ダミーデータでの確認時を含む) +/exports/ + +# ダミーデータの生成先 +/dummy/ diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/CLAUDE.md b/CLAUDE.md index 54548e4..1f3ff21 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,15 +1,23 @@ -# プロジェクト名 +# TIASDataHub - +舌診研究のデータ(舌画像・ラベル・派生データ等)を研究室 PC で一元管理する共有データ基盤.舌診研究の各リポジトリ(TIASJudgeModel・TIASjudge・DeepTIAS 等)はこのリポジトリをサブモジュール/パッケージとして参照し,データセット名を指定してデータを取り出す. + +- 技術スタック: Python 3.12・uv,目録は SQLite(標準 `sqlite3`)+ファイルのストア(SHA-256 名),実行時の依存は pydantic のみ.CLI `tiasdata`(argparse) +- 運用: 原本と目録は研究室 PC,学習は計算サーバ(Newton・Archimedes)へ版ごとのバンドルで渡す.開発は手元 PC(ダミーデータのみ) ## 開発進捗 -最新: GUIDE_01 に従いプロジェクト立ち上げ中 +最新: プロジェクト立ち上げ完了.次は PLAN_02 Step 1(設定と目録の土台) ※ 本欄は**最新ステップ 1 行のみ上書き更新**.詳細な進捗履歴(動機・設計判断・失敗パターン)は [docs/PROGRESS.md](docs/PROGRESS.md) に追記する.運用ルールは `.claude/rules/progress-log.md` を参照(該当ファイル編集時に自動ロードされる). ## 必須ルール(コード実装時) - +### 臨床データの扱い(最重要) + +- データ本体・実際の患者 ID ↔ 研究用 ID の対応表をリポジトリに入れない.手元 PC に臨床データを置かない.リポジトリに置くのはスキーマ・ライブラリ・CLI・手順書・ダミーデータだけ(PLAN_01「制約」) +- Claude はデータの中身を読まない.研究室 PC は手元 PC から `ssh labpc` で操作し,SSH の先でデータの中身・ファイル一覧を表示しない(`.claude/rules/lab-pc-operation.md`.常時ロードされる) +- CLI の出力・ログ・例外メッセージには件数・研究用 ID・SHA-256・種類名・所見の値だけを出す(SPEC_03「出力の規則」) +- データの場所をコードに書かない.凍結した版・所見の行を書き換えない(`.claude/rules/coding-style.md`) ### Git 運用 @@ -67,19 +75,25 @@ - プロジェクト立ち上げフロー: docs/01_GUIDE/GUIDE_01_プロジェクト立ち上げフロー.md - エージェント運用ルール: docs/01_GUIDE/GUIDE_02_エージェント運用ルール.md - ※ Git 規約・ドキュメント書式・命名規則・進捗記録・テンプレート改変記録は `.claude/rules/`(git-conventions / markdown-style / docs-naming / progress-log / template-customization)に定義されている(git-conventions は常時,他は該当ファイル編集時に自動ロード) -- ※ コーディング規約,テスト方針等はプロジェクト立ち上げ時に path-scoped rules(`.claude/rules/`)として作成する(GUIDE_01 参照) +- ※ プロジェクト固有の規約は `.claude/rules/` の coding-style / project-structure / testing / error-handling(該当ファイル編集時に自動ロード)と lab-pc-operation(常時ロード) ### 02_ENV(環境) - +- 技術スタック: docs/02_ENV/ENV_01_技術スタック.md +- 環境構築手順: docs/02_ENV/ENV_02_環境構築手順.md +- 管理者用環境構築手順: docs/02_ENV/ENV_03_管理者用環境構築手順.md +- 開発コマンド: docs/02_ENV/ENV_04_開発コマンド.md ### 03_PLAN(計画) - +- 要件定義書: docs/03_PLAN/PLAN_01_要件定義書.md +- 開発ステップ: docs/03_PLAN/PLAN_02_開発ステップ.md ### 04_SPEC(仕様・設計) - +- アーキテクチャ設計: docs/04_SPEC/SPEC_01_アーキテクチャ設計.md +- データモデル: docs/04_SPEC/SPEC_02_データモデル.md +- データセットと受け渡し: docs/04_SPEC/SPEC_03_データセットと受け渡し.md ### 05_TECH(技術設計) diff --git a/README.md b/README.md new file mode 100644 index 0000000..aae6dce --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# TIASDataHub + +舌診研究のデータを研究室 PC で一元管理する共有データ基盤.各リポジトリはデータセット名を指定してデータを取り出す. + +- 要件: [docs/03_PLAN/PLAN_01_要件定義書.md](docs/03_PLAN/PLAN_01_要件定義書.md) +- 環境構築: [docs/02_ENV/ENV_02_環境構築手順.md](docs/02_ENV/ENV_02_環境構築手順.md) + +※ このリポジトリには臨床データ・患者 ID の対応表を入れない. diff --git "a/docs/01_GUIDE/GUIDE_03_\343\203\201\343\203\274\343\203\240\351\226\213\347\231\272\343\203\253\343\203\274\343\203\253.md" "b/docs/01_GUIDE/GUIDE_03_\343\203\201\343\203\274\343\203\240\351\226\213\347\231\272\343\203\253\343\203\274\343\203\253.md" deleted file mode 100644 index 7561097..0000000 --- "a/docs/01_GUIDE/GUIDE_03_\343\203\201\343\203\274\343\203\240\351\226\213\347\231\272\343\203\253\343\203\274\343\203\253.md" +++ /dev/null @@ -1,152 +0,0 @@ -# チーム開発ルール (Team Development Rules) - -チーム全員が Claude Code を用い,人間が直接コードを書くことはほぼない前提で,並行開発を破綻させずに進めるための協調ルールを定義する. - -> 本ガイドは**チーム開発モード**(`/setup` で team を選択したプロジェクト)でのみ有効.個人開発モード(solo)のプロジェクトには本ファイルは配置されない.なお `/task-create`・`/task-start`・`/task-handoff` は共通層の skill であり,solo でも Issue ベースのタスク管理に使用できる(その場合,本ガイドのチーム協調ルールは適用されない). - -Git の規約は `.claude/rules/git-conventions.md`(push・PR・マージ手順は `.claude/skills/commit/reference.md`),1 タスクの実装パイプラインは [GUIDE_02](GUIDE_02_エージェント運用ルール.md) に従う.本ガイドはそれらの上に立つ「複数人が同時に動くときの調整」を扱う. - -## 基本方針 (Basic Policy) - -- 全員が Claude Code で開発し,実装はエージェントが行う(チーム規模はプロジェクトによる). -- 原則として並行作業を行わず,1 人ずつ直列で開発する.急ぎではないため,スループットより単純さと `main` の整合性を優先する.これは禁止ではなく既定のスタンスであり,状況に応じた判断は妨げない(詳細は「作業の直列化」). -- 人間の本質的な役割は「AI にできない判断と確認」(GUIDE_02 と同じ).加えて「共有設定の番人」を担う. -- 進捗・タスクは GitHub Issues と git 履歴で追う.`CLAUDE.md` には進捗を書かない(この点は solo 既定の進捗記録ルール `.claude/rules/progress-log.md` を**上書き**する). -- `main` は常に動作可能な状態を保つ(`.claude/rules/git-conventions.md`).壊れたら他の作業より優先して直す. - -## タスク管理 (Task Management) - -進捗管理の補助として GitHub Issues と Projects を用いる.`gh` コマンドでの具体的な操作手順(スコープ・Projects 操作・トラブル対応)は `.claude/skills/task-start/reference.md` を参照. - -### Issue (Issue) - -Issue は**進捗管理の補助**として使う.追跡したい作業に対して立て,PR と紐付けてボードで可視化する. - -- 作業 1 単位 = 1 Issue を推奨する.既に Issue がある作業は,新たに作らずそれに従って進める(重複 Issue を作らない). -- **Issue は着手・PR 作成・マージのいずれの段階でも必須としない**.小さい変更(typo・1 行修正等)や試験的・探索的な作業は Issue 無しで進めてよい. -- ただし**追跡する価値のある作業には Issue を立てるのが望ましい**(後追いと進捗可視化のため).特に複数回の `/implement` に跨る作業や,他メンバーに見えていてほしい作業は Issue を作る. -- PR と Issue を紐付ける場合は本文に `Closes #` を記載する.マージ時に Issue が自動クローズされ,ボードのカードも `Done` へ移る. -- Issue 本文には少なくとも「やること」を書く.「完了条件(受け入れ基準)」は非自明な作業のみで構わない. -- 担当者は明確にしておくのが望ましい(重複着手を避けるため). -- Issue は最初に書き切らなくてよい.作業途中の進捗・確定事項は `/task-handoff` の進捗メモとして Issue に集約し,完了は PR の `Closes #<番号>` によるマージ時の自動クローズで反映する(`/implement` のフローでは Issue を更新しない). - -### Project ボード (Project Board) - -> ⚠ **既定では未使用**.タスク管理は Issue ベース(`gh issue list`)で運用する.Project ボードはチームが拡大したり Issue が常時 10 件超になった等の局面で再導入する想定.以下は将来導入時の参考. - -- 列: `Todo` / `In Progress` / `In Review` / `Done`. -- Issue をカードとして配置し,状態を移動して可視化する. -- マージ連動の自動化を設定し,カード移動の手作業を最小化する(設定は管理者作業.「管理者の初期設定」参照). - -### タスクの流れ (Task Flow) - -Issue を立てる場合の標準フロー(小さい変更や探索的作業はこの限りでない). - -```text -1. Issue 起票(完了条件を明記) -2. 自分にアサイン(ボード未使用のためアサインのみ.ボード運用復活時は In Progress へ移動) -3. main から作業ブランチ作成(git-conventions ルール) -4. /implement に Issue の内容を渡して実装(GUIDE_02) -5. PR 作成,本文に "Closes #<番号>" -6. セルフマージ条件を確認 → マージ -7. マージで Issue 自動クローズ → カード自動 Done -``` - -- 上記 1 は `/task-create`(事前定義用),2 と 3 は `/task-start `(既存 Issue から着手)で半自動化できる.Issue 無しで進める場合はどちらも不要. -- Claude へのタスク文脈は `CLAUDE.md` ではなく Issue から渡す.`gh issue view ` で取得する. - -## ブランチと PR (Branch & PR) - -`.claude/rules/git-conventions.md` に従う.チーム運用上の追加事項のみ以下に定める. - -- ブランチ命名は git-conventions ルールの基本形式(`[プレフィックス][概要]`)に従う.ブランチ名に Issue 番号は含めなくてよい. -- branch↔Issue の紐付けは PR 本文の `Closes #<番号>` で行う.マージ時に Issue が自動クローズされる.既存 Issue からブランチを作る場合は `gh issue develop` でネイティブにリンクできる(`.claude/skills/task-start/reference.md`). -- PR の `--title` はコミットメッセージと同じ書式(`[タグ] 内容`,`.claude/rules/git-conventions.md`). - -## レビューとマージ (Review & Merge) - -誰も手書きしない体制では,マージ条件が唯一の品質ゲートとなる.本プロジェクトは**条件付きセルフマージ**を採用する.他メンバーの Approve 待ちは原則作らない代わりに,作者自身が満たすべき条件を実質的なゲートとして定める. - -### 通常の機能 PR (Feature PR) - -次を全て満たせば,作者がセルフマージしてよい. - -- CI で全テストが緑である. -- 作者自身が Phase 1 の動作確認(実機・ブラウザ等,AI にできない確認)を済ませている(GUIDE_02). -- `/implement` のパイプラインをスキップせず完走している. -- 対応 Issue がある場合は完了条件を満たしている(Issue 無しの PR は不要). -- 共有設定(`CLAUDE.md` のルール部・`.claude/`)の変更を含まない. - -※ レビューは任意.他メンバーはコメントしてよいが,マージをブロックしない. - -### 共有設定を変更する PR (Shared Config PR) - -`CLAUDE.md` のルール部や `.claude/` を変更する PR は,全員の Claude の挙動を変えるため例外とする. - -- 機能 PR に混ぜず,専用 PR とする(「共有設定の扱い」参照). -- 他メンバー 1 名の Approve を必須とする(セルフマージ不可). - -### Dependabot の依存更新 PR (Dependabot PR) - -Dependabot が作成する依存更新 PR(security updates / version updates)は,人間が書いた PR ではないため上記の「`/implement` 完走」「Phase 1 動作確認」を満たせない.代わりに `/deps-update` のゲートを品質ゲートとする(`.claude/skills/deps-update/SKILL.md`). - -- **`/deps-update` がマージしてよいもの**: メジャー更新でなく,競合が無く,CI 緑(CI 未構築ならローカル検証緑)の PR.`gh pr merge --merge` で取り込む.これは「AI が `main` へマージする」唯一の例外である(GUIDE_02「コミットルール」). -- **人間が判断するもの**: メジャー更新(`0.x` のマイナー更新を含む),CI 赤,修正版の無い alert,Dependabot が PR を作れなかった alert.`/deps-update` が影響分析と推奨(そのままマージ/`/implement` で追従/`@dependabot ignore` で見送り)を PR コメントと完了報告に出すので,それを見て決める. -- 依存更新 PR は共有設定(`CLAUDE.md`・`.claude/`)を変更しないため,他メンバーの Approve は不要.`main` にブランチ保護(必須レビュー)を設定している場合は `/deps-update` はマージできないので,マージ可と判定した一覧を提示するにとどまり,人間がマージする. -- `/deps-update` を実行するのは原則 1 名(管理者またはその週の担当)とし,マージ後はチームに周知して全員が `git pull` する(直列運用のため,他メンバーの作業ブランチは次の rebase で追従する). - -### CI 構築までの暫定ゲート (Interim Gate) - -CI が未構築の間は「CI 緑」を以下で代替する.CI 構築後はこの節を削除する. - -- `/implement` を完走している. -- ローカルで全テストが緑である. -- 作者自身が Phase 1 の動作確認を済ませている. - -## 共有設定の扱い (Shared Configuration) - -`CLAUDE.md` と `.claude/`(agents・skills・rules・hooks・settings.json・template-overrides.md)は全員の Claude の挙動を決める共有インフラである. - -- **git 追跡を継続する**.untrack や `.gitignore` 化はしない(履歴の保全と挙動の統一のため). -- **進捗は `CLAUDE.md` に書かない**.進捗は Issues と git 履歴で追う(「タスク管理」参照). -- **変更は「チームルール変更」カテゴリ**として扱う. - - 機能 PR に混ぜない.専用 PR とする(ブランチは `chore/` または `docs/`). - - 他メンバー 1 名の Approve を必須とする. - - マージ後はチームに周知し,全員が `git pull` して Claude セッションを再起動する. - - ※ Claude はセッション開始時に `CLAUDE.md` を読むため,古いまま動くと文脈がずれる. -- **例外: ドキュメント一覧への追記**は,そのドキュメントを追加する PR と同じ PR で行ってよい.衝突しても両方の行を残せば解決できる. -- **個人・マシン固有の設定**は `.claude/settings.local.json` に置く.これは `.gitignore` 対象とし,共有の `settings.json` を個人都合で汚さない. - -## 作業の直列化 (Serialized Work) - -- 原則として,同時に進行中のコード作業は 1 件とする(進行中の Issue は原則 1 件.Project ボード運用復活時は `In Progress` 列も同様). -- 次の作業に着手してよいのは,前の作業の PR が `main` にマージされた後とする(マージ条件は「レビューとマージ」参照). -- タスクは小さく保つ.直列運用では,1 単位が小さいほど次の人の待ち時間が短くなる. -- 直列運用により並行コンフリクトと共有設定のドリフトは原理的にほぼ発生しない.例外的に並行が生じてコンフリクトした場合は `.claude/skills/commit/reference.md` の rebase 手順をフォールバックとして用いる. -- `main` を壊した場合は他作業より優先して復旧する.自分で対処できない場合は直ちにチームへ報告する(`.claude/skills/commit/reference.md`「間違えて main にコミットしてしまった場合」). - -## メンバーのオンボーディング (Onboarding) - -新メンバー参加時の手順. - -1. リポジトリへのコラボレーター招待を承認する(GitHub の通知,または `https://github.com///invitations`). -2. リポジトリを clone する. -3. `ENV_02_環境構築手順.md`(GUIDE_01 参照,立ち上げ時に作成)に従い開発環境を構築する. -4. `gh auth login` で GitHub CLI を認証する.Projects も操作する場合は `project` スコープを含める(`.claude/skills/task-start/reference.md`「必要なスコープ」). -5. 必要なら `.claude/settings.local.json` を用意する(個人設定.git 追跡されない). -6. 最初に `CLAUDE.md` と `docs/01_GUIDE/` を読む(特に本ガイドと GUIDE_02).Git 規約は `.claude/rules/git-conventions.md` にある. -7. `.gitignore` が整備されていることを確認する(`.claude/rules/git-conventions.md`). - -## 管理者の初期設定 (Admin Setup) - -以下は外部サービス操作を含み,原則として AI には実施できない(`gh` で操作できる設定のみ例外として注記する).ルール確定後に管理者(人間)が一度だけ行う. - -- GitHub リポジトリの作成,メンバー招待. - - リポジトリ作成後,**GitHub リポジトリのセキュリティ設定**(Dependabot alerts / security updates の有効化)は AI が `gh api` で行う(`/setup` の Phase 3・7 で実行.コマンド・検証・トラブル対応は `.claude/skills/setup/reference.md`「GitHub リポジトリのセキュリティ設定」,背景は GUIDE_01「環境構築」).`/setup` 完了後にリポジトリを作成した場合は,同 `reference.md` または `ENV_03_管理者用環境構築手順.md` に転記したコマンドを実行する(管理者が直接実行してもよい). -- (既定では未使用)GitHub Project の作成,列(`Todo` / `In Progress` / `In Review` / `Done`)の定義. -- (既定では未使用)マージ連動の自動化設定(PR マージ時に Issue クローズ・カードを `Done` へ移動). -- Issue テンプレートの用意(「やること」「完了条件」欄). -- 共有設定変更 PR に他メンバー Approve を求める運用の周知(必要なら `main` のブランチ保護設定). -- CI の構築(全テストを実行し,緑をマージ条件にできる状態にする). - -※ 完了するまでは本ガイドの「CI 構築までの暫定ゲート」を適用する. diff --git "a/docs/02_ENV/ENV_01_\346\212\200\350\241\223\343\202\271\343\202\277\343\203\203\343\202\257.md" "b/docs/02_ENV/ENV_01_\346\212\200\350\241\223\343\202\271\343\202\277\343\203\203\343\202\257.md" new file mode 100644 index 0000000..78fcebc --- /dev/null +++ "b/docs/02_ENV/ENV_01_\346\212\200\350\241\223\343\202\271\343\202\277\343\203\203\343\202\257.md" @@ -0,0 +1,67 @@ +# 技術スタック (Tech Stack) + +## 概要 (Overview) + +研究室 PC(Windows)に置いた**目録(SQLite の DB ファイル 1 つ)**と**データ置き場(画像等のファイル)**を,Python のライブラリと CLI で操作する.使う側の Python プロジェクトはライブラリを import し,Python 以外や計算サーバには CLI が書き出した manifest(CSV)を渡す.手元 PC では臨床データの代わりにダミーデータで同じコードを動かす. + +使う側(TIASJudgeModel・TIASjudge)と同じ言語・ツールにそろえ,サブモジュールとして編集可能な形で読み込んだときに依存がぶつからないようにする. + +## 言語・実行環境 (Language & Runtime) + +| 項目 | 採用 | 備考 | +| --- | --- | --- | +| 言語 | Python | 使う側(TIASJudgeModel・TIASjudge)と同じ | +| Python バージョン | 3.12(`.python-version` で固定) | 使う側と同じ.`requires-python = ">=3.12"` | +| パッケージマネージャ | **uv** | 使う側と同じ.`uv.lock` をコミットする | +| パッケージ名 | `tiasdata`(`src/tiasdata/`) | 使う側は `tiasdata = { path = "external/TIASDataHub", editable = true }` で読み込む | +| 手元 OS | Windows 11 | 開発・ダミーデータでのテストのみ | +| 運用環境 | 研究室 PC(Windows) | 目録とデータ置き場を置き,CLI を実行する | + +## 目録とデータ置き場 (Catalog & Storage) + +| 項目 | 採用 | 備考 | +| --- | --- | --- | +| 目録 | **SQLite**(Python 標準の `sqlite3`) | サーバを立てずに DB ファイル 1 つで済む.1 人で使うため同時書き込みの心配がない.C#(DeepTIAS)からも `Microsoft.Data.Sqlite` で読める | +| DB へのアクセス | 標準の `sqlite3` で SQL を直接書く(ORM は使わない) | テーブル数が少なく,SQL がそのまま仕様として読める.依存を増やさない | +| スキーマの変更 | 連番の SQL ファイル(`migrations/0001_*.sql` …)を順に適用し,適用済みの版を DB の `PRAGMA user_version` に記録する | データの種類を足すたびに表・列が増えるため,変更履歴をファイルで残す.Alembic 等は 1 ファイルの SQLite には過大 | +| データ本体 | **ファイルのまま**データ置き場に置き,目録にはデータ置き場からの相対パスを記録する | 画像を DB に入れない(DB が肥大化し,サーバへのコピーや他ツールからの参照がしにくくなる) | +| 同一性の確認 | ファイルの **SHA-256**(標準の `hashlib`) | データセットの版にハッシュを含め,ファイルが差し替わったことを検出する.再現性の根拠にする | +| 置き場所の設定 | 設定ファイル(TOML.標準の `tomllib` で読む)または環境変数で,DB ファイルとデータ置き場のパスを指定する | コードにパスを書かない.設定ファイルはリポジトリの外(研究室 PC のユーザー領域)に置く.具体的なキー・既定の場所は仕様設計で決める | + +## ライブラリ・CLI (Library & CLI) + +| 項目 | 採用 | 備考 | +| --- | --- | --- | +| 設定・入出力の検証 | **pydantic** | 使う側(TIASJudgeModel)と同じ流儀.設定ファイル・manifest の行・登録時の入力を検証する | +| CLI | 標準の `argparse`(エントリポイントは `pyproject.toml` の `[project.scripts]`) | サブコマンド数が少ない.依存を増やさない | +| manifest | CSV(標準の `csv`)+版の情報を書いた JSON | TIASJudgeModel の現行 manifest(`image_path`・`patient_id`・`label`・`severity`・`note`)と互換にできる形にする.列の詳細は仕様設計で決める | +| 画像の読み込み | 使わない(基盤は画像の中身を読まない) | 画素を扱うのは使う側.ダミーデータの生成だけ Pillow を開発用の依存に入れる | + +- 実行時の依存は **pydantic だけ**にする(使う側の依存と衝突しにくくするため) + +## 開発ツール (Development Tools) + +| 項目 | 採用 | 備考 | +| --- | --- | --- | +| テスト | pytest | ダミーデータ(テスト内で一時ディレクトリに作る DB とファイル)で動くテストのみ.臨床データに依存するテストは書かない | +| lint / format | ruff | 使う側と同じ | +| CI | **使わない** | 1 人開発でローカル実行で足りる.必要になれば GitHub Actions を追加する | +| 依存更新 | Dependabot(`uv`・`github-actions` エコシステム) | 週 1 回.CI が無いので更新 PR はローカル検証で確認する | + +## 採用しなかった選択肢 (Rejected Alternatives) + +| 候補 | 却下理由 | +| --- | --- | +| PostgreSQL 等の DB サーバ | 1 人で使う規模に対し,研究室 PC でサービスを常駐・保守する負担が大きい | +| DB を使わず CSV・YAML だけで目録を持つ | 症例 → 撮影 → ラベル・派生データの関係と,データセットの版をファイルの組み合わせで整合させるのが難しい.重複・参照切れを DB の制約で防げない | +| 画像を DB に格納(BLOB) | DB ファイルが巨大になり,計算サーバへのコピー・DeepTIAS 等のファイル前提のツールからの参照がしにくい | +| SQLAlchemy + Alembic | テーブル数が少なく 1 ファイルの SQLite には過大.使う側に余計な依存を持ち込む | +| DVC・git-annex 等のデータのバージョン管理ツール | データ本体をリポジトリの管理下(キャッシュ・リモート)に置く仕組みで,「データをリポジトリに入れない」方針と相性が悪い.必要なのは「どのファイルの集合か」の記録で,目録の版とハッシュで足りる | +| typer・click | `argparse` で足りる規模.依存を増やさない | + +## 未確定事項 (Open Items) + +- 研究室 PC に uv・Python 3.12 が入っているか(環境構築フェーズで確認する) +- 設定ファイルの置き場所・キー,DB ファイルとデータ置き場の具体的なパス(仕様設計・環境構築フェーズで決める.OneDrive の同期対象外に置く) +- DB ファイルのバックアップ方法(環境構築フェーズで決める) +- DeepTIAS(C#)が目録を直接読むか,manifest だけを読むか(仕様設計フェーズで決める.当面は manifest) diff --git "a/docs/02_ENV/ENV_02_\347\222\260\345\242\203\346\247\213\347\257\211\346\211\213\351\240\206.md" "b/docs/02_ENV/ENV_02_\347\222\260\345\242\203\346\247\213\347\257\211\346\211\213\351\240\206.md" new file mode 100644 index 0000000..ea2ab7f --- /dev/null +++ "b/docs/02_ENV/ENV_02_\347\222\260\345\242\203\346\247\213\347\257\211\346\211\213\351\240\206.md" @@ -0,0 +1,107 @@ +# 環境構築手順 (Environment Setup) + +環境の再構築・別の PC での作業開始時に使う手順.プロジェクト作成時に一度だけ行う設定は [ENV_03](ENV_03_管理者用環境構築手順.md) にまとめる. + +## 前提ツール (Prerequisites) + +| ツール | 用途 | 備考 | +| --- | --- | --- | +| git | バージョン管理 | 運用は `.claude/rules/git-conventions.md` | +| gh | GitHub CLI | 手元 PC のみ.PR 作成・リポジトリ設定 | +| uv | Python・依存管理 | `powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 \| iex"`(ユーザー領域 `%USERPROFILE%\.local\bin` に入る.`winget install astral-sh.uv` でもよい) | +| Python 3.12 | 実行環境 | `uv python install 3.12`(uv が管理する.`.python-version` で固定) | +| Python 3.7 以上 + bash | `.claude/hooks/` の実行 | 手元 PC のみ(研究室 PC では Claude Code を使わない).Windows は Git for Windows 付属の bash.※ Python が見つからないとアクセス制限フックが全ツールをブロックする | +| OpenSSH | 研究室 PC の操作 | Windows 11 標準.SSH の別名 `labpc`(研究室 PC.Tailscale 経由)は SmTIAS-LightSim で構築済みのものを使う | + +## 場所ごとの役割 (Roles by Location) + +| 場所 | 役割 | 臨床データ | +| --- | --- | --- | +| 手元 PC | 開発.ダミーデータでのテスト | 置かない | +| 研究室 PC(`DESKTOP-1QLOO91`.SSH の別名 `labpc`) | 目録とデータ置き場を置き,CLI で登録・manifest の書き出しを行う | 原本 | +| 計算サーバ(Newton・Archimedes) | 使う側のリポジトリが学習・解析を行う.研究室 PC から manifest と作業用コピーを受け取る | 作業用コピー | + +## 手元 PC のセットアップ (Local Setup) + +```bash +git clone https://github.com/rintoHasegawa/TIASDataHub.git +cd TIASDataHub +uv sync +uv run pytest +``` + +- `uv sync` で `.venv/` に依存(実行時は pydantic のみ.開発用に pytest・ruff・Pillow)が入る +- 手元 PC には臨床データを置かない.テストはダミーデータ(テスト内で一時ディレクトリに作る)だけで動く + +## 研究室 PC の操作方針 (Lab PC Operation Policy) + +研究室 PC は**手元 PC から SSH(`ssh labpc`)で操作する**.研究室 PC の前で作業したり,研究室 PC で Claude Code を開いたりはしない(2026-10-02 のユーザー判断). + +- 研究室 PC は `main` を取り込んで CLI を実行するだけにし,**研究室 PC では編集・コミットしない**(変更は手元 PC で作業ブランチ → PR で `main` に入れる) +- SSH 経由の操作は非対話で行う(パスワード入力・GUI が必要な操作は SSH ではできない) +- **SSH 経由でデータの中身を表示しない**.`.claude/repo-access.json` のフックは SSH の先のファイル読み取りまでは止められないため,データの確認は CLI の集計出力(件数・ハッシュの不一致・参照切れ等.患者 ID・ファイル名を含まない)で行う.`type`・`Get-Content` 等でデータ置き場・目録・対応表を直接読まない +- データの置き場所は研究室 PC 上の設定ファイルから CLI が読む.SSH のコマンドにデータのパスを書かなくて済むようにする(仕様設計で決める) +- 研究室 PC 側で人間が必要な作業: 新しいデータを研究室 PC に置くこと(USB 等で持ち込む場合),GUI でしか動かないプログラムの操作,研究室 PC の起動と Tailscale への接続の維持 + +## 研究室 PC のセットアップ (Lab PC Setup) + +GitHub から取得する.研究室 PC には個人のアカウントの認証情報を置かず,このリポジトリ専用の**読み取り専用のデプロイキー**(SSH の別名 `github-tiasdatahub`)で取得する(設定は [ENV_03「研究室 PC のデプロイキー」](ENV_03_管理者用環境構築手順.md)).手元 PC から次のように実行する(uv は非対話の SSH では PATH に入らないため絶対パスで呼ぶ). + +```bash +ssh -n labpc "cd C:\Users\Papin\source && git clone git@github-tiasdatahub:rintoHasegawa/TIASDataHub.git" +ssh -n labpc "cd C:\Users\Papin\source\TIASDataHub && C:\Users\Papin\.local\bin\uv.exe sync --no-dev" +``` + +更新するとき: + +```bash +ssh -n labpc "cd C:\Users\Papin\source\TIASDataHub && git pull && C:\Users\Papin\.local\bin\uv.exe sync --no-dev" +``` + +- ※ clone・`uv sync` は未検証(立ち上げの成果物を `main` に取り込んだ後に実行する).デプロイキーでの接続(`git ls-remote`)は 2026-10-02 に確認済み +- 研究室 PC からは取得しかできない(デプロイキーは読み取り専用).研究室 PC では編集・push しない(上記「操作方針」) +- 置き場所は `C:\Users\Papin\source\`(OneDrive の同期対象外).**ドキュメント・デスクトップ配下には置かない**(研究室 PC ではドキュメントフォルダが OneDrive にリダイレクトされている) +- 目録・ストア・対応表は `C:\Users\Papin\clinical-data\tiasdata\`・`tiasdata-identity\` に置き,場所は設定ファイル `C:\Users\Papin\.config\tiasdata\config.toml` に書く(配置と設定のキーは [SPEC_01「配置」「設定ファイル」](../04_SPEC/SPEC_01_アーキテクチャ設計.md)).設定ファイルの作成と `tiasdata init` は実装後に行う ※ 未検証 + +## 使う側のリポジトリへの組み込み (Using from Other Repositories) + +Python のプロジェクトでは,本リポジトリを git サブモジュールとして置き,編集可能なパッケージとして読み込む. + +```bash +git submodule add https://github.com/rintoHasegawa/TIASDataHub.git external/TIASDataHub +``` + +```toml +# 使う側の pyproject.toml +[project] +dependencies = ["tiasdata"] + +[tool.uv.sources] +tiasdata = { path = "external/TIASDataHub", editable = true } +``` + +- サブモジュールは**本リポジトリの `main` にマージ済みのコミット**を指す.使う側の作業中に基盤を直した場合は,サブモジュールの中で作業ブランチを切って本リポジトリに PR を出し,マージ後にサブモジュールの参照を更新する +- ※ 未検証(使う側への組み込みは実装フェーズで行う) + +## 臨床データの取り扱い (Clinical Data Handling) + +**手順ではなく,守るべき制約である.** + +- データ本体と,実際の患者 ID ↔ 研究用 ID の対応表はリポジトリに入れない.`.gitignore` で `/data/`・`*.sqlite`・`*.db`・`/exports/` 等を除外しているが,そもそもリポジトリの配下に置かない +- 手元 PC に臨床データを置かない +- Claude にデータの中身を読ませない.`.claude/repo-access.json` の `deny_read` に研究室 PC のデータフォルダ(`C:/Users/Papin/clinical-data`)とリポジトリ内の `data/`・`exports/` を入れている.データ置き場・DB の場所を追加・変更したら,このリストも更新する + +## 環境再現性のルール (Reproducibility Rules) + +- 依存の追加は `uv add`(開発用は `uv add --dev`).`uv.lock` はコミットする +- 実行時の依存は最小限にする(使う側に入るため.[ENV_01](ENV_01_技術スタック.md)) +- データの置き場所をコードに書かない(設定ファイル・環境変数で渡す) + +## トラブルシューティング (Troubleshooting) + +| 症状 | 原因 | 対処 | +| --- | --- | --- | +| `python --version` が `Python` とだけ出る | Microsoft Store の案内用スタブ(`WindowsApps\python.exe`)が呼ばれている | `uv python install 3.12` で入れた Python を使う.プロジェクト内では `uv run` を使えば uv が管理する Python が使われる | +| SSH で `uv` が見つからない | 非対話の SSH セッションに PATH の変更が反映されていない | `C:\Users\Papin\.local\bin\uv.exe` を絶対パスで呼ぶ | +| SSH の出力が文字化けする | 研究室 PC の出力が CP932 | 手元で `iconv -f cp932 -t utf-8` を通す | +| ファイル操作系のツールが全てブロックされる | フック用の Python が見つからない | `python3` / `python` / `py` のいずれかが起動できるようにする | diff --git "a/docs/02_ENV/ENV_03_\347\256\241\347\220\206\350\200\205\347\224\250\347\222\260\345\242\203\346\247\213\347\257\211\346\211\213\351\240\206.md" "b/docs/02_ENV/ENV_03_\347\256\241\347\220\206\350\200\205\347\224\250\347\222\260\345\242\203\346\247\213\347\257\211\346\211\213\351\240\206.md" new file mode 100644 index 0000000..fb8a1be --- /dev/null +++ "b/docs/02_ENV/ENV_03_\347\256\241\347\220\206\350\200\205\347\224\250\347\222\260\345\242\203\346\247\213\347\257\211\346\211\213\351\240\206.md" @@ -0,0 +1,108 @@ +# 管理者用環境構築手順 (Administrator Setup) + +プロジェクト作成時に**一度だけ**行う初期設定. + +## GitHub リポジトリの作成 (Repository Creation) + +Private リポジトリとして作成する(臨床データは含めないが,研究途中のコードを公開しないため).2026-10-02 時点で作成済み. + +```bash +gh repo create rintoHasegawa/TIASDataHub --private --source=. --remote=origin \ + --description "舌診研究のデータを研究室 PC で一元管理する共有データ基盤" +``` + +## リポジトリのセキュリティ設定 (Repository Security Settings) + +Dependabot alerts(脆弱性検出)と Dependabot security updates は**リポジトリごとに既定で OFF** なので,作成直後に有効化する(冪等なので再実行してよい).2026-10-02 に実施し,検証済み. + +```bash +gh api -X PUT repos/rintoHasegawa/TIASDataHub/vulnerability-alerts +gh api -X PUT repos/rintoHasegawa/TIASDataHub/automated-security-fixes +``` + +検証: + +```bash +gh api repos/rintoHasegawa/TIASDataHub/vulnerability-alerts -i # 204 なら有効(404 なら無効) +gh api repos/rintoHasegawa/TIASDataHub/automated-security-fixes # {"enabled":true,...} なら有効 +``` + +※ 依存バージョンの定期更新は `.github/dependabot.yml` で設定する(`uv`・`github-actions`.リポジトリ設定とは独立に動く). + +## 研究室 PC のツール導入 (Lab PC Tools) + +2026-10-02 に手元 PC から SSH(`ssh labpc`)で実施.研究室 PC には `git` はあったが,`uv`・`gh` は無く,`python` は Microsoft Store の案内用スタブだけだった. + +```powershell +powershell -NoProfile -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" +C:\Users\Papin\.local\bin\uv.exe python install 3.12 +``` + +- uv 0.12.22・Python 3.12.15 がユーザー領域(`C:\Users\Papin\.local\bin`・`C:\Users\Papin\AppData\Roaming\uv\python\`)に入った.管理者権限は不要 +- `gh` は入れない(研究室 PC では編集・コミットしない) + +## 研究室 PC のデプロイキー (Lab PC Deploy Key) + +研究室 PC が GitHub(Private)から取得するため,このリポジトリ専用の**読み取り専用のデプロイキー**を使う(個人のアカウントの認証情報を研究室 PC に置かないため).2026-10-02 に手元 PC から SSH で実施. + +1. 研究室 PC で鍵を作る(パスフレーズなし) + + ```bash + ssh -n labpc 'ssh-keygen -q -t ed25519 -f C:\Users\Papin\.ssh\id_ed25519_tiasdatahub -N "" -C papin-DESKTOP-1QLOO91-TIASDataHub-deploy' + ``` + +2. 公開鍵をデプロイキーとして登録する(手元 PC の `gh`.既定で読み取り専用) + + ```bash + ssh -n labpc 'type C:\Users\Papin\.ssh\id_ed25519_tiasdatahub.pub' > labpc_deploy.pub + gh repo deploy-key add labpc_deploy.pub --repo rintoHasegawa/TIASDataHub --title "labpc DESKTOP-1QLOO91 (read-only)" + ``` + +3. 研究室 PC の `C:\Users\Papin\.ssh\config` に別名を追加する + + ```text + Host github-tiasdatahub + HostName github.com + User git + IdentityFile C:/Users/Papin/.ssh/id_ed25519_tiasdatahub + IdentitiesOnly yes + ``` + +4. 接続を確かめる.初回は GitHub のホスト鍵を受け入れる(指紋が GitHub の公開値 `SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU`(ED25519)と一致することを `ssh-keygen -lF github.com` で確認した) + + ```bash + ssh -n labpc "ssh -o StrictHostKeyChecking=accept-new -T github-tiasdatahub" + ssh -n labpc "git ls-remote git@github-tiasdatahub:rintoHasegawa/TIASDataHub.git" + ``` + +- 別名を鍵ごとに分けているのは,GitHub のデプロイキーが 1 つのリポジトリにしか登録できないため(他のリポジトリを研究室 PC に置く場合は,そのリポジトリ用の鍵と別名を同様に作る) +- 鍵を作り直したら,GitHub の Settings → Deploy keys から古い鍵を削除する + +## 研究室 PC への配置 (Lab PC Deployment) + +立ち上げの成果物を `main` に取り込んだ後に行う.手順は [ENV_02「研究室 PC のセットアップ」](ENV_02_環境構築手順.md). + +- ※ 未検証(2026-10-02 時点で `main` は初期コミットのみ) + +## Claude のアクセス制限 (Claude Access Restriction) + +Claude にデータの中身を読ませないため,`.claude/repo-access.json` の `deny_read` にデータの置き場所を書く.研究室 PC は手元 PC から SSH で操作するため([ENV_02「研究室 PC の操作方針」](ENV_02_環境構築手順.md)),このフックが効くのは手元 PC のパスと,コマンド文字列に現れたパスだけである.SSH の先での読み取りは,CLI の出力規則と「SSH でデータの中身を表示しない」運用で防ぐ. + +```json +{ + "deny_read": [ + "C:/Users/Papin/clinical-data", + "data", + "exports" + ] +} +``` + +- 目録の DB ファイル・データ置き場・manifest の出力先を仕様設計で決めたら,リポジトリの外に置く場所もここに追加する + +## 人間が確認すべき事項 (Human Checklist) + +Claude は実施できない. + +- 倫理審査で定められた臨床データの保管ルール(保管場所・アクセスできる人の範囲・保管期間・計算サーバへのコピーの可否) +- 目録の DB ファイルのバックアップ方法(仕様設計で決める.バックアップ先も OneDrive の同期対象外にする) diff --git "a/docs/02_ENV/ENV_04_\351\226\213\347\231\272\343\202\263\343\203\236\343\203\263\343\203\211.md" "b/docs/02_ENV/ENV_04_\351\226\213\347\231\272\343\202\263\343\203\236\343\203\263\343\203\211.md" new file mode 100644 index 0000000..fdbbbf0 --- /dev/null +++ "b/docs/02_ENV/ENV_04_\351\226\213\347\231\272\343\202\263\343\203\236\343\203\263\343\203\211.md" @@ -0,0 +1,33 @@ +# 開発コマンド (Development Commands) + +日常の開発で使うコマンドの一覧.コマンドを推測せず,ここに書かれたものを使う. + +## セットアップ (Setup) + +| コマンド | 用途 | +| --- | --- | +| `uv sync` | 依存を `.venv/` に入れる(開発用の依存を含む) | +| `uv sync --no-dev` | 実行時の依存だけを入れる(研究室 PC) | + +## テスト・lint (Test & Lint) + +| コマンド | 用途 | +| --- | --- | +| `uv run pytest` | テストを実行する(ダミーデータのみ) | +| `uv run ruff check .` | lint | +| `uv run ruff format .` | 整形(確認だけなら `uv run ruff format --check .`) | + +## CLI + +| コマンド | 用途 | +| --- | --- | +| `uv run tiasdata --version` | 版を表示する | + +- サブコマンド(登録・データセットの定義・manifest の書き出し等)は実装フェーズで追加し,ここに追記する + +## 依存の追加 (Dependencies) + +| コマンド | 用途 | +| --- | --- | +| `uv add <パッケージ>` | 実行時の依存を追加する(使う側に入るため最小限にする) | +| `uv add --dev <パッケージ>` | 開発用の依存を追加する | diff --git "a/docs/03_PLAN/PLAN_01_\350\246\201\344\273\266\345\256\232\347\276\251\346\233\270.md" "b/docs/03_PLAN/PLAN_01_\350\246\201\344\273\266\345\256\232\347\276\251\346\233\270.md" new file mode 100644 index 0000000..d15b896 --- /dev/null +++ "b/docs/03_PLAN/PLAN_01_\350\246\201\344\273\266\345\256\232\347\276\251\346\233\270.md" @@ -0,0 +1,145 @@ +# 要件定義書 (Requirements) + +## プロジェクトの目的 (Purpose) + +舌診研究のデータ(舌画像・ラベル・派生データ等)を研究室 PC でまとめて管理する**共有のデータ基盤**を作る. + +舌診研究のデータを使うリポジトリは今後も増える.各リポジトリが個別に研究室 PC のフォルダを読む形をやめ,本リポジトリを**共通の入口**にする.各リポジトリは「データセット名」を指定してデータを取り出す. + +- 現在あるデータは舌色の画像とラベル(5 分類+程度)だけだが,今後ほかの種類のデータを増やす.**データの種類が増えても骨組みを作り直さずに済む**構造にする +- 置き場所・データ集合の版・派生データの由来を基盤側で管理し,各リポジトリの実験の**再現性**と**由来の追跡**を支える + +## 対象ユーザー (Target Users) + +| 区分 | 対象 | +| --- | --- | +| 直接 | 開発者本人(hasegawa)のみ | +| 使う側のリポジトリ | TIASJudgeModel(舌色分類モデルの学習),TIASjudge(舌色診断アプリ),DeepTIAS(舌画像の解析.C# WinForms),今後増えるもの | + +- 研究室の他メンバーが使う予定は無い.**複数人での利用(同時編集・利用者ごとの権限管理等)は考慮しない** + +## スコープ (Scope) + +### やること (In Scope) + +- **データの目録(カタログ)**: 症例・撮影・画像ファイル・ラベル・派生データと,それらの関係を記録する +- **データセットの定義と版**: 「データセット名」で取り出せるデータ集合を定義し,データを追加した後も過去と同じ集合を取り出せるようにする +- **由来(プロビナンス)の記録**: 派生データを「どの元データから,どのプログラムのどの版で」作ったかを記録する +- **アクセス用ライブラリ(Python)**: 使う側の Python プロジェクトが import して,データセット名から一覧・パスを取り出す +- **CLI**: データの登録・データセットの定義・manifest(CSV 等)の書き出し.Python 以外のプロジェクトや,目録に直接触れない計算サーバには書き出したファイルを渡す +- **置き場所の管理**: データの実際の置き場所は設定または目録で管理し,コードに書かない +- **ダミーデータ**: 臨床データの無い環境(手元 PC・CI)で,ライブラリ・CLI を同じコードパスで動かすための合成データ +- **手順書**: 研究室 PC での運用・データ登録・計算サーバへの受け渡しの手順 + +### やらないこと (Out of Scope) + +- データ本体・実際の患者 ID と研究用 ID の対応表を本リポジトリに入れること(GitHub・研究室の GitBucket のどちらにも入れない) +- 機械学習の学習・評価(TIASJudgeModel 等,使う側の仕事) +- 画像処理そのもの(色補正・セグメンテーション・特徴量の計算.DeepTIAS 等の仕事.基盤はその**結果と由来を記録する**だけ) +- 臨床データを手元 PC に置くこと +- 研究室の外から目録・データに直接アクセスする仕組み(Web API・クラウドストレージ等) + +## データ (Data) + +### 現在あるデータ (Current Data) + +| データ | 内容 | 原本の置き場所(研究室 PC) | +| --- | --- | --- | +| 舌色データセット | 舌領域を切り出した舌画像(PNG).舌色 5 分類+程度のラベルごとに 9 フォルダ・1085 枚.1 患者 1 枚(2026-09-27 確認) | `C:\Users\Papin\clinical-data\TIASJudgeModel\` | +| セグメンテーション用データ | ResUNet の開発時の画像と正解マスク | `C:\Users\Papin\clinical-data\TongueSegmentation\` | + +- 舌色データセットは DeepTIAS 2.7 の Image&Calib モードの出力(色補正後の画像を枠で切り出し,マスク外を黒にしたもの)から作られている(TIASJudgeModel の `SPEC_04`) +- ラベルは現在フォルダ名で表現されている.基盤に登録するときは,フォルダ名から読み取ったラベルを目録の項目として記録する + +### 表せるようにする関係 (Relationships) + +```text +症例(患者.研究用 ID) +└── 撮影(1 回の撮影.日時・撮影装置) + ├── 舌画像 + ├── カラーチャート画像 + ├── ラベル(舌色・程度.将来は別の所見も) + └── 派生データ(色補正後の画像・マスク・特徴量 analysis.csv 等) + └── 由来: 元データ・プログラム名・プログラムの版・作成日時 +``` + +- 現在は「1 患者 1 枚」だが,**同じ患者の複数回の撮影・複数枚**に対応する(経時的な撮影を含む) +- ラベルは 1 つの撮影に複数付きうる(所見の種類が増える,評価者が複数いる等) + +### 今後増やす候補 (Future Data Candidates) + +いずれも**現時点では作り込まず**,追加時に骨組みを変えずに足せることだけを要件とする. + +- 舌色以外の所見(苔の色・厚さ等) +- 問診・体質の情報 +- 同じ患者の経時的な撮影 +- 別の撮影装置で撮った画像 +- DeepTIAS の解析結果(色補正後の画像・マスク・特徴量 `analysis.csv`) + +## 基本方針 (Principles) + +1. **置き場所をコードに書かない**: データの置き場所は設定または目録で管理し,使う側はデータセット名で指定する.置き場所を変えても 1 か所の変更で済む +2. **再現性**: データセットは版(スナップショット)を持ち,実験が「どのデータセットのどの版」を使ったかを記録・再取得できる.データを追加しても過去の版の中身は変わらない +3. **由来の記録**: 派生データには作成したプログラムとその版を必ず記録する.DeepTIAS は旧 exe(2021 年 12 月版)と現行ソースで解析結果がずれることが分かっているため,同じ元データの派生データを版ごとに区別して保持できるようにする +4. **個人情報をリポジトリに入れない**: リポジトリに置くのはスキーマ・ライブラリ・CLI・手順書・ダミーデータのみ +5. **拡張しやすさ**: データの種類(所見・派生データの種類)の追加は,目録の項目の追加で済み,既存のテーブル構造・コードの作り直しを要しない + +## 使う側との連携 (Integration with Consumers) + +| 使う側 | 連携の方法 | +| --- | --- | +| Python のプロジェクト(TIASJudgeModel・TIASjudge 等) | 本リポジトリを **git サブモジュール**(例: `external/TIASDataHub`)として置き,`[tool.uv.sources]` で**編集可能なパッケージ**として読み込む(例: `tiasdata = { path = "external/TIASDataHub", editable = true }`) | +| Python 以外(DeepTIAS 等) | CLI で書き出した manifest 等のファイルを読む | +| 目録に直接触れない場所(計算サーバ Newton・Archimedes) | 研究室 PC で CLI が書き出した manifest と,コピーしたデータを使う | + +- 使う側の作業中に基盤側もその場で直せるよう,サブモジュールは編集可能な形で使う +- 複数のリポジトリから編集するため,基盤の変更は**本リポジトリの作業ブランチ → PR で `main` に入れる**.使う側のサブモジュールは**マージ済みのコミット**を指す +- 使う側から見たライブラリの API・manifest の列は**互換性を保つ**(変える場合は使う側への影響を手順書に書く) + +## 制約 (Constraints) + +### 個人情報・臨床データの扱い(必須) + +- **データ本体と,実際の患者 ID ↔ 研究用 ID の対応表は本リポジトリに入れない**(GitHub・研究室の GitBucket のどちらにも) +- 原本は研究室 PC の `C:\Users\Papin\clinical-data\` 配下に置く.研究室 PC のドキュメントフォルダは OneDrive に同期されるため,その配下には置かない(目録の DB ファイルも同様) +- 学習は研究室の計算サーバ(Newton・Archimedes)で行い,**臨床データを手元 PC に置かない**.手元 PC ではダミーデータで開発する +- **Claude にデータの中身を読ませない**.データフォルダは `.claude/repo-access.json` の `deny_read` に入れる +- エラーメッセージ・ログ・CLI の出力に,実際の患者 ID・個人を特定できる情報を出さない +- 倫理審査で定められた保管ルールに合わせる(詳細は要確認.下記「未確定事項」) + +### 運用環境 (Environment) + +| 場所 | 役割 | 臨床データ | +| --- | --- | --- | +| 研究室 PC | 原本と目録を置く.登録・manifest の書き出しを行う | あり(原本) | +| 計算サーバ(Newton・Archimedes) | 学習・解析.研究室 PC から作業用コピーを受け取る | あり(作業用コピー) | +| 手元 PC | 開発.ダミーデータのみ | なし | + +- リモート: `origin` = GitHub(`rintoHasegawa/TIASDataHub`,Private.メイン),`nakaguchi` = 研究室の GitBucket(コピー) + +## 成功基準 (Success Criteria) + +1. 現在の舌色データセット(1085 枚)が目録に登録され,データセット名で取り出せる +2. TIASJudgeModel が,研究室 PC の固定パスを書かずに本基盤経由で同じデータ集合(manifest)を得られる +3. データを追加した後も,過去の版のデータセットを同じ中身で取り出せる +4. 派生データに由来(元データ・プログラム・版)が記録され,同じ元データでも版の違う派生データを区別できる +5. 新しい種類の所見を 1 つ追加するとき,既存のスキーマ・コードを作り直さずに済む(ダミーデータで確認する) +6. 手元 PC ではダミーデータだけでライブラリ・CLI のテストが通る +7. リポジトリに個人情報・データ本体が含まれていない + +## 前提・未確定事項 (Assumptions & Open Questions) + +### 後のフェーズで一緒に決めること + +| 事項 | 決めるフェーズ | +| --- | --- | +| データベースの形(SQLite +画像ファイル置き場,等) | 技術選定 | +| 研究用 ID の付け方(匿名化の方法)と,対応表の置き場所・管理方法 | 仕様設計 | +| データセットの版の表し方(スナップショットの単位・識別子) | 仕様設計 | +| 計算サーバにコピーした作業用データと原本・データセットの版の対応の管理方法 | 仕様設計 | +| manifest の形式・列(TIASJudgeModel の現行 manifest との互換) | 仕様設計 | +| 最初に対応するデータの種類(舌色のみか,セグメンテーション用データ・DeepTIAS の派生データも含めるか) | 開発計画 | + +### 人間が確認すること + +- 倫理審査で定められた臨床データの保管ルール(保管場所・アクセスできる人の範囲・保管期間・計算サーバへのコピーの可否) diff --git "a/docs/03_PLAN/PLAN_02_\351\226\213\347\231\272\343\202\271\343\203\206\343\203\203\343\203\227.md" "b/docs/03_PLAN/PLAN_02_\351\226\213\347\231\272\343\202\271\343\203\206\343\203\203\343\203\227.md" new file mode 100644 index 0000000..0f58262 --- /dev/null +++ "b/docs/03_PLAN/PLAN_02_\351\226\213\347\231\272\343\202\271\343\203\206\343\203\203\343\203\227.md" @@ -0,0 +1,39 @@ +# 開発ステップ (Development Steps) + +[PLAN_01 要件定義書](PLAN_01_要件定義書.md)の成功基準に向けた実装の順序.仕様は [SPEC_01](../04_SPEC/SPEC_01_アーキテクチャ設計.md)〜[SPEC_03](../04_SPEC/SPEC_03_データセットと受け渡し.md). + +## 方針 (Policy) + +- **マイルストーン 1 は舌色データだけ**で,取り込み → 版 → バンドル → 計算サーバ → TIASJudgeModel までを一通りつなぐ(2026-10-02 のユーザー判断).セグメンテーション用データ・DeepTIAS の派生データはマイルストーン 2 以降にする +- 骨組みが種類の追加に耐えるかは,マイルストーン 1 の中でも**ダミーの所見の種類を 1 つ足すテスト**で確かめる(成功基準 5) +- 各ステップは `/implement` で進め,作業ブランチ → PR で `main` に入れる.Step 1〜5 は手元 PC だけで完結する(ダミーデータ).Step 6 以降は研究室 PC・計算サーバを使う + +## マイルストーン 1: 舌色データを基盤経由で使う (Milestone 1) + +| Step | 内容 | 完了条件 | +| --- | --- | --- | +| 1 | **設定と目録の土台**: `config.py`(設定ファイルの読み込み・検証),`catalog/`(接続・マイグレーションの適用),`migrations/0001`(SPEC_02 の全表と最初の種類),`tiasdata init` | 一時ディレクトリの設定ファイルから `init` で目録ができ,`PRAGMA user_version` が最新になる.テスト(設定ファイル・マイグレーション)が通る | +| 2 | **ストア**: `store.py`(ハッシュ・コピー・照合),テスト用のダミーデータ(小さい画像・取り込み元のフォルダ)を作る `conftest.py` | 同じ中身が 1 つにまとまり,コピー後の照合・上書きしないことがテストで確かめられる | +| 3 | **取り込みと点検**: `kinds/`(`tongue_color` の値の型),`importers/tongue_color_folders`(9 フォルダ → 症例・撮影・ファイル・所見・由来),`tiasdata import`・`stats`・`check` | ダミーの 9 フォルダを取り込め,2 回実行しても同じ結果になる.想定外の構成で停止する.出力に元のファイル名が出ない | +| 4 | **データセットと版**: `datasets/tongue_color.toml`,`datasets.py`(定義の評価・版の作成),`tiasdata dataset freeze`・`list`・`show` | 所見を足す・訂正しても過去の版の構成が変わらない.構成が同じなら版を作らない.ダミーの所見の種類を 1 つ足しても既存の表を変えずに済む | +| 5 | **バンドル**: `bundle.py`(書き出し・読み込み・検証),`tiasdata export`,公開 API(`load`・`open_bundle`・`Bundle.fingerprint`) | 書き出したバンドルを目録なしで読める.改変・欠けを検出する.同じ版から同じ `manifest.csv` が得られる | +| 6 | **研究室 PC での運用開始**: 立ち上げの成果物と Step 1〜5 を `main` に取り込む → 研究室 PC に GitHub から clone・設定ファイル作成・`init` → 舌色データ(1085 枚)を取り込み → `check`・`stats` → 版 1 を作成 → `tiasdata backup` | すべて手元 PC から SSH で行える.件数が 1085 枚・9 グループで TIASJudgeModel の TEST_01 と一致する.`check` が問題 0 件 | +| 7 | **計算サーバへの受け渡し**: `deliver.py`(tar・scp・展開・`sha256sum -c`・記録),`tiasdata deliver` | 版 1 を Newton・Archimedes に渡し,両方で照合が通り,`deliveries` に記録される.データが手元 PC を通らない | +| 8 | **TIASJudgeModel の切り替え**: TIASJudgeModel にサブモジュールを追加し,`tiasdata.load("tongue_color", version=1)` から現行の manifest の形に変換して学習・評価に使う(TIASJudgeModel 側の作業ブランチで行う) | 変換した manifest が現行の manifest と同じ構成(件数・`label`・`severity` の内訳・1 患者 1 枚)になる.実験記録にバンドルの指紋が残る.研究室 PC の固定パスを書かずに済む | + +- Step 6 の前に,立ち上げの成果物(`chore/project-setup`)を `main` に取り込む +- Step 6・7 はコード(Step 7 の `deliver`)と運用(研究室 PC・サーバでの実行)が混ざる.運用の手順と結果は ENV_02・ENV_03 に記録する +- Step 8 の終了時点で PLAN_01 の成功基準 1〜7 を確認する + +## マイルストーン 2 以降の候補 (Later Milestones) + +優先度はマイルストーン 1 の完了後に決める. + +| 候補 | 内容 | +| --- | --- | +| セグメンテーション用データ | TongueSegmentation(画像・正解マスク)を取り込む importer を追加する | +| DeepTIAS の派生データ | 撮影画像・カラーチャート画像と,DeepTIAS の出力(色補正後の画像・マスク・`analysis.csv`)を由来付きで登録する.旧 exe(2021 年 12 月版)と現行ソースの出力を版で区別する | +| 実際の患者 ID を持つデータ | 対応表(`identity.sqlite`)を使う取り込み.同じ患者の複数枚・経時的な撮影.倫理審査の保管ルールの確認が前提 | +| 他の所見 | 苔の色・厚さ,問診・体質等の種類を追加する | +| バックアップ | 目録・ストアを別の媒体に退避する方法を決めて手順化する | +| TIASjudge・DeepTIAS からの利用 | 使う側を増やす | diff --git "a/docs/04_SPEC/SPEC_01_\343\202\242\343\203\274\343\202\255\343\203\206\343\202\257\343\203\201\343\203\243\350\250\255\350\250\210.md" "b/docs/04_SPEC/SPEC_01_\343\202\242\343\203\274\343\202\255\343\203\206\343\202\257\343\203\201\343\203\243\350\250\255\350\250\210.md" new file mode 100644 index 0000000..b6e62a9 --- /dev/null +++ "b/docs/04_SPEC/SPEC_01_\343\202\242\343\203\274\343\202\255\343\203\206\343\202\257\343\203\201\343\203\243\350\250\255\350\250\210.md" @@ -0,0 +1,118 @@ +# アーキテクチャ設計 (Architecture) + +## 基本方針 (Principles) + +1. **置き場所をコードに書かない**: 目録・データ置き場・取り込み元・計算サーバの場所は,研究室 PC の設定ファイルに書く.使う側はデータセット名と版で指定する +2. **登録したファイルは変えない**: 取り込むときに管理用の置き場(ストア)へコピーし,ファイル名を中身の SHA-256 にする.同じ名前のファイルは常に同じ中身であり,過去の版を確実に再現できる +3. **版は凍結する**: データセットの版は作った時点の構成(ファイルとラベル)を記録し,以後変更しない.データやラベルを足しても過去の版の中身は変わらない +4. **由来を必ず記録する**: 派生データは,元データ・プログラム名・プログラムの版とひも付けて登録する +5. **個人情報を出さない**: 実際の患者 ID は目録に入れない(研究用 ID だけを使う).CLI の出力・ログ・エラーメッセージは集計値と研究用 ID に限る([SPEC_03「出力の規則」](SPEC_03_データセットと受け渡し.md)) +6. **拡張は行の追加で行う**: 所見の種類・ファイルの種類の追加は,種類の定義(表の行と検証用の型)の追加で済ませ,表の構造を作り直さない([SPEC_02](SPEC_02_データモデル.md)) + +## 構成要素 (Components) + +```text +研究室 PC(原本・目録) 計算サーバ(Newton・Archimedes) +┌──────────────────────────────┐ ┌──────────────────────────────┐ +│ 取り込み元(原本.読むだけ) │ │ ~/data/tiasdata/<名前>/v<版>/ │ +│ clinical-data/TIASJudgeModel │ │ manifest.csv・dataset.json │ +│ │ tiasdata import │ │ files/.<拡張子> │ +│ ▼ │ tiasdata │ ▲ │ +│ 目録 catalog.sqlite │ deliver │ │ tiasdata.load() │ +│ ストア store/ │ ────────────▶ │ 使う側(TIASJudgeModel 等) │ +│ バンドル bundles/<名前>/v<版>/ │ (直送) └──────────────────────────────┘ +└──────────────────────────────┘ + ▲ ssh labpc "tiasdata ..."(手元 PC から操作.ENV_02) +``` + +| 要素 | 役割 | +| --- | --- | +| 目録(`catalog.sqlite`) | 症例・撮影・ファイル・ラベル・由来・データセットの版・受け渡しの記録([SPEC_02](SPEC_02_データモデル.md)) | +| ストア(`store/`) | 登録したファイルの実体.中身の SHA-256 をファイル名にし,変更しない | +| 対応表(`identity.sqlite`) | 実際の患者 ID ↔ 研究用 ID.目録とは別のファイル・別のフォルダに置く.現在のデータには実際の患者 ID が無いため空 | +| バンドル | データセットの 1 つの版を,manifest とファイルのコピーにまとめたもの.計算サーバ・Python 以外のツールへ渡す単位([SPEC_03](SPEC_03_データセットと受け渡し.md)) | +| ライブラリ(`tiasdata`) | 目録の操作(研究室 PC)と,バンドルの読み込み・検証(どこでも) | +| CLI(`tiasdata`) | 取り込み・データセットの版の作成・バンドルの書き出し・受け渡し・点検(研究室 PC) | +| データセットの定義(`datasets/*.toml`) | どのファイル・ラベルを集めるかの条件.個人情報を含まないので**リポジトリで管理**し,PR で変更する | + +## 配置 (Locations) + +### リポジトリ (Repository) + +```text +TIASDataHub/ +├── src/tiasdata/ +│ ├── config.py # 設定ファイルの読み込み・検証(pydantic) +│ ├── catalog/ # 目録(SQLite)の接続・マイグレーション・各表の操作 +│ ├── store.py # ストア(ハッシュの計算・コピー・検証) +│ ├── kinds/ # ファイルの種類・所見の種類の定義(値の検証用の型) +│ ├── importers/ # 取り込み元ごとの読み取り(例: 舌色の 9 フォルダ) +│ ├── datasets.py # データセットの定義の評価・版の作成 +│ ├── bundle.py # バンドルの書き出し・読み込み・検証 +│ ├── deliver.py # 計算サーバへの受け渡し +│ └── cli.py # エントリポイント +├── migrations/ # 目録のスキーマ変更(0001_*.sql …) +├── datasets/ # データセットの定義(TOML) +├── tests/ # ダミーデータのみで動くテスト +└── docs/ +``` + +依存の向きは `cli` → `datasets`・`bundle`・`deliver`・`importers` → `catalog`・`store`・`kinds`・`config` とする.**`bundle` の読み込み側は `catalog` に依存しない**(目録の無い計算サーバで使うため). + +### 研究室 PC (Lab PC) + +```text +C:\Users\Papin\clinical-data\ +├── TIASJudgeModel\ # 取り込み元(原本.基盤は読むだけで変更しない) +├── TongueSegmentation\ # 取り込み元(原本) +├── tiasdata\ # 基盤のデータ(catalog.root) +│ ├── catalog.sqlite # 目録 +│ ├── store\ # ストア(store\<先頭 2 文字>\.<拡張子>) +│ ├── bundles\ # 書き出したバンドル(受け渡し後は削除してよい.再生成できる) +│ └── backups\ # 目録のバックアップ +└── tiasdata-identity\ # 対応表(identity.sqlite).目録とは別のフォルダ +``` + +- すべて OneDrive の同期対象外(`C:\Users\Papin\` 直下の独自フォルダ)に置く +- `.claude/repo-access.json` の `deny_read` に `C:/Users/Papin/clinical-data` が入っているため,上記はすべて Claude の読み取り禁止の対象になる + +## 設定ファイル (Configuration) + +研究室 PC のユーザー領域に置き(リポジトリには入れない),pydantic で検証する(未知のキーは拒否する). + +- 場所: 環境変数 `TIASDATA_CONFIG` があればそのパス,無ければ `%USERPROFILE%\.config\tiasdata\config.toml`(Linux では `~/.config/tiasdata/config.toml`) +- 相対パスは設定ファイルのあるフォルダを基準にする.`~` を展開する + +```toml +[catalog] +root = "C:/Users/Papin/clinical-data/tiasdata" # catalog.sqlite・store/・bundles/・backups/ +identity = "C:/Users/Papin/clinical-data/tiasdata-identity/identity.sqlite" + +[sources.tiasjudgemodel] # 取り込み元(名前で指定する) +path = "C:/Users/Papin/clinical-data/TIASJudgeModel" +importer = "tongue_color_folders" + +[servers.newton] # 受け渡し先 +host = "hasegawa@192.168.23.14" +key = "C:/Users/Papin/.ssh/id_ed25519_newton" +root = "~/data/tiasdata" + +[servers.archimedes] +host = "hasegawa@192.168.23.15" +key = "C:/Users/Papin/.ssh/id_ed25519_newton" +root = "~/data/tiasdata" +``` + +- CLI はパスを引数に取らず,**取り込み元・受け渡し先を名前で指定する**(SSH 経由のコマンドにデータのパスが現れないようにするため.ENV_02「研究室 PC の操作方針」) +- テストは一時ディレクトリに設定ファイル・目録・ストアを作り,`TIASDATA_CONFIG` で指す + +## 使う側からの利用 (Consumer Usage) + +| 場所 | 使い方 | +| --- | --- | +| 計算サーバ(Python) | `tiasdata.load("tongue_color", version=1)` がバンドルを開く.バンドルの置き場所は環境変数 `TIASDATA_BUNDLES`(既定 `~/data/tiasdata`)の下の `<名前>/v<版>/` | +| Python 以外(DeepTIAS 等) | バンドルの `manifest.csv` と `files/` を読む | +| 研究室 PC(Python) | `tiasdata.Catalog.open()` で目録を直接開ける(通常は CLI を使う) | + +- 使う側の実験記録には,バンドルの**指紋**(データセット名・版・`manifest.csv` の SHA-256)を残す(`Bundle.fingerprint`).これにより「どの実験がどの時点のデータ集合を使ったか」を辿れる +- 版を省略した `load("tongue_color")` は許さない(暗黙に最新を使うと再現性が崩れるため) diff --git "a/docs/04_SPEC/SPEC_02_\343\203\207\343\203\274\343\202\277\343\203\242\343\203\207\343\203\253.md" "b/docs/04_SPEC/SPEC_02_\343\203\207\343\203\274\343\202\277\343\203\242\343\203\207\343\203\253.md" new file mode 100644 index 0000000..a96996c --- /dev/null +++ "b/docs/04_SPEC/SPEC_02_\343\203\207\343\203\274\343\202\277\343\203\242\343\203\207\343\203\253.md" @@ -0,0 +1,176 @@ +# データモデル (Data Model) + +目録(`catalog.sqlite`)の表の定義.スキーマは `migrations/` の連番 SQL で管理し,適用済みの版を `PRAGMA user_version` に記録する([ENV_01](../02_ENV/ENV_01_技術スタック.md)).外部キー制約を有効にする(`PRAGMA foreign_keys = ON`). + +## 全体像 (Overview) + +```text +subjects(症例) +└── captures(撮影) + ├── assets(ファイル.舌画像・カラーチャート画像・派生データ) + │ ▲ derivation_outputs ── derivations(由来: プログラム・版)── derivation_inputs ▶ assets + └── annotations(所見.舌色等.撮影またはファイルに付く) + +datasets ── dataset_versions ── dataset_members(ファイル)── dataset_member_annotations(所見) + └── deliveries(受け渡しの記録) + +kinds(ファイルの種類・所見の種類の定義) +``` + +## ID の書式 (Identifiers) + +| 対象 | 書式 | 例 | +| --- | --- | --- | +| 症例(研究用 ID) | `S` + 6 桁の連番 | `S000123` | +| 撮影 | `C` + 6 桁の連番 | `C000456` | +| ファイル | 中身の SHA-256(16 進 64 文字.小文字) | `3f9a…` | +| データセット | 英小文字・数字・`_`(英小文字で始まる) | `tongue_color` | +| データセットの版 | 1 から始まる整数 | `1` | + +- 研究用 ID は基盤が振る.実際の患者 ID・カルテ番号は目録に入れない(対応表 `identity.sqlite` にだけ置く) +- 現在の舌色データは実際の患者 ID が無く,1 患者 1 枚であるため,**画像 1 枚ごとに症例と撮影を 1 つずつ**作る(対応表は使わない) + +## 表の定義 (Tables) + +### kinds(種類の定義) + +ファイルの種類と所見の種類を 1 つの表で管理する.種類の追加はこの表への行の追加と,`src/tiasdata/kinds/` への検証用の型の追加で行う. + +| 列 | 型 | 内容 | +| --- | --- | --- | +| `kind` | TEXT PK | 種類名(例: `tongue_crop_masked`・`tongue_color`) | +| `category` | TEXT | `asset`(ファイル)または `annotation`(所見) | +| `description` | TEXT | 説明 | +| `value_schema_version` | INTEGER | 所見の値の型の版(`annotation` のみ.値の形を変えたら上げる) | + +最初に用意する種類: + +| `kind` | `category` | 内容 | +| --- | --- | --- | +| `tongue_raw` | `asset` | 撮影した舌画像(補正前) | +| `color_chart` | `asset` | カラーチャート画像 | +| `tongue_crop_masked` | `asset` | 色補正後の画像を舌の枠で切り出し,マスク外を黒 (0,0,0) にしたもの(DeepTIAS Image&Calib の `2-cropped` × `3-resized`.現在の舌色データ) | +| `tongue_mask` | `asset` | 舌のマスク画像 | +| `tongue_color` | `annotation` | 舌色 5 分類+程度.値は `{"label": , "severity": "strong" \| "mild" \| "normal"}`.slug は `pale`・`light_red`・`red`・`dark_red`・`purple`(TIASJudgeModel の SPEC_01「クラス定義」と同じ).`light_red` のときだけ `normal` | + +### subjects(症例) + +| 列 | 型 | 内容 | +| --- | --- | --- | +| `subject_id` | TEXT PK | 研究用 ID | +| `created_at` | TEXT | 登録日時(ISO 8601) | +| `note` | TEXT | 備考 | + +### captures(撮影) + +| 列 | 型 | 内容 | +| --- | --- | --- | +| `capture_id` | TEXT PK | 撮影 ID | +| `subject_id` | TEXT FK | 症例 | +| `captured_on` | TEXT NULL | 撮影日(日付まで.不明なら NULL.現在のデータは不明) | +| `device` | TEXT NULL | 撮影装置(例: `TIAS`) | +| `note` | TEXT | 備考 | + +### assets(ファイル) + +| 列 | 型 | 内容 | +| --- | --- | --- | +| `sha256` | TEXT PK | 中身の SHA-256 | +| `kind` | TEXT FK | ファイルの種類(`kinds.category = 'asset'`) | +| `capture_id` | TEXT FK NULL | 撮影(撮影にひも付かないファイルは NULL) | +| `ext` | TEXT | 拡張子(`png` 等.小文字) | +| `size_bytes` | INTEGER | サイズ | +| `source` | TEXT | 取り込み元の名前(設定ファイルの `sources.<名前>`) | +| `source_name` | TEXT | 取り込み元での相対パス(現在のデータのファイル名は個人をたどれる情報を含まないことを 2026-10-02 にユーザーが確認) | +| `registered_at` | TEXT | 登録日時 | + +- ストア上のパスは `store//.` で決まるため,目録には保存しない +- 同じ中身のファイルを 2 度取り込むと,同じ行を指す(重複登録しない).ただし別の撮影・別の種類として登録しようとした場合は停止する + +### derivations・derivation_inputs・derivation_outputs(由来) + +派生データを「どのファイルから,どのプログラムのどの版で」作ったかを記録する. + +| 表 | 列 | 内容 | +| --- | --- | --- | +| `derivations` | `derivation_id` INTEGER PK | — | +| | `tool` | プログラム名(例: `DeepTIAS`) | +| | `tool_version` | プログラムの版(例: `2021-12-exe`・`2.7.1`・git のコミット).不明なら `unknown` | +| | `mode` | 実行モード・設定(例: `Image&Calib`.JSON で詳細を持ってよい) | +| | `performed_at` | 実行日時(不明なら NULL) | +| | `note` | 備考 | +| `derivation_inputs` | `derivation_id`・`sha256`・`role` | 入力ファイルと役割(例: `tongue`・`chart`) | +| `derivation_outputs` | `derivation_id`・`sha256`・`role` | 出力ファイルと役割(例: `cropped`・`mask`・`analysis`) | + +- 同じ入力から版の違うプログラムで作った出力は,別の `derivations` 行になる(DeepTIAS の旧 exe と現行ソースの結果を区別できる) +- 現在の舌色データ(`tongue_crop_masked`)は元の撮影画像が基盤に無いため,入力なし・`tool = 'DeepTIAS'`・`tool_version = 'unknown'`・`mode = 'Image&Calib'` の由来を付けて登録する(TIASJudgeModel の SPEC_04 の確認結果を `note` に書く) + +### annotations(所見) + +| 列 | 型 | 内容 | +| --- | --- | --- | +| `annotation_id` | INTEGER PK | — | +| `kind` | TEXT FK | 所見の種類(`kinds.category = 'annotation'`) | +| `capture_id` | TEXT FK NULL | 撮影に付く所見の場合 | +| `sha256` | TEXT FK NULL | ファイルに付く所見の場合(`capture_id` とどちらか一方だけ) | +| `value` | TEXT | 値(JSON.`kinds/` の型で検証してから保存する) | +| `annotator` | TEXT NULL | 評価者(研究用の記号.個人名は入れない) | +| `source` | TEXT | 由来(例: `tiasjudgemodel_folder`=フォルダ名から読み取った) | +| `created_at` | TEXT | 登録日時 | +| `superseded_by` | INTEGER NULL | 訂正したときの新しい行 | + +- **所見の行は書き換えない**.訂正は新しい行を追加し,古い行の `superseded_by` を埋める(過去の版が古い行を参照し続けられるようにするため) +- 現在の舌色ラベルは撮影に付ける(医師が撮影を見て付けたラベルのため) + +### datasets・dataset_versions・dataset_members・dataset_member_annotations(データセット) + +| 表 | 列 | 内容 | +| --- | --- | --- | +| `datasets` | `name` TEXT PK・`description`・`created_at` | — | +| `dataset_versions` | `name`・`version`(PK は 2 列) | — | +| | `definition` | 版を作ったときのデータセットの定義(TOML の内容をそのまま保存) | +| | `hub_commit` | 版を作ったときの本リポジトリのコミット | +| | `manifest_sha256` | この版の `manifest.csv` の SHA-256(指紋) | +| | `created_at`・`note` | — | +| `dataset_members` | `name`・`version`・`sha256` | 版に含まれるファイル | +| `dataset_member_annotations` | `name`・`version`・`sha256`・`annotation_id` | 版を作った時点で採用した所見 | + +- **版の行と,その版の構成の行は書き換え・削除しない**(凍結) +- 版の作り方・manifest の形は [SPEC_03](SPEC_03_データセットと受け渡し.md) + +### deliveries(受け渡しの記録) + +計算サーバにコピーした作業用データと,データセットの版との対応を記録する. + +| 列 | 内容 | +| --- | --- | +| `delivery_id` INTEGER PK | — | +| `name`・`version` | 渡したデータセットの版 | +| `server` | 受け渡し先(設定ファイルの `servers.<名前>`) | +| `path` | 受け渡し先のパス(例: `~/data/tiasdata/tongue_color/v1`) | +| `delivered_at` | 日時 | +| `verified` | 受け渡し先でハッシュを照合できたか | + +## 対応表 (Identity Map) + +`identity.sqlite`(目録とは別のファイル・別のフォルダ).実際の患者 ID が分かるデータを取り込むときだけ使う. + +| 列 | 内容 | +| --- | --- | +| `patient_key` TEXT PK | 実際の患者 ID(取り込み元が持つもの) | +| `subject_id` TEXT | 研究用 ID | +| `created_at` | — | + +- 同じ患者の別の撮影を取り込むと,同じ研究用 ID にひも付く(複数枚・経時的な撮影に対応する) +- CLI は対応表の中身を表示しない.対応表を開くのは取り込みのときだけ +- 匿名化の方法・対応表の保管ルールは倫理審査の保管ルールを確認して確定する([PLAN_01「人間が確認すること」](../03_PLAN/PLAN_01_要件定義書.md)) + +## 拡張の例 (Extension Examples) + +| 追加したいもの | やること | +| --- | --- | +| 苔の色・厚さ等の所見 | `kinds` に行を追加し,`kinds/` に値の型を足す.マイグレーションは種類の行の追加だけ | +| 問診・体質 | 撮影または症例に付く所見として同様に追加する(症例に付く所見が必要になったら `annotations` に `subject_id` 列を足す) | +| 経時的な撮影 | 同じ症例に撮影を追加する(対応表で同じ研究用 ID にひも付ける) | +| 別の撮影装置 | `captures.device` に記録する.装置固有の情報が必要なら所見として持つ | +| DeepTIAS の解析結果 | 出力ファイルの種類(`tongue_mask`・`analysis_csv` 等)を足し,`derivations` で入力の撮影画像とひも付ける | diff --git "a/docs/04_SPEC/SPEC_03_\343\203\207\343\203\274\343\202\277\343\202\273\343\203\203\343\203\210\343\201\250\345\217\227\343\201\221\346\270\241\343\201\227.md" "b/docs/04_SPEC/SPEC_03_\343\203\207\343\203\274\343\202\277\343\202\273\343\203\203\343\203\210\343\201\250\345\217\227\343\201\221\346\270\241\343\201\227.md" new file mode 100644 index 0000000..dfa452c --- /dev/null +++ "b/docs/04_SPEC/SPEC_03_\343\203\207\343\203\274\343\202\277\343\202\273\343\203\203\343\203\210\343\201\250\345\217\227\343\201\221\346\270\241\343\201\227.md" @@ -0,0 +1,135 @@ +# データセットと受け渡し (Datasets & Delivery) + +データセットの定義・版の作成・バンドル(manifest +ファイル)の書き出し・計算サーバへの受け渡し・CLI・出力の規則を定める.表の定義は [SPEC_02](SPEC_02_データモデル.md). + +## データセットの定義 (Dataset Definition) + +`datasets/<名前>.toml` に置く(リポジトリで管理し,PR で変更する). + +```toml +name = "tongue_color" +description = "舌色 5 分類+程度(TIASJudgeModel の学習データ)" + +[select] +asset_kind = "tongue_crop_masked" # 集めるファイルの種類 +require = ["tongue_color"] # この所見を持つものだけ(撮影に付いた所見を含む) + +[columns] # manifest に出す所見の列 +tongue_color = ["label", "severity"] +``` + +- 条件は「ファイルの種類」と「必須の所見」から始め,必要になったら所見の値による絞り込み等を足す +- 1 つのファイル・撮影に同じ種類の有効な所見(訂正されていないもの)が 2 つ以上ある場合は,版の作成を止める(どちらを使うかを黙って決めない) + +## 版の作成 (Creating a Version) + +`tiasdata dataset freeze <名前>` で,その時点の目録に定義を当てはめて新しい版を作る. + +1. 定義を検証し,条件に合うファイルと採用する所見を集める +2. 直前の版と構成(ファイルと所見)が同じなら,版を作らずにその旨を表示する +3. 構成・定義の内容・本リポジトリのコミット・`manifest.csv` の SHA-256 を記録する(以後変更しない) + +- 本リポジトリに未コミットの変更がある状態では版を作らない(定義とコードの版を記録できなくなるため) +- 行の順序は `subject_id`・`capture_id`・`sha256` の順に並べる(同じ構成なら同じ `manifest.csv` になるようにする) + +## バンドル (Bundle) + +データセットの 1 つの版を,計算サーバ・Python 以外のツールに渡す形にまとめたもの. + +```text +<名前>/v<版>/ +├── dataset.json # 名前・版・作成日時・hub_commit・件数・manifest.csv の SHA-256 +├── manifest.csv # 1 行 1 ファイル +├── SHA256SUMS # files/ の照合用(sha256sum -c で検証できる形式) +└── files/. # ストアからのコピー +``` + +### manifest.csv の列 + +| 列 | 内容 | +| --- | --- | +| `path` | バンドルのルートからの相対パス(`files/.`.区切りは `/`) | +| `sha256` | ファイルの SHA-256 | +| `kind` | ファイルの種類 | +| `subject_id` | 研究用の症例 ID(分割の単位にする) | +| `capture_id` | 撮影 ID | +| `<所見>.<項目>` | 定義の `[columns]` で指定した所見の値(例: `tongue_color.label`・`tongue_color.severity`) | + +- 文字コードは UTF-8(BOM なし),改行は LF +- 実際の患者 ID・元のファイル名は出さない + +### TIASJudgeModel との互換 (Compatibility) + +TIASJudgeModel の manifest(`image_path`・`patient_id`・`label`・`severity`・`note`)は,次の対応で同じ情報を持つ. + +| TIASJudgeModel | 本基盤 | +| --- | --- | +| `image_path` | `path` | +| `patient_id` | `subject_id` | +| `label` | `tongue_color.label` | +| `severity` | `tongue_color.severity` | + +- TIASJudgeModel 側は `tiasdata.load()` でバンドルを読み,自分の manifest の形に変換して使う(変換は TIASJudgeModel 側の実装).基盤側は使う側ごとの形式を持たない +- 現在のデータは 1 患者 1 枚のため,`subject_id` は TIASJudgeModel の現行の `patient_id`(画像 ID)と同じ分け方になる + +## 計算サーバへの受け渡し (Delivery) + +`tiasdata deliver <名前> --version <版> --to <サーバ名>` を研究室 PC で実行する(手元 PC からは `ssh labpc` 経由.データは手元 PC を通らない). + +1. バンドルを書き出す(`bundles/<名前>/v<版>/`.既にあれば中身を照合して使う) +2. tar にまとめ,設定ファイルの鍵で `scp` する +3. サーバで `/<名前>/v<版>/` に展開し,`chmod -R u+rwX,go-rwx` で自分だけに絞る(Windows で作った tar は `tar --delay-directory-restore -xf` で展開する.TIASJudgeModel の ENV_03 の実測) +4. サーバで `sha256sum -c SHA256SUMS` を実行して照合する(サーバに `tiasdata` が無くても検証できる) +5. tar ファイルを両側で削除し,`deliveries` に記録する + +- 受け渡し先に同じ版が既にあれば,照合だけ行って記録する(上書きしない) +- 版は凍結されているため,一度渡した版の中身が変わることはない.新しいデータは新しい版として渡す + +## ライブラリの API (Library API) + +```python +import tiasdata + +bundle = tiasdata.load("tongue_color", version=1) # TIASDATA_BUNDLES/<名前>/v<版>/ を開く +bundle.verify() # ハッシュを照合する(任意.時間がかかる) +for row in bundle.rows: # manifest の行(pydantic の型) + row.path, row.subject_id, row.annotations["tongue_color"]["label"] +bundle.fingerprint # {"name", "version", "manifest_sha256"}.実験記録に残す +``` + +- `load()` は版の指定を必須にする +- `tiasdata.open_bundle(path)` でパスを直接指定して開くこともできる(テスト・Python 以外との突き合わせ用) +- 読み込み時に `manifest.csv` の SHA-256 が `dataset.json` と一致しなければ停止する + +## CLI (Commands) + +研究室 PC で使う.パスは引数に取らず,名前で指定する([SPEC_01「設定ファイル」](SPEC_01_アーキテクチャ設計.md)). + +| コマンド | 役割 | +| --- | --- | +| `tiasdata init` | 設定ファイルに従って目録・ストアを作り,マイグレーションを適用する | +| `tiasdata import <取り込み元>` | 取り込み元を読み,ファイルをストアにコピーして症例・撮影・ファイル・所見・由来を登録する(取り込み元の形ごとに importer を用意する.最初は舌色の 9 フォルダ `tongue_color_folders`) | +| `tiasdata dataset list` / `show <名前>` | データセットと版の一覧・件数 | +| `tiasdata dataset freeze <名前>` | 新しい版を作る | +| `tiasdata export <名前> --version <版>` | バンドルを `bundles/` に書き出す | +| `tiasdata deliver <名前> --version <版> --to <サーバ名>` | 計算サーバへ渡す | +| `tiasdata check` | 点検: ストアのファイルのハッシュの照合,目録にあってストアに無いファイル・その逆,所見の参照切れ,凍結した版の構成の欠け.件数だけを表示する | +| `tiasdata stats` | 種類・所見の値ごとの件数 | +| `tiasdata backup` | 目録を `backups/catalog-<日時>.sqlite` に複製する(SQLite のバックアップ API を使う) | + +- 取り込みは何度実行しても同じ結果になるようにする(既に登録済みのファイルは飛ばす) +- 登録・版の作成は 1 つのトランザクションで行い,途中で失敗したら何も残さない +- 問題を見つけたら黙って飛ばさず,件数と理由を表示して停止する(`check`・`stats` は止めずに件数を報告する) + +## 出力の規則 (Output Rules) + +CLI の出力・ログ・エラーメッセージに出してよいもの・出さないもの.Claude が SSH 経由で CLI を実行し,その出力を読むことを前提にする. + +| 出してよい | 出さない | +| --- | --- | +| 件数・割合・サイズの合計 | 実際の患者 ID・対応表の中身 | +| 研究用 ID(`S…`・`C…`)・SHA-256 | 画像の画素値・画像そのもの | +| 種類名・所見の値(舌色の slug 等)・版 | 取り込み元のファイルの中身 | +| 設定ファイルで付けた名前(取り込み元・サーバ) | — | + +- 元のファイル名(`source_name`)は個人をたどれる情報を含まないが,既定では表示しない(エラーの特定に必要な場合だけ表示する) diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 68e9e62..cd6fb7a 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -9,3 +9,9 @@ - 書式・運用ルールは .claude/rules/progress-log.md を参照 (本ファイルの編集時に自動ロードされる) --> + +- [x] **プロジェクト立ち上げ(GUIDE_01)**: 要件定義・技術選定・環境構築・仕様設計・規約整備・開発計画を `chore/project-setup` で完了(2026-10-02).solo モード + - 動機: 舌診研究の各リポジトリが研究室 PC のフォルダを個別に読む形をやめ,データセット名と版で取り出せる共通の入口を作る + - 設計判断: 目録は SQLite + SHA-256 名のストア(取り込み時にコピー.原本参照より版の再現が確実).計算サーバへは版ごとのバンドル(`SHA256SUMS` 付き)を直送.所見は種類の定義+ JSON 値で持ち,種類の追加で表を作り直さない + - 運用: 研究室 PC は手元 PC から `ssh labpc` で操作し,研究室 PC では編集しない.研究室 PC に uv 0.12.22・Python 3.12.15 を導入済み.検証プロファイルは作らない(CLI はテストと `tiasdata check` で確認する) + - 未実装・後段: `main` がまだ存在しない(GitHub・GitBucket とも空).取り込み後に研究室 PC へ clone する(ENV_02).次は PLAN_02 Step 1 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..dce91df --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,40 @@ +[project] +name = "tiasdata" +version = "0.1.0" +description = "舌診研究のデータを研究室 PC で一元管理する共有データ基盤(目録・アクセス用ライブラリ・CLI)" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "pydantic>=2.9", +] + +[project.scripts] +tiasdata = "tiasdata.cli:main" + +[build-system] +requires = ["uv_build>=0.12.5,<0.13"] +build-backend = "uv_build" + +[tool.ruff] +line-length = 100 +target-version = "py312" +# .claude/ はテンプレート所有のファイル(フック・テンプレートのテスト)であり, +# このプロジェクトの規約で整形しないため lint 対象から外す +extend-exclude = [".claude"] + +[tool.ruff.lint] +select = ["E", "F", "W", "I", "B", "UP", "N"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" + +[tool.pytest.ini_options] +testpaths = ["tests"] + +[dependency-groups] +dev = [ + "pillow>=11.0", + "pytest>=8.0", + "ruff>=0.8", +] diff --git a/src/tiasdata/__init__.py b/src/tiasdata/__init__.py new file mode 100644 index 0000000..3ad8978 --- /dev/null +++ b/src/tiasdata/__init__.py @@ -0,0 +1,3 @@ +"""舌診研究の共有データ基盤 TIASDataHub のアクセス用ライブラリ.""" + +__version__ = "0.1.0" diff --git a/src/tiasdata/cli.py b/src/tiasdata/cli.py new file mode 100644 index 0000000..c858750 --- /dev/null +++ b/src/tiasdata/cli.py @@ -0,0 +1,17 @@ +"""tiasdata コマンドのエントリポイント(サブコマンドは実装フェーズで追加する).""" + +import argparse + +from tiasdata import __version__ + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(prog="tiasdata", description="舌診研究の共有データ基盤") + parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}") + parser.parse_args(argv) + parser.print_help() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_smoke.py b/tests/test_smoke.py new file mode 100644 index 0000000..636c4f9 --- /dev/null +++ b/tests/test_smoke.py @@ -0,0 +1,15 @@ +import pytest + +import tiasdata +from tiasdata.cli import main + + +def test_package_imports() -> None: + assert tiasdata.__version__ + + +def test_cli_version(capsys: pytest.CaptureFixture[str]) -> None: + with pytest.raises(SystemExit) as exc: + main(["--version"]) + assert exc.value.code == 0 + assert tiasdata.__version__ in capsys.readouterr().out diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..bed80dc --- /dev/null +++ b/uv.lock @@ -0,0 +1,305 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pillow" +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, + { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, + { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, + { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, + { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, + { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, + { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, + { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, + { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, + { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, + { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" }, + { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" }, + { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" }, + { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" }, + { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" }, + { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" }, + { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" }, + { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" }, + { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" }, + { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3f/76358795aa7a8c6d4f36e2cb828ad1c90ee118e1393a9281664f5aade9d4/pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d", size = 2076516, upload-time = "2026-08-28T09:58:21.576Z" }, + { url = "https://files.pythonhosted.org/packages/db/50/26b091836076ce4cb2fac264186936acc069e0595772cfd02a563bc4761a/pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e", size = 1922874, upload-time = "2026-08-28T09:58:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/2a8ce3849e299d44e2d2c196b6082643a3235565a735cb51db7a6261f614/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29", size = 1951772, upload-time = "2026-08-28T09:58:25.435Z" }, + { url = "https://files.pythonhosted.org/packages/87/46/ac0dc8bdd9e6048183a14eb127764e7ad9240021c17513074a4711b0e31e/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4", size = 2031832, upload-time = "2026-08-28T09:58:27.102Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/339de5bef7be36301a2231eaa52e62163742c2281f11b5f4892bc79785cd/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a", size = 2208645, upload-time = "2026-08-28T09:58:28.948Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a0/9ff22b797724262da14427abaed4dd1d864a139693fc5e7809114376a716/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62", size = 2265935, upload-time = "2026-08-28T09:58:30.625Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a4/eb9409ec0736e50aa70a412f16c204ed149516846912f7e6724d4c73ee53/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2", size = 2066284, upload-time = "2026-08-28T09:58:32.289Z" }, + { url = "https://files.pythonhosted.org/packages/c0/02/7f6156ffc926857f1c37c07d9a388682865a81830ab6a1b637082c25e399/pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869", size = 2105889, upload-time = "2026-08-28T09:58:33.986Z" }, + { url = "https://files.pythonhosted.org/packages/92/b1/e781d357ebe09fc929f995700f1b3503e8897f1cece183ecb1300d4d67e9/pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5", size = 2158006, upload-time = "2026-08-28T09:58:35.647Z" }, + { url = "https://files.pythonhosted.org/packages/70/0a/644597d84ab400e50609c192120b85c9681c22d3a20461b9060a79be0a7a/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3", size = 2158408, upload-time = "2026-08-28T09:58:37.38Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ee/ca3b7b3a4b3769ffe9ce9432a7c9be755de9593a46d3b0d54d0409323e44/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b", size = 2309609, upload-time = "2026-08-28T09:58:39.22Z" }, + { url = "https://files.pythonhosted.org/packages/ce/52/39fa1f451486019524ca685020390e7ca351832fd874530ba30c8628e6dc/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0", size = 2342618, upload-time = "2026-08-28T09:58:40.89Z" }, + { url = "https://files.pythonhosted.org/packages/81/5e/468fc630568c61dcef3cd47ad32ffbeed9af643f49208d1ea86ab4f890c4/pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b", size = 1939475, upload-time = "2026-08-28T09:58:42.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c9/4c19f41b84cf6b622a72fbeed7665b25d47a187d68d47d0d430c07f23268/pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8", size = 2043140, upload-time = "2026-08-28T09:58:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/af/dd/0c1a050299147c746e5256db16d645ab5efd4f78c59937d581a0524e74a2/pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084", size = 1997729, upload-time = "2026-08-28T09:58:46.13Z" }, + { url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" }, + { url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" }, + { url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" }, + { url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" }, + { url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" }, + { url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" }, + { url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" }, + { url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" }, + { url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" }, + { url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" }, + { url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" }, + { url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" }, + { url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" }, + { url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" }, + { url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" }, + { url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" }, + { url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" }, + { url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" }, + { url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" }, + { url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" }, + { url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" }, + { url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" }, + { url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" }, + { url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" }, + { url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" }, + { url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" }, + { url = "https://files.pythonhosted.org/packages/df/dd/053c2e4303f791f3b8f8a14ab0b22008e8eb21d868c0c90b4f9be705b76a/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942", size = 2062540, upload-time = "2026-08-28T10:01:00.318Z" }, + { url = "https://files.pythonhosted.org/packages/d7/dd/a18df751a5e37dd51bfad7f68e766999125bebe68c9e1d10a493ad01bd63/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f", size = 1902040, upload-time = "2026-08-28T10:01:02.529Z" }, + { url = "https://files.pythonhosted.org/packages/b7/13/01d40f9d07ce8a779fd6e0bd8ad4fba91309500dd67b869e2e219d261a6d/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433", size = 1967479, upload-time = "2026-08-28T10:01:05.004Z" }, + { url = "https://files.pythonhosted.org/packages/fa/04/c81d4841331c2178b6fb09ae225425e110ed72d990c9fe556c4ec03d1013/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c", size = 2111034, upload-time = "2026-08-28T10:01:07.345Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "ruff" +version = "0.16.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/49/23802c45f093eb14bde54b141d2b2f058edfa63a06db7beed047308cc08f/ruff-0.16.10.tar.gz", hash = "sha256:eff4728c4eaae93f0955cd264d24b2ab348e74bf59986ccf282ba6dc16b3b017", size = 4958724, upload-time = "2026-10-01T18:03:21.697Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/21/ebce22e1d90cdb2cd691026b9c6e9bec6499f0b396089481755b5d49efff/ruff-0.16.10-py3-none-linux_armv6l.whl", hash = "sha256:488b0fe3f3574210e5cf80d9f59b9e3ab17a127a8155de3f307b392589cfb511", size = 10095557, upload-time = "2026-10-01T18:02:36.072Z" }, + { url = "https://files.pythonhosted.org/packages/cb/98/a54de85876a8b2612bfa0d84c7b9abfb39c6a3354aee7800b09c1649b9e3/ruff-0.16.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e748ff95c934c4e978783b8e687bc174e7bd84e8ad24e3243e1ecfcda5e0282d", size = 10340577, upload-time = "2026-10-01T18:02:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/9f/16/1a5a4a2657effe4806110f2b907313802f1367fcdbb29e8122766e407fab/ruff-0.16.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3031a4a2e8e7b8a46f70be45f198c35a11ece509a94b80334d8d397a33c67550", size = 9774282, upload-time = "2026-10-01T18:02:41.79Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/470085af734da396e68cd80fb0a3e7c109a459716ae59588c0f6fab8a17d/ruff-0.16.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:494401c86df4c4c25f69b9419605d944467ee98c42fb6ad405ef4fa40b8fb67d", size = 9920895, upload-time = "2026-10-01T18:02:44.458Z" }, + { url = "https://files.pythonhosted.org/packages/57/de/f10cffe4f88a37ea6615ec460f01bf76f0bc1477737a35d9ee61a630a78b/ruff-0.16.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d203abc0ff2b773ee33d00ab8df0bb67046fbc7c07b119332f08b9b345cf8221", size = 9902707, upload-time = "2026-10-01T18:02:47.041Z" }, + { url = "https://files.pythonhosted.org/packages/ff/44/3fdcedf83ae60ef837dd239170e480dce606a9142cfa2db911afdf855fd9/ruff-0.16.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd83d1235a5258d318477bc5b576303974cbdef5df0c01a1bff14efcc23bd12a", size = 10619287, upload-time = "2026-10-01T18:02:49.485Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/02e76a5574002153618eafbb70e159468addc72a5d2c488e65cbb4639d2d/ruff-0.16.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc2610fb269fa56dd8a68669ae470fa6272902668c0fc2ebc3aa112b2633d5b8", size = 11339942, upload-time = "2026-10-01T18:02:52.008Z" }, + { url = "https://files.pythonhosted.org/packages/6b/c4/cde27d47ad8d4126c587e608c67c46e7feac11763f606d261a1bc8a489e6/ruff-0.16.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e70175e29cc94c26ea296c80e470180b744b7419026898e58f520c6ab32578e", size = 10934316, upload-time = "2026-10-01T18:02:54.811Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/17234145f302a645a123e8c3bb2411ecf4669fbc350de3b0d803230a1729/ruff-0.16.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f33f43a864a8483eebd160e713336c8bab02c934feaff0a33cf5ccb41546d09a", size = 10387968, upload-time = "2026-10-01T18:02:57.497Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/2493af60240ee7644b38a4b821f5f9c3b5a4fa3178770fe1d0c685217395/ruff-0.16.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1dfc6f0088149fb6a362c1c446bcbb3fd2157b3852fe2fa68409276eab9ad9b3", size = 10537906, upload-time = "2026-10-01T18:03:00.006Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9a/f56b28f3b143bb9e518c34fb89ab191b626bca8b70dbf8af0d2e2d473572/ruff-0.16.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6553498afc35f580f036030795810b9e6bcea31604b0fd9e8d352795473042e3", size = 10012938, upload-time = "2026-10-01T18:03:03.006Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/fca37362848ea4d4d80712df13632e645e7c7cfb1bdedf140699ac7a090b/ruff-0.16.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a3b8471dea115d37f123882be852bed13403746d3a76c11de4a19ec5f9ff5a03", size = 9897945, upload-time = "2026-10-01T18:03:05.818Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c7/e0bc57664d6e0c61fd22f620af260fe9662d7e1ddb320ea7f160e76ef165/ruff-0.16.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:92e59a70bcbd9d3a5483656da906ec28edfdacfce00afd99edb8b4e9d15644be", size = 10333134, upload-time = "2026-10-01T18:03:08.25Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/5c9f3b68737c0e4a33a1db5dfab44f7b786d96ca91a7233112ddab1e6dc2/ruff-0.16.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7ae7375f803b5520dc9f546bed7e3a0acb70b91812e9bb4b19927de22f25b77d", size = 10741702, upload-time = "2026-10-01T18:03:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/78/fa/0f9c2020dc316c53d983be011720b8157cc052b97e3f4dfa7db6d0f880a6/ruff-0.16.10-py3-none-win32.whl", hash = "sha256:2a12e01cb9156c10c466f63b46eaae5ecea28dfbd21b5836353ae498e7d1349a", size = 10139176, upload-time = "2026-10-01T18:03:13.267Z" }, + { url = "https://files.pythonhosted.org/packages/99/29/cfb0df9448d4d4ad48c2de029ada9ebd71baa6da983a6c77ee6c6cd0fe82/ruff-0.16.10-py3-none-win_amd64.whl", hash = "sha256:97f2015c92aa97105b0eab19eb5d224884399281cfc5da86a92db4ab5e7fb2ca", size = 10584734, upload-time = "2026-10-01T18:03:16.006Z" }, + { url = "https://files.pythonhosted.org/packages/fc/05/c16957eb287c3fc062e032619a25868d93d408a844b725bcf514f7a378ff/ruff-0.16.10-py3-none-win_arm64.whl", hash = "sha256:25a65fe998c4e6861ec079ada5826a2fc605e6cbccbe9dcd7fac1f54e791621b", size = 10366440, upload-time = "2026-10-01T18:03:19.04Z" }, +] + +[[package]] +name = "tiasdata" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "pydantic" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pillow" }, + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [{ name = "pydantic", specifier = ">=2.9" }] + +[package.metadata.requires-dev] +dev = [ + { name = "pillow", specifier = ">=11.0" }, + { name = "pytest", specifier = ">=8.0" }, + { name = "ruff", specifier = ">=0.8" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, +]