diff --git a/CLAUDE.md b/CLAUDE.md index fbbfd38..b0dac32 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,13 +5,14 @@ ## 開発進捗 -現在の進捗: フェーズ 7(実装)— Step 1 完了. -次のアクション: `/implement Step 2: YAML スキーマ定義`([PLAN_02 参照](docs/03_PLAN/PLAN_02_開発ステップ.md)) +現在の進捗: フェーズ 7(実装)— Step 1〜2 完了. +次のアクション: `/implement Step 3: ジオメトリ層`([PLAN_02 参照](docs/03_PLAN/PLAN_02_開発ステップ.md)) 実装済みモジュール: - `src/sm_tias_lightsim/uniformity.py` — Rec.709 輝度変換 + CoV/std/max-min(MiniTias-Evaluation コピー) - `src/sm_tias_lightsim/evaluation/zones.py` — 正規化楕円距離マップ + 3 ゾーン分類 + ゾーン別統計 - `src/sm_tias_lightsim/evaluation/radial.py` — 20 ビン放射状輝度プロファイル +- `src/sm_tias_lightsim/scene/schema.py` — Pydantic ベース YAML スキーマ + ローダ ※ ステップ完了時にここを更新すること. ## 必須ルール(コード実装時) 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" index 8938b3c..aa44a9d 100644 --- "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" @@ -56,6 +56,8 @@ | シミュレータ | `mitsuba` | 光線追跡コア | | 数値計算 | `numpy` | 配列計算(評価指標と共用) | | ジオメトリ | `trimesh` | STL の読み込み・三角形メッシュ操作 | +| シーン定義 | `pydantic` | YAML スキーマのバリデーション | +| 設定ファイル | `pyyaml` | YAML パース | | 画像 I/O | `pillow` | PNG 出力(実機画像と同形式に揃える) | | 可視化 | `matplotlib` | 輝度マップ・ヒストグラム・放射状プロファイル等 | | 表データ | `pandas` | 均一性指標 CSV の生成・統計集計 | diff --git a/pyproject.toml b/pyproject.toml index 7e0940c..8dd8137 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,8 @@ "numpy>=2.4.4", "pandas>=3.0.2", "pillow>=12.2.0", + "pydantic>=2.13.3", + "pyyaml>=6.0.3", "trimesh>=4.12.1", ] diff --git a/src/sm_tias_lightsim/scene/__init__.py b/src/sm_tias_lightsim/scene/__init__.py new file mode 100644 index 0000000..06e1e16 --- /dev/null +++ b/src/sm_tias_lightsim/scene/__init__.py @@ -0,0 +1,8 @@ +"""scene パッケージ — YAML スキーマ定義・シーン構築モジュール群.""" + +from sm_tias_lightsim.scene.schema import ( + SceneConfig, + load_scene_yaml, +) + +__all__ = ["SceneConfig", "load_scene_yaml"] diff --git a/src/sm_tias_lightsim/scene/schema.py b/src/sm_tias_lightsim/scene/schema.py new file mode 100644 index 0000000..47bf488 --- /dev/null +++ b/src/sm_tias_lightsim/scene/schema.py @@ -0,0 +1,325 @@ +"""YAML シーン定義スキーマ(Pydantic モデル). + +SPEC_02_シーン定義仕様 に準拠したモデル群と,YAML ファイルローダを提供する. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Annotated, Literal + +import yaml +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator + +# --------------------------------------------------------------------------- +# 共通基底・型エイリアス +# --------------------------------------------------------------------------- + + +# 全モデルで frozen=True を共有するため基底クラスに集約する +class _FrozenBase(BaseModel): + model_config = ConfigDict(frozen=True) + + +# 3D ベクトル(mm 座標・方向ベクトル・RGB 輝度)の共通型 +Vec3 = tuple[float, float, float] + + +# --------------------------------------------------------------------------- +# Film / Sensor +# --------------------------------------------------------------------------- + + +class FilmConfig(_FrozenBase): + """レンダリングフィルム(出力解像度・ピクセルフィルタ)設定.""" + + width: int = Field(..., ge=1, description="フィルム幅 [px]") + height: int = Field(..., ge=1, description="フィルム高さ [px]") + rfilter: Literal["gaussian", "box", "tent"] = Field( + default="gaussian", description="ピクセル再構成フィルタ" + ) + + +class SensorConfig(_FrozenBase): + """カメラセンサ設定.現在は perspective のみ対応.""" + + type: Literal["perspective"] + position: Vec3 = Field(..., description="カメラ位置 [mm]") + target: Vec3 = Field(..., description="注視点 [mm]") + up: Vec3 = Field(..., description="上方向ベクトル") + fov_deg: float = Field(..., description="水平方向 FOV [deg]") + film: FilmConfig + + @field_validator("fov_deg") + @classmethod + def _validate_fov(cls, v: float) -> float: + """FOV が 0 < fov_deg < 180 の範囲内であることを検証する.""" + if not (0.0 < v < 180.0): + msg = f"fov_deg must be in range (0, 180), got {v}" + raise ValueError(msg) + return v + + +# --------------------------------------------------------------------------- +# Render +# --------------------------------------------------------------------------- + + +class RenderConfig(_FrozenBase): + """レンダリングパラメータ設定.""" + + variant: str = Field(default="llvm_ad_rgb", description="Mitsuba variant 文字列") + integrator: Literal["path", "direct", "volpath"] = Field( + default="path", description="積分器の種類" + ) + max_depth: int = Field(default=8, ge=1, description="最大光線追跡深度") + spp: int = Field(default=256, ge=1, description="ピクセルあたりのサンプル数") + + +# --------------------------------------------------------------------------- +# Materials(discriminated union) +# --------------------------------------------------------------------------- + + +class DiffuseMaterial(_FrozenBase): + """Lambertian 拡散反射材料.""" + + type: Literal["diffuse"] + reflectance: float | Vec3 = Field(..., description="反射率(スカラまたは RGB タプル)") + + +class _IorBase(_FrozenBase): + """屈折率フィールドを持つ誘電体材料の共通基底.""" + + int_ior: float = Field(..., description="内側の屈折率") + ext_ior: float = Field(..., description="外側の屈折率") + + +class DielectricMaterial(_IorBase): + """誘電体材料(厚みあり).""" + + type: Literal["dielectric"] + + +class ThinDielectricMaterial(_IorBase): + """薄膜誘電体材料(ディフューザー薄板向け).""" + + type: Literal["thindielectric"] + + +# type フィールドで識別される discriminated union +MaterialConfig = Annotated[ + DiffuseMaterial | DielectricMaterial | ThinDielectricMaterial, + Field(discriminator="type"), +] + + +# --------------------------------------------------------------------------- +# Lights(discriminated union) +# --------------------------------------------------------------------------- + + +class PointLight(_FrozenBase): + """点光源(LED 単体モデル等).""" + + id: str = Field(..., description="シーン内ユニーク ID") + type: Literal["point"] + position: Vec3 = Field(..., description="位置 [mm]") + intensity: float | Vec3 = Field(..., description="強度(スカラまたは RGB タプル)") + profile: Literal["lambertian", "isotropic"] = Field(..., description="配光プロファイル") + + +class AreaLight(_FrozenBase): + """面光源(矩形).""" + + id: str = Field(..., description="シーン内ユニーク ID") + type: Literal["area"] + shape: Literal["rectangle"] = Field(..., description="面形状") + center: Vec3 = Field(..., description="中心位置 [mm]") + size: tuple[float, float] = Field(..., description="サイズ [mm, mm]") + direction: Vec3 = Field(..., description="放射方向ベクトル") + radiance: float | Vec3 = Field(..., description="放射輝度(スカラまたは RGB タプル)") + + +class DirectionalLight(_FrozenBase): + """平行光源(無限遠光源).""" + + id: str = Field(..., description="シーン内ユニーク ID") + type: Literal["directional"] + direction: Vec3 = Field(..., description="光の進行方向ベクトル") + irradiance: float | Vec3 = Field(..., description="照度(スカラまたは RGB タプル)") + + +# type フィールドで識別される discriminated union +LightConfig = Annotated[ + PointLight | AreaLight | DirectionalLight, + Field(discriminator="type"), +] + + +# --------------------------------------------------------------------------- +# Geometry +# --------------------------------------------------------------------------- + + +class MeshConfig(_FrozenBase): + """メッシュ(STL)定義.""" + + id: str = Field(..., description="シーン内ユニーク ID") + path: str = Field(..., description="プロジェクトルートからの相対パス(存在チェックは行わない)") + unit: Literal["mm", "m"] = Field(..., description="モデルの単位系") + material: str = Field(..., description="materials の dict キー参照") + + +class GeometryConfig(_FrozenBase): + """ジオメトリ設定(メッシュ一覧).""" + + meshes: list[MeshConfig] = Field(..., min_length=1, description="メッシュ定義リスト") + + +# --------------------------------------------------------------------------- +# Evaluation +# --------------------------------------------------------------------------- + + +class RoiConfig(_FrozenBase): + """評価 ROI 設定(image_pixel 型のみ対応). + + scene_face 型は将来実装予定. + """ + + type: Literal["image_pixel"] + x: int = Field(..., ge=0, description="ROI 左上 X 座標 [px]") + y: int = Field(..., ge=0, description="ROI 左上 Y 座標 [px]") + width: int = Field(..., ge=1, description="ROI 幅 [px]") + height: int = Field(..., ge=1, description="ROI 高さ [px]") + + +class ZonesConfig(_FrozenBase): + """3 ゾーン分析設定(中心 / 中間 / 周辺).""" + + enabled: bool = Field(default=True, description="ゾーン分析の有効/無効") + boundaries: tuple[float, float] = Field( + default=(0.33, 0.66), + description="境界値 (b1, b2).0 < b1 < b2 < 1 を満たす正規化距離", + ) + + @field_validator("boundaries") + @classmethod + def _validate_boundaries(cls, v: tuple[float, float]) -> tuple[float, float]: + """境界値が 0 < b1 < b2 < 1 を満たすことを検証する.""" + b1, b2 = v + if not (0.0 < b1 < b2 < 1.0): + msg = f"boundaries must satisfy 0 < b1 < b2 < 1, got ({b1}, {b2})" + raise ValueError(msg) + return v + + +class MetricsConfig(_FrozenBase): + """算出する均一性指標フラグ設定.""" + + cov: bool = Field(default=True, description="変動係数 (CoV)") + std: bool = Field(default=True, description="標準偏差") + max_min: bool = Field(default=True, description="最大値/最小値比") + radial_profile: bool = Field(default=True, description="放射状プロファイル") + + +class EvaluationConfig(_FrozenBase): + """評価設定(ROI・ゾーン・指標).""" + + roi: RoiConfig + zones: ZonesConfig | None = None + metrics: MetricsConfig | None = None + + +# --------------------------------------------------------------------------- +# Top-level SceneConfig +# --------------------------------------------------------------------------- + + +class SceneConfig(_FrozenBase): + """シーン定義全体を表すトップレベルモデル. + + SPEC_02_シーン定義仕様 に準拠した構造を持つ. + model_validator でメッシュ・光源 ID の一意性,および材料参照整合性を検証する. + """ + + name: str = Field(..., description="設計識別名(出力ディレクトリ名に使用)") + description: str | None = Field(default=None, description="自由記述") + geometry: GeometryConfig + materials: dict[str, MaterialConfig] + lights: list[LightConfig] = Field(..., min_length=1, description="光源リスト(1 個以上)") + sensor: SensorConfig + render: RenderConfig = Field(default_factory=RenderConfig) + evaluation: EvaluationConfig | None = None + + @model_validator(mode="after") + def _validate_cross_references(self) -> SceneConfig: + """メッシュ/光源 ID の一意性と材料参照整合性を検証する. + + 検証内容: + 1. geometry.meshes の各 id が重複していないこと. + 2. lights の各 id が重複していないこと. + 3. 各 mesh.material が materials の dict キーに存在すること. + + Returns: + 検証済みの SceneConfig 自身. + + Raises: + ValueError: 重複 ID または未定義材料参照が見つかった場合. + """ + # 1. mesh.id ユニーク性チェック + seen_mesh_ids: set[str] = set() + for mesh in self.geometry.meshes: + if mesh.id in seen_mesh_ids: + msg = f"Duplicate mesh id: '{mesh.id}'" + raise ValueError(msg) + seen_mesh_ids.add(mesh.id) + + # 2. light.id ユニーク性チェック + # LightConfig は discriminated union なので各エントリは必ず id を持つ + seen_light_ids: set[str] = set() + for light in self.lights: + if light.id in seen_light_ids: + msg = f"Duplicate light id: '{light.id}'" + raise ValueError(msg) + seen_light_ids.add(light.id) + + # 3. mesh.material 参照整合性チェック + material_keys = set(self.materials.keys()) + for mesh in self.geometry.meshes: + if mesh.material not in material_keys: + msg = f"mesh '{mesh.id}' references unknown material '{mesh.material}'" + raise ValueError(msg) + + return self + + +# --------------------------------------------------------------------------- +# YAML Loader +# --------------------------------------------------------------------------- + + +def load_scene_yaml(path: str | Path) -> SceneConfig: + """YAML ファイルを読み込み,検証済み SceneConfig を返す. + + Args: + path: YAML ファイルのパス. + + Returns: + 検証済みの SceneConfig インスタンス. + + Raises: + FileNotFoundError: ファイルが存在しない場合. + pydantic.ValidationError: スキーマ違反. + yaml.YAMLError: YAML 構文エラー. + """ + resolved = Path(path) + if not resolved.exists(): + msg = f"Scene YAML file not found: {resolved}" + raise FileNotFoundError(msg) + + with resolved.open(encoding="utf-8") as f: + data = yaml.safe_load(f) + + return SceneConfig.model_validate(data) diff --git a/tests/scene/__init__.py b/tests/scene/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/scene/__init__.py diff --git a/tests/scene/conftest.py b/tests/scene/conftest.py new file mode 100644 index 0000000..ef94eaf --- /dev/null +++ b/tests/scene/conftest.py @@ -0,0 +1,92 @@ +"""scene テスト共通フィクスチャ. + +有効な最小構成 YAML 文字列を提供する. +異常系テストはこの文字列を replace するか,dict を直接組み立てて検証する. +""" + +import pytest + + +@pytest.fixture +def valid_minimal_yaml() -> str: + """必須 6 セクション(name, geometry, materials, lights, sensor, render)のみの最小 YAML. + + 各テストで replace/加工のベースとして使う. + """ + return """\ +name: test_scene +geometry: + meshes: + - id: housing + path: configs/meshes/housing.stl + unit: mm + material: mat_diffuse +materials: + mat_diffuse: + type: diffuse + reflectance: 0.5 +lights: + - id: led_top + type: point + position: [0.0, 0.0, 50.0] + intensity: [1.0, 1.0, 1.0] + profile: lambertian +sensor: + type: perspective + position: [0.0, 0.0, 100.0] + target: [0.0, 0.0, 0.0] + up: [0.0, 1.0, 0.0] + fov_deg: 60.0 + film: + width: 1920 + height: 1080 +render: + variant: llvm_ad_rgb + integrator: path + max_depth: 8 + spp: 256 +""" + + +@pytest.fixture +def valid_minimal_dict() -> dict: + """valid_minimal_yaml と同内容の Python dict.モデル直接検証に使う.""" + return { + "name": "test_scene", + "geometry": { + "meshes": [ + { + "id": "housing", + "path": "configs/meshes/housing.stl", + "unit": "mm", + "material": "mat_diffuse", + } + ] + }, + "materials": { + "mat_diffuse": {"type": "diffuse", "reflectance": 0.5}, + }, + "lights": [ + { + "id": "led_top", + "type": "point", + "position": [0.0, 0.0, 50.0], + "intensity": [1.0, 1.0, 1.0], + "profile": "lambertian", + } + ], + "sensor": { + "type": "perspective", + "position": [0.0, 0.0, 100.0], + "target": [0.0, 0.0, 0.0], + "up": [0.0, 1.0, 0.0], + "fov_deg": 60.0, + "film": {"width": 1920, "height": 1080}, + }, + "render": { + "variant": "llvm_ad_rgb", + "integrator": "path", + "max_depth": 8, + "spp": 256, + }, + } diff --git a/tests/scene/test_loader.py b/tests/scene/test_loader.py new file mode 100644 index 0000000..fb4da9a --- /dev/null +++ b/tests/scene/test_loader.py @@ -0,0 +1,157 @@ +"""scene/schema.py の load_scene_yaml 関数の仕様ベーステスト. + +仕様出典: docs/04_SPEC/SPEC_02_シーン定義仕様.md +モック方針: ファイル I/O はモックせず tmp_path フィクスチャで実ファイルを使う (GUIDE_08) +""" + +from pathlib import Path + +import pytest +import yaml +from pydantic import ValidationError + +from sm_tias_lightsim.scene import SceneConfig, load_scene_yaml + + +@pytest.fixture +def valid_yaml_content() -> str: + """load_scene_yaml テスト用の有効な YAML 文字列.""" + return """\ +name: loader_test_scene +geometry: + meshes: + - id: housing + path: configs/meshes/housing.stl + unit: mm + material: mat_diffuse +materials: + mat_diffuse: + type: diffuse + reflectance: 0.5 +lights: + - id: led_top + type: point + position: [0.0, 0.0, 50.0] + intensity: [1.0, 1.0, 1.0] + profile: lambertian +sensor: + type: perspective + position: [0.0, 0.0, 100.0] + target: [0.0, 0.0, 0.0] + up: [0.0, 1.0, 0.0] + fov_deg: 60.0 + film: + width: 1920 + height: 1080 +render: + variant: llvm_ad_rgb + integrator: path + max_depth: 8 + spp: 256 +""" + + +def test_load_scene_yaml_returns_scene_config(tmp_path, valid_yaml_content): + """正常な YAML ファイルから SceneConfig インスタンスが返ること. + + 仕様: load_scene_yaml(path) -> SceneConfig (SPEC_02 バリデーション節) + """ + yaml_file = tmp_path / "scene.yaml" + yaml_file.write_text(valid_yaml_content, encoding="utf-8") + + config = load_scene_yaml(yaml_file) + + assert isinstance(config, SceneConfig) + assert config.name == "loader_test_scene" + + +def test_load_scene_yaml_raises_file_not_found(tmp_path): + """存在しないパスを渡したとき FileNotFoundError が発生すること. + + 仕様: FileNotFoundError: ファイルが存在しない場合 (schema.py docstring) + """ + nonexistent = tmp_path / "does_not_exist.yaml" + + with pytest.raises(FileNotFoundError): + load_scene_yaml(nonexistent) + + +def test_load_scene_yaml_raises_yaml_error(tmp_path): + """不正な YAML 構文のファイルを渡したとき yaml.YAMLError 系の例外が発生すること. + + 仕様: yaml.YAMLError: YAML 構文エラー (schema.py docstring) + """ + invalid_yaml = tmp_path / "invalid.yaml" + # タブ文字を含む不正な YAML + invalid_yaml.write_text("name: test\n\t invalid_indent: [unclosed", encoding="utf-8") + + with pytest.raises(yaml.YAMLError): + load_scene_yaml(invalid_yaml) + + +def test_load_scene_yaml_raises_validation_error(tmp_path): + """構文は正しいが必須キーが欠落した YAML で ValidationError が発生すること. + + 仕様: pydantic.ValidationError: スキーマ違反 (schema.py docstring) + """ + # name が欠落した構文的に正しい YAML + missing_name_yaml = """\ +geometry: + meshes: + - id: housing + path: configs/meshes/housing.stl + unit: mm + material: mat_diffuse +materials: + mat_diffuse: + type: diffuse + reflectance: 0.5 +lights: + - id: led_top + type: point + position: [0.0, 0.0, 50.0] + intensity: 1.0 + profile: lambertian +sensor: + type: perspective + position: [0.0, 0.0, 100.0] + target: [0.0, 0.0, 0.0] + up: [0.0, 1.0, 0.0] + fov_deg: 60.0 + film: + width: 1920 + height: 1080 +""" + yaml_file = tmp_path / "missing_name.yaml" + yaml_file.write_text(missing_name_yaml, encoding="utf-8") + + with pytest.raises(ValidationError): + load_scene_yaml(yaml_file) + + +def test_load_scene_yaml_accepts_pathlib_path(tmp_path, valid_yaml_content): + """Path オブジェクトを引数として受け付けること. + + 仕様: load_scene_yaml(path: str | Path) (schema.py シグネチャ) + """ + yaml_file = tmp_path / "scene.yaml" + yaml_file.write_text(valid_yaml_content, encoding="utf-8") + + # Path オブジェクトを渡す + config = load_scene_yaml(Path(yaml_file)) + + assert isinstance(config, SceneConfig) + + +def test_load_scene_yaml_accepts_str_path(tmp_path, valid_yaml_content): + """文字列パスを引数として受け付けること. + + 仕様: load_scene_yaml(path: str | Path) (schema.py シグネチャ) + """ + yaml_file = tmp_path / "scene.yaml" + yaml_file.write_text(valid_yaml_content, encoding="utf-8") + + # 文字列パスを渡す + config = load_scene_yaml(str(yaml_file)) + + assert isinstance(config, SceneConfig) diff --git a/tests/scene/test_schema.py b/tests/scene/test_schema.py new file mode 100644 index 0000000..7a4480a --- /dev/null +++ b/tests/scene/test_schema.py @@ -0,0 +1,517 @@ +"""scene/schema.py の仕様ベーステスト. + +仕様出典: docs/04_SPEC/SPEC_02_シーン定義仕様.md +""" + +import copy + +import pytest +import yaml +from pydantic import ValidationError + +from sm_tias_lightsim.scene.schema import ( + AreaLight, + DielectricMaterial, + DiffuseMaterial, + DirectionalLight, + PointLight, + SceneConfig, + ThinDielectricMaterial, +) + +# --------------------------------------------------------------------------- +# 正常系: 最小構成 +# --------------------------------------------------------------------------- + + +def test_minimal_valid_yaml_parses(valid_minimal_yaml): + """必須 6 セクションのみで SceneConfig が生成されること. + + 仕様: name, geometry, materials, lights, sensor, render が必須 (SPEC_02 トップレベル構造) + """ + data = yaml.safe_load(valid_minimal_yaml) + config = SceneConfig.model_validate(data) + assert config.name == "test_scene" + assert len(config.geometry.meshes) == 1 + assert len(config.lights) == 1 + + +def test_optional_evaluation_section(valid_minimal_dict): + """evaluation を省略しても SceneConfig が生成されること. + + 仕様: evaluation は任意(SPEC_02 evaluation セクション) + """ + d = copy.deepcopy(valid_minimal_dict) + d.pop("evaluation", None) + config = SceneConfig.model_validate(d) + assert config.evaluation is None + + +def test_optional_description(valid_minimal_dict): + """description を省略しても SceneConfig が生成されること. + + 仕様: description は任意(SPEC_02 トップレベル構造) + """ + d = copy.deepcopy(valid_minimal_dict) + d.pop("description", None) + config = SceneConfig.model_validate(d) + assert config.description is None + + +def test_render_defaults(valid_minimal_dict): + """render セクションを省略しても既定値が設定されること. + + 仕様: render.variant デフォルト llvm_ad_rgb, spp デフォルト 256 (SPEC_02 render セクション) + """ + d = copy.deepcopy(valid_minimal_dict) + d.pop("render", None) + config = SceneConfig.model_validate(d) + assert config.render.variant == "llvm_ad_rgb" + assert config.render.spp == 256 + assert config.render.integrator == "path" + assert config.render.max_depth == 8 + + +# --------------------------------------------------------------------------- +# 正常系: マテリアル +# --------------------------------------------------------------------------- + + +def test_diffuse_material_with_scalar_reflectance(valid_minimal_dict): + """reflectance をスカラ値で指定できること. + + 仕様: reflectance スカラ可 (SPEC_02 materials セクション) + """ + d = copy.deepcopy(valid_minimal_dict) + d["materials"]["mat_diffuse"] = {"type": "diffuse", "reflectance": 0.95} + config = SceneConfig.model_validate(d) + mat = config.materials["mat_diffuse"] + assert isinstance(mat, DiffuseMaterial) + assert mat.reflectance == pytest.approx(0.95) + + +def test_diffuse_material_with_rgb_reflectance(valid_minimal_dict): + """reflectance を RGB リストで指定できること. + + 仕様: reflectance: [0.05, 0.05, 0.05] の RGB 表記 (SPEC_02 materials セクション) + """ + d = copy.deepcopy(valid_minimal_dict) + d["materials"]["mat_diffuse"] = {"type": "diffuse", "reflectance": [0.05, 0.05, 0.05]} + config = SceneConfig.model_validate(d) + mat = config.materials["mat_diffuse"] + assert isinstance(mat, DiffuseMaterial) + assert mat.reflectance == pytest.approx((0.05, 0.05, 0.05)) + + +def test_dielectric_and_thindielectric_materials(valid_minimal_dict): + """dielectric / thindielectric の type が正しく識別されること. + + 仕様: type: dielectric / thindielectric の discriminated union (SPEC_02 materials セクション) + """ + d = copy.deepcopy(valid_minimal_dict) + d["materials"]["mat_dielectric"] = { + "type": "dielectric", + "int_ior": 1.5, + "ext_ior": 1.0, + } + d["materials"]["mat_thin"] = { + "type": "thindielectric", + "int_ior": 1.49, + "ext_ior": 1.0, + } + d["geometry"]["meshes"].append( + {"id": "mesh2", "path": "m.stl", "unit": "mm", "material": "mat_dielectric"} + ) + d["geometry"]["meshes"].append( + {"id": "mesh3", "path": "m.stl", "unit": "mm", "material": "mat_thin"} + ) + config = SceneConfig.model_validate(d) + assert isinstance(config.materials["mat_dielectric"], DielectricMaterial) + assert isinstance(config.materials["mat_thin"], ThinDielectricMaterial) + + +# --------------------------------------------------------------------------- +# 正常系: ライト +# --------------------------------------------------------------------------- + + +def test_point_light_parses(valid_minimal_dict): + """point 光源の必須フィールドが正しくパースされること. + + 仕様: type: point, position, intensity, profile (SPEC_02 lights セクション) + """ + d = copy.deepcopy(valid_minimal_dict) + config = SceneConfig.model_validate(d) + light = config.lights[0] + assert isinstance(light, PointLight) + assert light.id == "led_top" + assert light.type == "point" + assert light.position == pytest.approx((0.0, 0.0, 50.0)) + assert light.profile == "lambertian" + + +def test_area_light_parses(valid_minimal_dict): + """area 光源の必須フィールドが正しくパースされること. + + 仕様: type: area, shape, center, size, direction, radiance (SPEC_02 lights セクション) + """ + d = copy.deepcopy(valid_minimal_dict) + d["lights"] = [ + { + "id": "led_area", + "type": "area", + "shape": "rectangle", + "center": [0.0, 0.0, 20.0], + "size": [10.0, 10.0], + "direction": [0.0, 0.0, -1.0], + "radiance": [50.0, 50.0, 50.0], + } + ] + config = SceneConfig.model_validate(d) + light = config.lights[0] + assert isinstance(light, AreaLight) + assert light.id == "led_area" + assert light.shape == "rectangle" + assert light.size == pytest.approx((10.0, 10.0)) + assert light.radiance == pytest.approx((50.0, 50.0, 50.0)) + + +def test_directional_light_parses(valid_minimal_dict): + """directional 光源が正しくパースされること. + + 仕様: type: directional, direction, irradiance (SPEC_02 lights セクション) + """ + d = copy.deepcopy(valid_minimal_dict) + d["lights"] = [ + { + "id": "sun", + "type": "directional", + "direction": [0.0, 0.0, -1.0], + "irradiance": 1.0, + } + ] + config = SceneConfig.model_validate(d) + light = config.lights[0] + assert isinstance(light, DirectionalLight) + assert light.id == "sun" + assert light.direction == pytest.approx((0.0, 0.0, -1.0)) + + +# --------------------------------------------------------------------------- +# 正常系: evaluation / zones +# --------------------------------------------------------------------------- + + +def test_zones_default_boundaries(valid_minimal_dict): + """boundaries を省略したとき既定値 (0.33, 0.66) が設定されること. + + 仕様: boundaries デフォルト [0.33, 0.66] (SPEC_02 evaluation セクション) + """ + d = copy.deepcopy(valid_minimal_dict) + d["evaluation"] = { + "roi": {"type": "image_pixel", "x": 0, "y": 0, "width": 100, "height": 100}, + "zones": {"enabled": True}, + } + config = SceneConfig.model_validate(d) + assert config.evaluation is not None + assert config.evaluation.zones is not None + assert config.evaluation.zones.boundaries == pytest.approx((0.33, 0.66)) + + +# --------------------------------------------------------------------------- +# 異常系: 必須キー欠落 +# --------------------------------------------------------------------------- + + +def test_missing_name_raises(valid_minimal_dict): + """name が欠落したとき ValidationError が発生すること. + + 仕様: name 必須 (SPEC_02 トップレベル構造) + """ + d = copy.deepcopy(valid_minimal_dict) + del d["name"] + with pytest.raises(ValidationError): + SceneConfig.model_validate(d) + + +def test_missing_geometry_raises(valid_minimal_dict): + """geometry が欠落したとき ValidationError が発生すること. + + 仕様: geometry 必須 (SPEC_02 トップレベル構造) + """ + d = copy.deepcopy(valid_minimal_dict) + del d["geometry"] + with pytest.raises(ValidationError): + SceneConfig.model_validate(d) + + +def test_missing_materials_raises(valid_minimal_dict): + """materials が欠落したとき ValidationError が発生すること. + + 仕様: materials 必須 (SPEC_02 トップレベル構造) + """ + d = copy.deepcopy(valid_minimal_dict) + del d["materials"] + with pytest.raises(ValidationError): + SceneConfig.model_validate(d) + + +def test_missing_lights_raises(valid_minimal_dict): + """lights が欠落したとき ValidationError が発生すること. + + 仕様: lights 必須 (SPEC_02 トップレベル構造) + """ + d = copy.deepcopy(valid_minimal_dict) + del d["lights"] + with pytest.raises(ValidationError): + SceneConfig.model_validate(d) + + +def test_missing_sensor_raises(valid_minimal_dict): + """sensor が欠落したとき ValidationError が発生すること. + + 仕様: sensor 必須 (SPEC_02 トップレベル構造) + """ + d = copy.deepcopy(valid_minimal_dict) + del d["sensor"] + with pytest.raises(ValidationError): + SceneConfig.model_validate(d) + + +def test_empty_lights_list_raises(valid_minimal_dict): + """lights が空リストのとき ValidationError が発生すること. + + 仕様: lights は 1 個以上必須 (SPEC_02 lights セクション) + """ + d = copy.deepcopy(valid_minimal_dict) + d["lights"] = [] + with pytest.raises(ValidationError): + SceneConfig.model_validate(d) + + +def test_empty_meshes_list_raises(valid_minimal_dict): + """geometry.meshes が空リストのとき ValidationError が発生すること. + + 仕様: meshes は 1 個以上必須(min_length=1) + """ + d = copy.deepcopy(valid_minimal_dict) + d["geometry"]["meshes"] = [] + with pytest.raises(ValidationError): + SceneConfig.model_validate(d) + + +# --------------------------------------------------------------------------- +# 異常系: 参照整合性・ID 重複 +# --------------------------------------------------------------------------- + + +def test_dangling_material_reference_raises(valid_minimal_dict): + """存在しない material を mesh が参照したとき ValidationError が発生し, + エラーメッセージに mesh id と material name が含まれること. + + 仕様: 参照整合性チェック (SPEC_02 バリデーション節) + """ + d = copy.deepcopy(valid_minimal_dict) + d["geometry"]["meshes"][0]["material"] = "nonexistent_material" + with pytest.raises(ValidationError) as exc_info: + SceneConfig.model_validate(d) + error_str = str(exc_info.value) + assert "housing" in error_str + assert "nonexistent_material" in error_str + + +def test_duplicate_mesh_id_raises(valid_minimal_dict): + """同じ mesh.id が 2 つあるとき ValidationError が発生すること. + + 仕様: ID の重複チェック (SPEC_02 バリデーション節) + """ + d = copy.deepcopy(valid_minimal_dict) + d["geometry"]["meshes"].append( + { + "id": "housing", # 重複 + "path": "other.stl", + "unit": "mm", + "material": "mat_diffuse", + } + ) + with pytest.raises(ValidationError): + SceneConfig.model_validate(d) + + +def test_duplicate_light_id_raises(valid_minimal_dict): + """同じ light.id が 2 つあるとき ValidationError が発生すること. + + 仕様: ID の重複チェック (SPEC_02 バリデーション節) + """ + d = copy.deepcopy(valid_minimal_dict) + d["lights"].append( + { + "id": "led_top", # 重複 + "type": "point", + "position": [1.0, 0.0, 50.0], + "intensity": 1.0, + "profile": "isotropic", + } + ) + with pytest.raises(ValidationError): + SceneConfig.model_validate(d) + + +# --------------------------------------------------------------------------- +# 異常系: 型・値範囲 +# --------------------------------------------------------------------------- + + +def test_invalid_unit_cm_raises(valid_minimal_dict): + """unit: cm を指定したとき ValidationError が発生すること. + + 仕様: unit は mm | m のみ (SPEC_02 geometry セクション) + """ + d = copy.deepcopy(valid_minimal_dict) + d["geometry"]["meshes"][0]["unit"] = "cm" + with pytest.raises(ValidationError): + SceneConfig.model_validate(d) + + +def test_invalid_material_type_raises(valid_minimal_dict): + """未対応の material type(mask 等)で ValidationError が発生すること. + + 仕様: type は diffuse / dielectric / thindielectric の discriminated union (SPEC_02) + 注: SPEC_02 では mask が将来拡張として記載されているが,現在はサポート外 + """ + d = copy.deepcopy(valid_minimal_dict) + d["materials"]["mat_diffuse"] = {"type": "mask", "opacity": 0.5} + with pytest.raises(ValidationError): + SceneConfig.model_validate(d) + + +def test_invalid_light_type_raises(valid_minimal_dict): + """未対応の light type(laser 等)で ValidationError が発生すること. + + 仕様: type は point / area / directional の discriminated union (SPEC_02 lights セクション) + """ + d = copy.deepcopy(valid_minimal_dict) + d["lights"] = [ + { + "id": "laser_1", + "type": "laser", + "position": [0.0, 0.0, 50.0], + "intensity": 1.0, + } + ] + with pytest.raises(ValidationError): + SceneConfig.model_validate(d) + + +def test_invalid_sensor_type_raises(valid_minimal_dict): + """type: orthographic を指定したとき ValidationError が発生すること. + + 仕様: sensor.type は perspective のみ (SPEC_02 sensor セクション) + """ + d = copy.deepcopy(valid_minimal_dict) + d["sensor"]["type"] = "orthographic" + with pytest.raises(ValidationError): + SceneConfig.model_validate(d) + + +def test_fov_zero_raises(valid_minimal_dict): + """fov_deg: 0 のとき ValidationError が発生すること. + + 仕様: 0 < fov_deg < 180 (SPEC_02 バリデーション節) + """ + d = copy.deepcopy(valid_minimal_dict) + d["sensor"]["fov_deg"] = 0.0 + with pytest.raises(ValidationError): + SceneConfig.model_validate(d) + + +def test_fov_180_raises(valid_minimal_dict): + """fov_deg: 180 のとき ValidationError が発生すること. + + 仕様: 0 < fov_deg < 180(端点は含まない)(SPEC_02 バリデーション節) + """ + d = copy.deepcopy(valid_minimal_dict) + d["sensor"]["fov_deg"] = 180.0 + with pytest.raises(ValidationError): + SceneConfig.model_validate(d) + + +def test_zones_invalid_boundaries_raises(valid_minimal_dict): + """boundaries: [0.66, 0.33](逆順)のとき ValidationError が発生すること. + + 仕様: 0 < b1 < b2 < 1 (SPEC_02 evaluation/zones セクション) + """ + d = copy.deepcopy(valid_minimal_dict) + d["evaluation"] = { + "roi": {"type": "image_pixel", "x": 0, "y": 0, "width": 100, "height": 100}, + "zones": {"enabled": True, "boundaries": [0.66, 0.33]}, + } + with pytest.raises(ValidationError): + SceneConfig.model_validate(d) + + +def test_zones_boundaries_out_of_range_raises(valid_minimal_dict): + """boundaries: [0.0, 1.0](範囲外)のとき ValidationError が発生すること. + + 仕様: 0 < b1 < b2 < 1(端点 0 と 1 は含まない)(SPEC_02 evaluation/zones セクション) + """ + d = copy.deepcopy(valid_minimal_dict) + d["evaluation"] = { + "roi": {"type": "image_pixel", "x": 0, "y": 0, "width": 100, "height": 100}, + "zones": {"enabled": True, "boundaries": [0.0, 1.0]}, + } + with pytest.raises(ValidationError): + SceneConfig.model_validate(d) + + +# --------------------------------------------------------------------------- +# 異常系: フィールドの細部 +# --------------------------------------------------------------------------- + + +def test_film_dimensions_must_be_positive(valid_minimal_dict): + """film.width: 0 のとき ValidationError が発生すること. + + 仕様: width >= 1 (ge=1) + """ + d = copy.deepcopy(valid_minimal_dict) + d["sensor"]["film"]["width"] = 0 + with pytest.raises(ValidationError): + SceneConfig.model_validate(d) + + +def test_max_depth_must_be_positive(valid_minimal_dict): + """render.max_depth: 0 のとき ValidationError が発生すること. + + 仕様: max_depth >= 1 (ge=1) + """ + d = copy.deepcopy(valid_minimal_dict) + d["render"]["max_depth"] = 0 + with pytest.raises(ValidationError): + SceneConfig.model_validate(d) + + +def test_spp_must_be_positive(valid_minimal_dict): + """render.spp: 0 のとき ValidationError が発生すること. + + 仕様: spp >= 1 (ge=1) + """ + d = copy.deepcopy(valid_minimal_dict) + d["render"]["spp"] = 0 + with pytest.raises(ValidationError): + SceneConfig.model_validate(d) + + +# --------------------------------------------------------------------------- +# イミュータビリティ +# --------------------------------------------------------------------------- + + +def test_scene_config_is_frozen(valid_minimal_dict): + """パース後の SceneConfig インスタンスへの代入で ValidationError が発生すること. + + 仕様: frozen=True (schema.py ConfigDict) + """ + config = SceneConfig.model_validate(valid_minimal_dict) + with pytest.raises(ValidationError): + config.name = "modified" # type: ignore[misc] diff --git a/uv.lock b/uv.lock index 6c52c49..78a87fb 100644 --- a/uv.lock +++ b/uv.lock @@ -11,6 +11,15 @@ ] [[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } @@ -534,6 +543,96 @@ ] [[package]] +name = "pydantic" +version = "2.13.3" +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/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/cb/5b47425556ecc1f3fe18ed2a0083188aa46e1dd812b06e406475b3a5d536/pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", size = 2101946, upload-time = "2026-04-20T14:40:52.581Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/2fb62c2267cae99b815bbf4a7b9283812c88ca3153ef29f7707200f1d4e5/pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", size = 1951612, upload-time = "2026-04-20T14:42:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/50/6e/b7348fd30d6556d132cddd5bd79f37f96f2601fe0608afac4f5fb01ec0b3/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", size = 1977027, upload-time = "2026-04-20T14:42:02.001Z" }, + { url = "https://files.pythonhosted.org/packages/82/11/31d60ee2b45540d3fb0b29302a393dbc01cd771c473f5b5147bcd353e593/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", size = 2063008, upload-time = "2026-04-20T14:44:17.952Z" }, + { url = "https://files.pythonhosted.org/packages/8a/db/3a9d1957181b59258f44a2300ab0f0be9d1e12d662a4f57bb31250455c52/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", size = 2233082, upload-time = "2026-04-20T14:40:57.934Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e1/3277c38792aeb5cfb18c2f0c5785a221d9ff4e149abbe1184d53d5f72273/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", size = 2304615, upload-time = "2026-04-20T14:42:12.584Z" }, + { url = "https://files.pythonhosted.org/packages/5e/d5/e3d9717c9eba10855325650afd2a9cba8e607321697f18953af9d562da2f/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", size = 2094380, upload-time = "2026-04-20T14:43:05.522Z" }, + { url = "https://files.pythonhosted.org/packages/a1/20/abac35dedcbfd66c6f0b03e4e3564511771d6c9b7ede10a362d03e110d9b/pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", size = 2135429, upload-time = "2026-04-20T14:41:55.549Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a5/41bfd1df69afad71b5cf0535055bccc73022715ad362edbc124bc1e021d7/pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", size = 2174582, upload-time = "2026-04-20T14:41:45.96Z" }, + { url = "https://files.pythonhosted.org/packages/79/65/38d86ea056b29b2b10734eb23329b7a7672ca604df4f2b6e9c02d4ee22fe/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", size = 2187533, upload-time = "2026-04-20T14:40:55.367Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/a1129141678a2026badc539ad1dee0a71d06f54c2f06a4bd68c030ac781b/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", size = 2332985, upload-time = "2026-04-20T14:44:13.05Z" }, + { url = "https://files.pythonhosted.org/packages/d7/60/cb26f4077719f709e54819f4e8e1d43f4091f94e285eb6bd21e1190a7b7c/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", size = 2373670, upload-time = "2026-04-20T14:41:53.421Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7e/c3f21882bdf1d8d086876f81b5e296206c69c6082551d776895de7801fa0/pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", size = 1966722, upload-time = "2026-04-20T14:44:30.588Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/6b5e757b859013ebfbd7adba02f23b428f37c86dcbf78b5bb0b4ffd36e99/pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", size = 2072970, upload-time = "2026-04-20T14:42:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f8/a989b21cc75e9a32d24192ef700eea606521221a89faa40c919ce884f2b1/pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", size = 2035963, upload-time = "2026-04-20T14:44:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3c/9b5e8eb9821936d065439c3b0fb1490ffa64163bfe7e1595985a47896073/pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", size = 2102109, upload-time = "2026-04-20T14:41:24.219Z" }, + { url = "https://files.pythonhosted.org/packages/91/97/1c41d1f5a19f241d8069f1e249853bcce378cdb76eec8ab636d7bc426280/pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", size = 1951820, upload-time = "2026-04-20T14:42:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/30/b4/d03a7ae14571bc2b6b3c7b122441154720619afe9a336fa3a95434df5e2f/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", size = 1977785, upload-time = "2026-04-20T14:42:31.648Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0c/4086f808834b59e3c8f1aa26df8f4b6d998cdcf354a143d18ef41529d1fe/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", size = 2062761, upload-time = "2026-04-20T14:40:37.093Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/a649be5a5064c2df0db06e0a512c2281134ed2fcc981f52a657936a7527c/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", size = 2232989, upload-time = "2026-04-20T14:42:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/7756e75763e810b3a710f4724441d1ecc5883b94aacb07ca71c5fb5cfb69/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", size = 2303975, upload-time = "2026-04-20T14:41:32.287Z" }, + { url = "https://files.pythonhosted.org/packages/6c/35/68a762e0c1e31f35fa0dac733cbd9f5b118042853698de9509c8e5bf128b/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", size = 2095325, upload-time = "2026-04-20T14:42:47.685Z" }, + { url = "https://files.pythonhosted.org/packages/77/bf/1bf8c9a8e91836c926eae5e3e51dce009bf495a60ca56060689d3df3f340/pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", size = 2133368, upload-time = "2026-04-20T14:41:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/e5/50/87d818d6bab915984995157ceb2380f5aac4e563dddbed6b56f0ed057aba/pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", size = 2173908, upload-time = "2026-04-20T14:42:52.044Z" }, + { url = "https://files.pythonhosted.org/packages/91/88/a311fb306d0bd6185db41fa14ae888fb81d0baf648a761ae760d30819d33/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", size = 2186422, upload-time = "2026-04-20T14:43:29.55Z" }, + { url = "https://files.pythonhosted.org/packages/8f/79/28fd0d81508525ab2054fef7c77a638c8b5b0afcbbaeee493cf7c3fef7e1/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", size = 2332709, upload-time = "2026-04-20T14:42:16.134Z" }, + { url = "https://files.pythonhosted.org/packages/b3/21/795bf5fe5c0f379308b8ef19c50dedab2e7711dbc8d0c2acf08f1c7daa05/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", size = 2372428, upload-time = "2026-04-20T14:41:10.974Z" }, + { url = "https://files.pythonhosted.org/packages/45/b3/ed14c659cbe7605e3ef063077680a64680aec81eb1a04763a05190d49b7f/pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", size = 1965601, upload-time = "2026-04-20T14:41:42.128Z" }, + { url = "https://files.pythonhosted.org/packages/ef/bb/adb70d9a762ddd002d723fbf1bd492244d37da41e3af7b74ad212609027e/pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", size = 2071517, upload-time = "2026-04-20T14:43:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/52/eb/66faefabebfe68bd7788339c9c9127231e680b11906368c67ce112fdb47f/pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", size = 2035802, upload-time = "2026-04-20T14:43:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" }, + { url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" }, + { url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" }, + { url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" }, + { url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" }, + { url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" }, + { url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" }, + { url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" }, + { url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" }, + { url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" }, + { url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" }, + { url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" }, + { url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/34/42/f426db557e8ab2791bc7562052299944a118655496fbff99914e564c0a94/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", size = 2091877, upload-time = "2026-04-20T14:43:27.091Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4f/86a832a9d14df58e663bfdf4627dc00d3317c2bd583c4fb23390b0f04b8e/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", size = 1932428, upload-time = "2026-04-20T14:40:45.781Z" }, + { url = "https://files.pythonhosted.org/packages/11/1a/fe857968954d93fb78e0d4b6df5c988c74c4aaa67181c60be7cfe327c0ca/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", size = 1997550, upload-time = "2026-04-20T14:44:02.425Z" }, + { url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" }, +] + +[[package]] name = "pygments" version = "2.20.0" source = { registry = "https://pypi.org/simple" } @@ -580,6 +679,52 @@ ] [[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] name = "ruff" version = "0.15.12" source = { registry = "https://pypi.org/simple" } @@ -623,6 +768,8 @@ { name = "numpy" }, { name = "pandas" }, { name = "pillow" }, + { name = "pydantic" }, + { name = "pyyaml" }, { name = "trimesh" }, ] @@ -639,6 +786,8 @@ { name = "numpy", specifier = ">=2.4.4" }, { name = "pandas", specifier = ">=3.0.2" }, { name = "pillow", specifier = ">=12.2.0" }, + { name = "pydantic", specifier = ">=2.13.3" }, + { name = "pyyaml", specifier = ">=6.0.3" }, { name = "trimesh", specifier = ">=4.12.1" }, ] @@ -661,6 +810,27 @@ ] [[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] name = "tzdata" version = "2026.2" source = { registry = "https://pypi.org/simple" }