diff --git a/.gitignore b/.gitignore index 880c208..8639605 100644 --- a/.gitignore +++ b/.gitignore @@ -223,3 +223,6 @@ justfile.local # Cursor local config .cursor/ + +# Example script runtime output +examples/output/ diff --git a/.typos.toml b/.typos.toml index e31e352..8c2a703 100644 --- a/.typos.toml +++ b/.typos.toml @@ -17,6 +17,10 @@ macor = "macor" Ba = "Ba" # Chemical element symbols that otherwise look like typos. Nd = "Nd" +# Three.js MeshPhysicalMaterial parameter name. typos flags this +# as a typo for "metallicity", which isn't a word. See ADR-0002. +metalness = "metalness" +metalnessMap = "metalnessMap" [files] extend-exclude = [ diff --git a/docs/decisions/0002-pbr-via-threejs-materials-optional-extra.md b/docs/decisions/0002-pbr-via-threejs-materials-optional-extra.md new file mode 100644 index 0000000..42dfecb --- /dev/null +++ b/docs/decisions/0002-pbr-via-threejs-materials-optional-extra.md @@ -0,0 +1,234 @@ +# 0002. PBR integration via threejs-materials as optional `[pbr]` extra + +- Status: Accepted +- Date: 2026-04-15 +- Deciders: @gerchowl +- Context issue: [#3](https://github.com/MorePET/mat/issues/3) + +## Context + +Issue #3 (from Roger Maitland / @gumyr, author of build123d) asks for +`py-materials` to become the material layer for build123d, with +support for loading PBR (physically-based rendering) materials from +the four major open MaterialX libraries — ambientcg, polyhaven, +gpuopen, physicallybased.info — so build123d shapes can carry +texture-driven materials and render in `ocp_vscode` with full PBR. + +Investigation turned up [`bernhard-42/threejs-materials`][tjm], a +pure-Python Apache-2.0 library (v1.0.0) that **already does all the +heavy lifting**: + +- Loaders for all four MaterialX sources +- MaterialX shader-graph baking into flat textures +- Texture cache + `resolution` tier selection +- Output as Three.js `MeshPhysicalMaterial`-shaped JSON + +`ocp_vscode` already consumes it directly — the example at +`bernhard-42/vscode-ocp-cad-viewer/examples/material-object.py` has + +```python +shader_ball.material = PbrProperties.from_gpuopen("Stainless Steel Brushed") +``` + +with a `FutureWarning` saying *"the required type of +`build123d`'s `shape.material` will change"*. That warning is +explicitly waiting for `py-materials` to become the canonical +carrier type. + +**Design question**: how should `py-materials` integrate with +`threejs-materials` and `build123d`? + +## Decision + +`py-materials` **depends on `threejs-materials` as an optional +`[pbr]` extra** and defines a narrow `PbrSource` typing `Protocol` +that both the native lite `PBRProperties` dataclass and +`threejs_materials.PbrProperties` conform to. `Material` gains an +optional `pbr_source` field typed as `Optional[PbrSource]` that +carries the rich backend when present, alongside the existing +`properties.pbr` (the lite native in-tree dataclass). + +Concretely: + +```python +# pymat/pbr/_protocol.py +@runtime_checkable +class PbrSource(Protocol): + def to_dict(self) -> dict: ... + +# pymat/pbr/__init__.py +try: + from threejs_materials import PbrProperties # when [pbr] extra installed +except ImportError: + pass +``` + +```python +# pymat/core.py +@dataclass +class _MaterialInternal: + ... + pbr_source: Optional["PbrSource"] = None + + def to_three_js_material_dict(self) -> dict: + """Pick the right backend and emit Three.js JSON.""" + if self.pbr_source is not None: + return self.pbr_source.to_dict() + return self.properties.pbr.to_dict() +``` + +Usage: + +```python +from pymat import Material +from pymat.pbr import PbrProperties # requires [pbr] extra + +steel = Material( + name="Brushed Steel", + density=7.85, + formula="Fe", + pbr_source=PbrProperties.from_gpuopen("Stainless Steel Brushed"), +) +shape.material = steel # future build123d.Shape.material integration + +# Both consumers read from the same object: +json_for_viewer = steel.to_three_js_material_dict() +density_for_mass = steel.density +molar_mass_for_radiation = steel.molar_mass # see ADR-0001 +``` + +## Backfill pattern (graceful enhancement for existing consumers) + +Setting `Material.pbr_source` also **projects the rich backend's +serialized fields onto the lite `properties.pbr` dataclass** via an +internal `_backfill_pbr_from_source()` pass in `__post_init__`. This +copies the overlapping fields (color, metalness, roughness, ior, +emissive, transmission, clearcoat, normal/roughness/metalness/ao +maps) from `pbr_source.to_dict()` into the lite dataclass +one-way at construction time. + +Why: existing downstream renderers that read +`material.properties.pbr.` directly — for example, +`ocp_vscode`'s `_extract_materials_from_node()` in `show.py`, which +reads `base_color`, `metallic`, `roughness`, `normal_map`, etc. — +pick up the rich-backend data **without any code change on their +side**. A user can assign +`material.pbr_source = PbrProperties.from_gpuopen("...")` and +`ocp_vscode.show()` will render with the MaterialX textures today. + +Fields on the rich source that don't have a corresponding lite +field (sheen, anisotropy, iridescence, dispersion, clearcoat +normal/roughness maps, specular, thickness, displacement, etc.) +are dropped in the projection — the lite dataclass is a lossy +subset. Consumers that can handle the full fidelity should read +`material.pbr_source` directly or call +`material.to_three_js_material_dict()`, which delegates to the +rich source first and so preserves every field. + +The backfill is a one-way copy at `__post_init__` — it does not +keep the lite dataclass in sync if the rich source is mutated +later. That's intentional: mutating a loaded PBR material after +assignment is unusual, and re-assigning `pbr_source` will re-run +the backfill. + +## Consequences + +**Enables**: + +- **Physics users stay lean.** `pip install py-materials` does not + pull `pillow`, `pygltflib`, `requests`, or any of the + texture-library HTTP surface. Monte Carlo particle-transport + users (the README's primary use case) are unaffected by this + ADR. +- **PBR users get full MaterialX support** with a single extra: + `pip install py-materials[pbr]`. Downloads, caches, baking, and + Three.js output are all handled by `threejs-materials` without + `py-materials` maintaining the HTTP / texture / MaterialX + code. +- **The canonical type for `shape.material` is `pymat.Material`**, + carrying both physics (density, thermal, molar mass) AND PBR + (via `pbr_source`). `ocp_vscode` / build123d viewers call + `material.to_three_js_material_dict()` and get uniform output + regardless of backend. +- **Zero cross-repo code duplication.** `threejs-materials` stays + the single source of truth for PBR loading. `py-materials` stays + the single source of truth for materials science. `build123d` + stays the single source of truth for CAD shapes. +- **Independent release cadences.** Each library evolves on its + own schedule. Version-compat is a semver pin, managed by + dependabot. +- **Protocol-based typing** lets users plug in custom PBR backends + (for example, a future `pymat.pbr` loader for a proprietary + texture library) without touching py-materials. + +**Costs**: + +- **Two parallel PBR code paths** on py-materials' side. The lite + `PBRProperties` dataclass stays (for TOML-authored materials and + users without the extra) and grows a `to_dict()` + method that implements the Protocol. The rich path is the + optional extra. Some duplication of intent between the two + serializers is unavoidable; their outputs may drift on edge + cases unless consciously kept in sync. +- **`threejs-materials` v2.x release would be a coordinated + update** with a dependabot PR + CI matrix check. Not much effort + but requires attention. +- **First-time users of the `[pbr]` extra pay a cold-install + cost**: `pillow`, `pygltflib`, `requests` plus their transitive + deps. ~10-20 MB added to the environment. + +**Rules out**: + +- **Vendoring `threejs-materials` into `py-materials`** (the + "absorb" option). That would require py-materials to track + Bernhard's changes manually, duplicate ~3000 lines of code, + and compete with a library that's actively maintained by + someone else. +- **Making `threejs-materials` depend on `py-materials`** + (reverse direction). Semantically wrong — the physics layer + shouldn't be a transitive dep of a rendering loader. +- **A required `threejs-materials` dep on `py-materials`**. + Bloats the physics-only install. + +## Alternatives considered + +- **Option I — required dep**: `threejs-materials` in + `dependencies`. Rejected: bloats physics-only users who don't + care about PBR rendering. +- **Option III — reverse dep**: `threejs-materials` depends on + `py-materials`. Rejected: conceptually backwards, couples + Bernhard's library to our release cadence. +- **Option IV — Protocol-only, no dep either way**: users install + both libraries manually and the Protocol is the only + connection. Rejected as the first user experience: + `pip install py-materials[pbr]` is the obvious wire. +- **Option V — absorb `threejs-materials` into `py-materials`**: + vendor the code. Rejected for the reasons above. +- **Option VI — no PBR integration**: py-materials stays + physics-only. Rejected: violates the spirit of issue #3 and + the build123d integration story. + +See the session discussion on [#3][#3] for the full +option-matrix and first-principles analysis. + +## Upgrade trigger + +Revisit this ADR if any of these happen: + +1. **`threejs-materials` adds a hard dep that py-materials users + object to** (e.g., a GPU-accelerated texture baker). Might + force reverting to a vendored or proxied approach. +2. **A second PBR backend emerges** (e.g., an OpenUSD-native + loader) that users want alongside `threejs-materials`. The + Protocol already supports this — just update the docs and + `pymat.pbr.__init__` to pick up the second backend when + installed. +3. **Bernhard steps away from `threejs-materials`**. py-materials + may then need to either fork or rewrite. The Protocol boundary + means either option keeps the downstream API stable. +4. **The `[pbr]` install cost becomes painful for common users** + (e.g., Pillow stops being pure-Python). Might need to trim or + split the extra. + +[tjm]: https://github.com/bernhard-42/threejs-materials +[#3]: https://github.com/MorePET/mat/issues/3 diff --git a/examples/pbr_integration.py b/examples/pbr_integration.py new file mode 100644 index 0000000..107ae99 --- /dev/null +++ b/examples/pbr_integration.py @@ -0,0 +1,133 @@ +""" +End-to-end example: Material with physics + PBR, Three.js JSON output. + +Demonstrates the `pymat.pbr` Protocol-based integration from ADR-0002. +Works with both the lite in-tree backend (no extra deps) and the rich +`threejs-materials` backend (install `pip install py-materials[pbr]`). + +Run: + python examples/pbr_integration.py + +Outputs: + - Physics properties to stdout + - Three.js MeshPhysicalMaterial dict to stdout + - Writes the JSON to `examples/output/` for downstream viewer + consumption + +This example deliberately avoids pulling in `build123d` or +`ocp_vscode` — it's the minimal py-materials-only demo. For the full +integration with build123d's `Shape.material` and live rendering in +`ocp_vscode`, see the matching example on the build123d fork: +`gerchowl/build123d@feature/pymat-material-integration:examples/pbr_material_pymat.py`, +which composes all three libraries. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +from pymat import Material +from pymat.pbr import PbrSource + +OUTPUT_DIR = Path(__file__).parent / "output" +OUTPUT_DIR.mkdir(exist_ok=True) + + +def build_steel_with_lite_pbr() -> Material: + """ + Build a `Material` using only the native in-tree PBR backend. + + This path works with `pip install py-materials` — no extras + needed. Physics users get a usable material with basic PBR + scalar values; no texture maps. + """ + return Material( + name="Stainless Steel 304", + density=8.0, + formula="Fe", # dominant element, approximated for molar mass + mechanical={"youngs_modulus": 193, "yield_strength": 170}, + thermal={"melting_point": 1450, "thermal_conductivity": 15.1}, + pbr={ + "base_color": (0.75, 0.75, 0.77, 1.0), + "metallic": 1.0, + "roughness": 0.35, + }, + ) + + +def build_steel_with_rich_pbr() -> Material | None: + """ + Build a `Material` using the rich `threejs-materials` backend. + + Requires `pip install py-materials[pbr]`. Downloads the + "Stainless Steel Brushed" MaterialX material from + matlib.gpuopen.com on first run and caches it for subsequent + runs. Returns None if the extra is not installed. + """ + try: + from pymat.pbr import PbrProperties # type: ignore[attr-defined] + except ImportError: + return None + + return Material( + name="Brushed Stainless Steel", + density=8.0, + formula="Fe", + mechanical={"youngs_modulus": 193, "yield_strength": 170}, + thermal={"melting_point": 1450, "thermal_conductivity": 15.1}, + pbr_source=PbrProperties.from_gpuopen("Stainless Steel Brushed"), + ) + + +def report(material: Material, label: str) -> dict: + """Print a summary of a Material and return its Three.js dict.""" + print(f"\n=== {label} ===") + print(f" name: {material.name}") + print(f" density: {material.density} g/cm³") + print(f" formula: {material.formula}") + print(f" molar mass: {material.molar_mass} g/mol") + print(f" pbr_source set: {material.pbr_source is not None}") + + three_js = material.to_three_js_material_dict() + print(" Three.js dict:") + print(json.dumps(three_js, indent=4, sort_keys=True)) + + # Sanity: whichever backend is active, it conforms to the Protocol. + source: PbrSource = ( + material.pbr_source if material.pbr_source is not None else material.properties.pbr + ) + assert isinstance(source, PbrSource), ( + f"Active PBR backend {type(source).__name__} does not conform to PbrSource" + ) + return three_js + + +def main() -> int: + lite_steel = build_steel_with_lite_pbr() + lite_dict = report(lite_steel, "Lite backend (zero extra deps)") + (OUTPUT_DIR / "steel_lite.json").write_text( + json.dumps(lite_dict, indent=2, sort_keys=True) + "\n" + ) + + rich_steel = build_steel_with_rich_pbr() + if rich_steel is not None: + rich_dict = report(rich_steel, "Rich backend (threejs-materials)") + (OUTPUT_DIR / "steel_rich.json").write_text( + json.dumps(rich_dict, indent=2, sort_keys=True) + "\n" + ) + else: + print( + "\n=== Rich backend skipped ===\n" + " Install `pip install py-materials[pbr]` to fetch MaterialX\n" + " materials from ambientcg / polyhaven / gpuopen / physicallybased.info." + ) + + print(f"\nJSON written to {OUTPUT_DIR.resolve()}") + return 0 + + +if __name__ == "__main__": + import sys + + sys.exit(main()) diff --git a/pyproject.toml b/pyproject.toml index c65abc5..e789b92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,11 @@ matproj = ["pymatgen>=2024.0.0"] # py-materials[build123d]` on Python 3.13+ is a no-op rather than a hard # wheel-resolution error — installers silently drop the dep. See #11. build123d = ["build123d>=0.7.0; python_version<'3.13'"] +# Full MaterialX PBR backend via Bernhard's threejs-materials library. +# Pure-Python, Apache-2.0, brings pillow + pygltflib + requests + the +# MaterialX SDK (via threejs-materials' own [materialx] extra) with it. +# Physics-only users never pull this. See ADR-0002. +pbr = ["threejs-materials[materialx]>=1.0.0"] dev = [ "pytest>=7.0.0", "pytest-cov>=4.0.0", @@ -49,6 +54,7 @@ all = [ "periodictable>=1.6.0", "pymatgen>=2024.0.0", "build123d>=0.7.0; python_version<'3.13'", + "threejs-materials>=1.0.0", ] [project.urls] diff --git a/src/pymat/core.py b/src/pymat/core.py index 24aaa67..016fca9 100644 --- a/src/pymat/core.py +++ b/src/pymat/core.py @@ -14,7 +14,7 @@ from typing import TYPE_CHECKING, Any, Dict, Optional, TypeVar if TYPE_CHECKING: - pass + from .pbr import PbrSource from .properties import AllProperties @@ -114,6 +114,13 @@ class _MaterialInternal: # Properties (all domains) properties: AllProperties = field(default_factory=AllProperties) + # Optional rich PBR backend. When set, takes precedence over + # `properties.pbr` (the lite native dataclass) for rendering. See + # ADR-0002 — this is the integration point for + # `threejs_materials.PbrProperties` and any other external PBR + # loader that conforms to the `pymat.pbr.PbrSource` Protocol. + pbr_source: Optional["PbrSource"] = field(default=None, repr=False) + # Hierarchy (not shown in repr) parent: Optional[_MaterialInternal] = field(default=None, repr=False) _children: Dict[str, _MaterialInternal] = field(default_factory=dict, repr=False) @@ -192,6 +199,91 @@ def __post_init__(self): ): self.properties.pbr.transmission = self.properties.optical.transparency / 100.0 + # When a rich pbr_source is provided, project its fields down + # onto the lite `properties.pbr` dataclass. This lets existing + # ocp_vscode / downstream renderers that only read + # `material.properties.pbr.` pick up the rich data + # without any changes on their side (graceful enhancement). + # `to_three_js_material_dict()` still prefers the full rich + # source for callers that can handle it. + if self.pbr_source is not None: + self._backfill_pbr_from_source() + + def _backfill_pbr_from_source(self) -> None: + """ + Populate the lite `properties.pbr` dataclass from the rich + `pbr_source`'s `to_dict()` output. + + Supports two `to_dict()` shapes, detected at runtime: + + - **Nested** (`threejs_materials.PbrProperties` format): + ``{id, name, source, url, license, values: {...}, textures: {...}}`` + — scalars under ``values``, maps under ``textures``. + - **Flat** (`pymat`'s own lite `PBRProperties.to_dict()` format): + ``{color, metalness, roughness, normalMap, ...}`` — Three.js + MeshPhysicalMaterial camelCase keys at the top level. + + The lite dataclass is a strict subset of the Three.js + MeshPhysicalMaterial spec — fields on the rich source that + don't have a corresponding lite field (sheen, anisotropy, + iridescence, dispersion, etc.) are dropped. Downstream + consumers that need full fidelity should read + `material.pbr_source` directly. + """ + if self.pbr_source is None: + return + try: + d = self.pbr_source.to_dict() + except Exception: + return + + # Detect shape: nested has `values` key, flat has top-level scalars. + if isinstance(d.get("values"), dict): + values = d["values"] + maps = d.get("textures") if isinstance(d.get("textures"), dict) else {} + else: + values = d + maps = {} + + lite = self.properties.pbr + + # Scalars + color = values.get("color") + if isinstance(color, (list, tuple)) and len(color) >= 3: + r, g, b = color[:3] + alpha = values.get( + "opacity", + lite.base_color[3] if len(lite.base_color) >= 4 else 1.0, + ) + lite.base_color = (r, g, b, alpha) + if "metalness" in values: + lite.metallic = values["metalness"] + if "roughness" in values: + lite.roughness = values["roughness"] + emissive = values.get("emissive") + if isinstance(emissive, (list, tuple)): + lite.emissive = tuple(emissive) + if "ior" in values: + lite.ior = values["ior"] + if "transmission" in values: + lite.transmission = values["transmission"] + if "clearcoat" in values: + lite.clearcoat = values["clearcoat"] + + # Texture maps. Nested (`textures`) keys are short names like + # `color`, `normal`, `roughness`, `metalness`, `ao`. Flat + # format uses Three.js camelCase: `map` (the color channel), + # `normalMap`, `roughnessMap`, `metalnessMap`, `aoMap`. + lite.base_color_map = maps.get("color") or values.get("map") or lite.base_color_map + lite.normal_map = maps.get("normal") or values.get("normalMap") or lite.normal_map + lite.roughness_map = ( + maps.get("roughness") or values.get("roughnessMap") or lite.roughness_map + ) + lite.metallic_map = maps.get("metalness") or values.get("metalnessMap") or lite.metallic_map + lite.ambient_occlusion_map = ( + maps.get("ao") or values.get("aoMap") or lite.ambient_occlusion_map + ) + # ========================================================================= # Chaining API # ========================================================================= @@ -450,6 +542,19 @@ def mass_from_volume_mm3(self, volume_mm3: float) -> float: """Calculate mass in grams from volume in mm³.""" return volume_mm3 * self.density_g_mm3 + def to_three_js_material_dict(self) -> dict: + """ + Return a Three.js ``MeshPhysicalMaterial`` dict for this material. + + Uses `pbr_source` if set (rich backend such as + `threejs_materials.PbrProperties` with full MaterialX + support), otherwise falls back to `properties.pbr` (the + lite native in-tree backend). See ADR-0002. + """ + if self.pbr_source is not None: + return self.pbr_source.to_dict() + return self.properties.pbr.to_dict() + def __repr__(self) -> str: """String representation showing path and density.""" density_str = f"ρ={self.density} g/cm³" if self.density else "ρ=?" @@ -502,6 +607,9 @@ class Material(_MaterialInternal): - electrical: Dict of electrical properties (resistivity, dielectric_constant, etc.) - optical: Dict of optical properties (refractive_index, transparency, etc.) - PHYSICS - pbr: Dict of PBR visualization properties (base_color, metallic, roughness) - RENDERING + - pbr_source: Optional rich PBR backend conforming to `pymat.pbr.PbrSource` + (typically `threejs_materials.PbrProperties` via the `[pbr]` extra). + Takes precedence over `pbr` for `to_three_js_material_dict()`. - manufacturing: Dict of manufacturing properties (machinability, weldability, etc.) - compliance: Dict of compliance properties (rohs_compliant, food_safe, etc.) - sourcing: Dict of sourcing properties (cost_per_kg, availability, etc.) @@ -529,6 +637,7 @@ def __init__( electrical: Optional[Dict[str, Any]] = None, optical: Optional[Dict[str, Any]] = None, pbr: Optional[Dict[str, Any]] = None, + pbr_source: Optional["PbrSource"] = None, manufacturing: Optional[Dict[str, Any]] = None, compliance: Optional[Dict[str, Any]] = None, sourcing: Optional[Dict[str, Any]] = None, @@ -551,6 +660,7 @@ def __init__( electrical=electrical, optical=optical, pbr=pbr, + pbr_source=pbr_source, manufacturing=manufacturing, compliance=compliance, sourcing=sourcing, diff --git a/src/pymat/pbr/__init__.py b/src/pymat/pbr/__init__.py new file mode 100644 index 0000000..6f5dbb9 --- /dev/null +++ b/src/pymat/pbr/__init__.py @@ -0,0 +1,34 @@ +""" +PBR (physically-based rendering) integration for `pymat.Material`. + +This package defines the `PbrSource` Protocol that `Material.pbr_source` +accepts. Any object conforming to the protocol can be assigned — the +native `PBRProperties` dataclass (lite, in-tree, no extra deps) and the +optional `threejs_materials.PbrProperties` (rich, full MaterialX +support) both satisfy it. + +Install the `[pbr]` extra to get the rich backend: + + pip install py-materials[pbr] + +Then `from pymat.pbr import PbrProperties` re-exports +`threejs_materials.PbrProperties`, the canonical PBR loader for +build123d / ocp_vscode. See ADR-0002 for the design rationale. +""" + +from __future__ import annotations + +from pymat.pbr._protocol import PbrSource + +__all__ = ["PbrSource"] + +# Optional re-export: threejs_materials.PbrProperties when the [pbr] +# extra is installed. Keeps `from pymat.pbr import PbrProperties` +# working as a thin alias, so downstream code doesn't need to know +# which underlying library provides the loader. +try: + from threejs_materials import PbrProperties # noqa: F401 + + __all__.append("PbrProperties") +except ImportError: # pragma: no cover + pass diff --git a/src/pymat/pbr/_protocol.py b/src/pymat/pbr/_protocol.py new file mode 100644 index 0000000..1ff9494 --- /dev/null +++ b/src/pymat/pbr/_protocol.py @@ -0,0 +1,34 @@ +"""The `PbrSource` typing Protocol.""" + +from __future__ import annotations + +from typing import Protocol, runtime_checkable + + +@runtime_checkable +class PbrSource(Protocol): + """A renderable PBR material source. + + Any object implementing `to_dict()` is a valid `PbrSource`. + The native `pymat.properties.PBRProperties` and the optional + `threejs_materials.PbrProperties` both conform. + + Consumers (ocp_vscode, build123d viewers, custom renderers) should + call `to_dict()` to get a Three.js `MeshPhysicalMaterial`- + shaped dict, agnostic of which backend is providing the data. This + keeps the rendering pipeline decoupled from how the material was + sourced (TOML authored / runtime downloaded / MaterialX baked). + + See ADR-0002 for the design rationale. + """ + + def to_dict(self) -> dict: + """Return a Three.js `MeshPhysicalMaterial` dict. + + Conforming implementations should use camelCase keys matching + Three.js's expected parameter names (`color`, `metalness`, + `roughness`, `transmission`, `opacity`, `transparent`, `emissive`, + `ior`, `clearcoat`, `normalMap`, etc.) and omit fields whose + value is the Three.js default. + """ + ... diff --git a/src/pymat/properties.py b/src/pymat/properties.py index 202b255..03fd8a9 100644 --- a/src/pymat/properties.py +++ b/src/pymat/properties.py @@ -324,11 +324,57 @@ class PBRProperties: clearcoat: float = 0.0 # secondary glossy layer # Texture maps (paths or URIs) + base_color_map: Optional[str] = None # albedo / diffuse texture normal_map: Optional[str] = None roughness_map: Optional[str] = None metallic_map: Optional[str] = None ambient_occlusion_map: Optional[str] = None + def to_dict(self) -> dict: + """ + Serialize to a Three.js ``MeshPhysicalMaterial`` dict. + + Makes this class conform to the `pymat.pbr.PbrSource` Protocol + so it can be assigned to `Material.pbr_source` as the + lightweight, in-tree, zero-dependency backend. For full + MaterialX / texture-library support, use + `threejs_materials.PbrProperties` instead (install the + `[pbr]` extra). + + Keys are Three.js camelCase. Defaults are omitted so the + result stays compact. + """ + out: dict = {} + r, g, b, a = self.base_color + out["color"] = [r, g, b] + if self.metallic != 0.0: + out["metalness"] = self.metallic + if self.roughness != 0.5: + out["roughness"] = self.roughness + if self.emissive != (0, 0, 0): + out["emissive"] = list(self.emissive) + if self.ior != 1.5: + out["ior"] = self.ior + if self.transmission > 0.0: + out["transmission"] = self.transmission + if self.clearcoat > 0.0: + out["clearcoat"] = self.clearcoat + if a < 1.0: + out["opacity"] = a + out["transparent"] = True + # Texture maps — keys use Three.js naming (normalMap, etc.). + if self.base_color_map is not None: + out["map"] = self.base_color_map # Three.js color channel is plain `map` + if self.normal_map is not None: + out["normalMap"] = self.normal_map + if self.roughness_map is not None: + out["roughnessMap"] = self.roughness_map + if self.metallic_map is not None: + out["metalnessMap"] = self.metallic_map + if self.ambient_occlusion_map is not None: + out["aoMap"] = self.ambient_occlusion_map + return out + @dataclass class ManufacturingProperties: diff --git a/tests/test_pbr.py b/tests/test_pbr.py new file mode 100644 index 0000000..8ccd5c8 --- /dev/null +++ b/tests/test_pbr.py @@ -0,0 +1,274 @@ +""" +Tests for `pymat.pbr` — PbrSource Protocol + native backend serializer. + +Covers the lite in-tree path (PBRProperties.to_dict) and the +Material.to_three_js_material_dict dispatch. The rich threejs-materials +backend is exercised via a duck-typed stub to avoid pulling the extra +into the base test dependency set. See ADR-0002. +""" + +from __future__ import annotations + +import pytest + +from pymat import Material +from pymat.pbr import PbrSource +from pymat.properties import PBRProperties + + +class TestPbrSourceProtocol: + """Native `PBRProperties` should satisfy the `PbrSource` Protocol.""" + + def test_native_pbr_properties_is_pbr_source(self): + pbr = PBRProperties() + assert isinstance(pbr, PbrSource) + + def test_native_minimal_serialization(self): + """Default PBRProperties emits only `color` (all other fields + are at Three.js defaults and get omitted).""" + pbr = PBRProperties() + out = pbr.to_dict() + assert out == {"color": [0.8, 0.8, 0.8]} + + def test_native_full_serialization(self): + pbr = PBRProperties( + base_color=(0.7, 0.2, 0.2, 0.8), + metallic=1.0, + roughness=0.25, + transmission=0.3, + clearcoat=0.5, + normal_map="/path/to/normal.png", + ) + out = pbr.to_dict() + assert out["color"] == [0.7, 0.2, 0.2] + assert out["metalness"] == 1.0 + assert out["roughness"] == 0.25 + assert out["transmission"] == 0.3 + assert out["clearcoat"] == 0.5 + assert out["opacity"] == pytest.approx(0.8) + assert out["transparent"] is True + assert out["normalMap"] == "/path/to/normal.png" + + +class TestMaterialToThreeJs: + """`Material.to_three_js_material_dict()` picks the right backend.""" + + def test_falls_back_to_native_pbr(self): + steel = Material( + name="Steel", + density=7.85, + pbr={"base_color": (0.6, 0.6, 0.65, 1.0), "metallic": 1.0}, + ) + out = steel.to_three_js_material_dict() + assert out["color"] == [0.6, 0.6, 0.65] + assert out["metalness"] == 1.0 + + def test_pbr_source_takes_precedence(self): + """When `pbr_source` is set, the native `properties.pbr` is ignored.""" + + class FakeRichBackend: + """Stub conforming to PbrSource Protocol.""" + + def to_dict(self) -> dict: + return { + "color": [0.91, 0.91, 0.88], + "metalness": 1.0, + "roughness": 0.05, + "normalMap": "textures/brushed_steel_normal.png", + } + + steel = Material( + name="Brushed Steel", + density=7.85, + pbr={"base_color": (0.3, 0.3, 0.3, 1.0)}, # would lose if used + pbr_source=FakeRichBackend(), + ) + out = steel.to_three_js_material_dict() + # Rich backend's output, not the lite pbr dict. + assert out["color"] == [0.91, 0.91, 0.88] + assert out["normalMap"] == "textures/brushed_steel_normal.png" + + def test_pbr_source_stub_is_pbr_source(self): + """Any class with `to_dict()` satisfies the Protocol.""" + + class Stub: + def to_dict(self): + return {} + + assert isinstance(Stub(), PbrSource) + + def test_non_conforming_object_is_not_pbr_source(self): + class NotPbr: + pass + + assert not isinstance(NotPbr(), PbrSource) + + +class TestPbrBackfill: + """ + When `pbr_source` is set, the rich backend's `to_dict()` + output is projected onto the lite `properties.pbr` dataclass. This + lets existing ocp_vscode / downstream renderers that only read + `material.properties.pbr.` pick up the rich data without + needing adapter changes on their side. See ADR-0002 + the session + discussion on MorePET/mat#3. + """ + + def _make_rich_backend(self): + """Flat Three.js-shaped dict (matches the native lite backend's + output shape).""" + + class RichBackend: + def to_dict(self) -> dict: + return { + "color": [0.91, 0.91, 0.88], + "metalness": 1.0, + "roughness": 0.08, + "ior": 2.5, + "transmission": 0.0, + "clearcoat": 0.2, + "emissive": [0.01, 0.01, 0.01], + "map": "cache/albedo.png", + "normalMap": "cache/normal.png", + "roughnessMap": "cache/roughness.png", + "metalnessMap": "cache/metalness.png", + "aoMap": "cache/ao.png", + } + + return RichBackend() + + def _make_nested_rich_backend(self): + """Nested `{values, textures}` dict shape — what + `threejs_materials.PbrProperties.to_dict()` actually emits + (verified against the real library at v1.0.4).""" + + class NestedBackend: + def to_dict(self) -> dict: + return { + "id": "ivory_walnut", + "name": "Ivory Walnut Solid Wood", + "source": "gpuopen", + "url": "https://matlib.gpuopen.com/...", + "license": "MIT Public Domain", + "values": { + "color": [0.8, 0.8, 0.8], # neutral tint, map carries visual + "metalness": 0.0, + "roughness": 1.0, + "ior": 1.5, + }, + "textures": { + "color": "data:image/png;base64,AAAA", # albedo + "normal": "data:image/png;base64,BBBB", + "roughness": "data:image/png;base64,CCCC", + }, + } + + return NestedBackend() + + def test_backfill_populates_lite_scalars(self): + steel = Material( + name="Brushed Steel", + density=7.85, + pbr_source=self._make_rich_backend(), + ) + lite = steel.properties.pbr + assert lite.base_color[:3] == (0.91, 0.91, 0.88) + assert lite.metallic == 1.0 + assert lite.roughness == 0.08 + assert lite.ior == 2.5 + assert lite.clearcoat == 0.2 + assert lite.emissive == (0.01, 0.01, 0.01) + + def test_backfill_populates_texture_maps(self): + steel = Material( + name="Brushed Steel", + density=7.85, + pbr_source=self._make_rich_backend(), + ) + lite = steel.properties.pbr + assert lite.base_color_map == "cache/albedo.png" + assert lite.normal_map == "cache/normal.png" + assert lite.roughness_map == "cache/roughness.png" + assert lite.metallic_map == "cache/metalness.png" + assert lite.ambient_occlusion_map == "cache/ao.png" + + def test_backfill_handles_nested_threejs_shape(self): + """ + `threejs_materials.PbrProperties.to_dict()` emits a nested + `{id, name, source, url, license, values: {...}, textures: {...}}` + dict, NOT a flat Three.js MeshPhysicalMaterial dict. The + backfill must read scalars from `values` and texture paths + from `textures`. See ADR-0002 + the live verification on + the build123d fork (gerchowl:feature/pymat-material-integration). + """ + wood = Material( + name="Walnut", + density=0.65, + pbr_source=self._make_nested_rich_backend(), + ) + lite = wood.properties.pbr + # Scalars from `values`. + assert lite.base_color[:3] == (0.8, 0.8, 0.8) + assert lite.metallic == 0.0 + assert lite.roughness == 1.0 + assert lite.ior == 1.5 + # Texture maps from `textures` (short names: color, normal, ...). + assert lite.base_color_map == "data:image/png;base64,AAAA" + assert lite.normal_map == "data:image/png;base64,BBBB" + assert lite.roughness_map == "data:image/png;base64,CCCC" + + def test_backfill_existing_adapter_compat(self): + """ + Simulate Bernhard's `_extract_materials_from_node` adapter: + read from `material.properties.pbr.` as he does today, + verify the rich-backend data makes it through. + """ + steel = Material( + name="Brushed Steel", + density=7.85, + pbr_source=self._make_rich_backend(), + ) + pbr = steel.properties.pbr + simulated_extraction = { + "color": pbr.base_color, + "metalness": pbr.metallic, + "roughness": pbr.roughness, + "normal_map": pbr.normal_map, + "roughness_map": pbr.roughness_map, + "metalness_map": pbr.metallic_map, + "ao_map": pbr.ambient_occlusion_map, + } + # Every field the adapter reads should carry the rich backend value. + assert simulated_extraction["color"][:3] == (0.91, 0.91, 0.88) + assert simulated_extraction["metalness"] == 1.0 + assert simulated_extraction["roughness"] == 0.08 + assert simulated_extraction["normal_map"] == "cache/normal.png" + assert simulated_extraction["metalness_map"] == "cache/metalness.png" + + def test_backfill_noop_when_no_source(self): + """With no pbr_source, lite dataclass keeps its authored values.""" + steel = Material( + name="Steel", + density=7.85, + pbr={"base_color": (0.5, 0.5, 0.5, 1.0), "metallic": 0.3}, + ) + assert steel.properties.pbr.base_color == (0.5, 0.5, 0.5, 1.0) + assert steel.properties.pbr.metallic == 0.3 + # Roughness stays at its dataclass default, not overridden. + assert steel.properties.pbr.roughness == 0.5 + + def test_rich_still_takes_precedence_in_dispatch(self): + """ + Even with backfill, `Material.to_three_js_material_dict()` still + prefers the rich `pbr_source` (full fidelity for callers that + can handle extra fields). + """ + rich = self._make_rich_backend() + steel = Material( + name="Brushed Steel", + density=7.85, + pbr_source=rich, + ) + out = steel.to_three_js_material_dict() + # Same object as rich's output, not re-serialized from lite. + assert out == rich.to_dict() diff --git a/uv.lock b/uv.lock index c247bcd..d81b16b 100644 --- a/uv.lock +++ b/uv.lock @@ -44,24 +44,24 @@ name = "build123d" version = "0.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anytree" }, - { name = "cadquery-ocp" }, - { name = "ezdxf" }, + { name = "anytree", marker = "python_full_version < '3.14'" }, + { name = "cadquery-ocp", marker = "python_full_version < '3.14'" }, + { name = "ezdxf", marker = "python_full_version < '3.14'" }, { name = "ipython", version = "8.39.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "ipython", version = "9.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "ipython", version = "9.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "lib3mf" }, + { name = "ipython", version = "9.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "lib3mf", marker = "python_full_version < '3.14'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "ocp-gordon" }, - { name = "ocpsvg" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.14'" }, + { name = "ocp-gordon", marker = "python_full_version < '3.14'" }, + { name = "ocpsvg", marker = "python_full_version < '3.14'" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "svgpathtools" }, - { name = "sympy" }, - { name = "trianglesolver" }, - { name = "typing-extensions" }, - { name = "webcolors" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.14'" }, + { name = "svgpathtools", marker = "python_full_version < '3.14'" }, + { name = "sympy", marker = "python_full_version < '3.14'" }, + { name = "trianglesolver", marker = "python_full_version < '3.14'" }, + { name = "typing-extensions", marker = "python_full_version < '3.14'" }, + { name = "webcolors", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7e/00/b56ebe1d2a31611dbfcc315d22ebb5403eddc60027d5f82acd493ebb5ac1/build123d-0.10.0.tar.gz", hash = "sha256:73ded38ddca8ebb95e7dd078ac3d7aacc8ca42fce8f1d176f1040e35fba4f608", size = 20011921, upload-time = "2025-11-05T22:04:37.054Z" } wheels = [ @@ -73,7 +73,7 @@ name = "cadquery-ocp" version = "7.8.1.1.post1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "vtk" }, + { name = "vtk", marker = "python_full_version < '3.14'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/61/a6/760c6383f8e0cac562583feab0d4733876522ee265f2cfa3a26bdffef330/cadquery_ocp-7.8.1.1.post1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4413961e98a90686a56c2ac58b126773c7da4eb82b967ddcc1f394fe6a7b71ad", size = 68085209, upload-time = "2025-01-29T14:33:03.08Z" }, @@ -497,6 +497,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, ] +[[package]] +name = "dataclasses-json" +version = "0.6.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "marshmallow" }, + { name = "typing-inspect" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227, upload-time = "2024-06-09T16:20:19.103Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" }, +] + [[package]] name = "decorator" version = "5.2.1" @@ -506,6 +519,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, ] +[[package]] +name = "deprecated" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, +] + [[package]] name = "exceptiongroup" version = "1.3.1" @@ -532,11 +557,11 @@ name = "ezdxf" version = "1.4.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "fonttools" }, + { name = "fonttools", marker = "python_full_version < '3.14'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "pyparsing" }, - { name = "typing-extensions" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.14'" }, + { name = "pyparsing", marker = "python_full_version < '3.14'" }, + { name = "typing-extensions", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6d/ff/e2fea17633a4c04abdf260d53e0d67463b01e11d957b8faaf3b195666e10/ezdxf-1.4.3.tar.gz", hash = "sha256:403adf7ce305877f6c9f3c007fe2e5c5df504dfb797032122abedd7170176764", size = 1816226, upload-time = "2025-10-19T03:48:12.137Z" } wheels = [ @@ -735,16 +760,16 @@ resolution-markers = [ "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'win32'", ] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version >= '3.12'" }, - { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.12'" }, - { name = "jedi", marker = "python_full_version >= '3.12'" }, - { name = "matplotlib-inline", marker = "python_full_version >= '3.12'" }, - { name = "pexpect", marker = "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version >= '3.12'" }, - { name = "pygments", marker = "python_full_version >= '3.12'" }, - { name = "stack-data", marker = "python_full_version >= '3.12'" }, - { name = "traitlets", marker = "python_full_version >= '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "jedi", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "pexpect", marker = "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "pygments", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "stack-data", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, + { name = "traitlets", marker = "python_full_version >= '3.12' and python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/86/28/a4698eda5a8928a45d6b693578b135b753e14fa1c2b36ee9441e69a45576/ipython-9.11.0.tar.gz", hash = "sha256:2a94bc4406b22ecc7e4cb95b98450f3ea493a76bec8896cda11b78d7752a6667", size = 4427354, upload-time = "2026-03-05T08:57:30.549Z" } wheels = [ @@ -756,7 +781,7 @@ name = "ipython-pygments-lexers" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pygments", marker = "python_full_version >= '3.11'" }, + { name = "pygments", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } wheels = [ @@ -768,7 +793,7 @@ name = "jedi" version = "0.19.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "parso" }, + { name = "parso", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } wheels = [ @@ -918,6 +943,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a9/b3/7a844247dfa1f0b24de6fa0425afb808dd1feeef2d05d6335170433c1071/lib3mf-2.5.0-py3-none-win_amd64.whl", hash = "sha256:e378470855d634708c6ae7f2f2c6fcd89e4996350de86020849e7b2d51ffe6ac", size = 857560, upload-time = "2026-02-23T21:17:39.431Z" }, ] +[[package]] +name = "marshmallow" +version = "3.26.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/79/de6c16cc902f4fc372236926b0ce2ab7845268dcc30fb2fbb7f71b418631/marshmallow-3.26.2.tar.gz", hash = "sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57", size = 222095, upload-time = "2025-12-22T06:53:53.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/2f/5108cb3ee4ba6501748c4908b908e55f42a5b66245b4cfe0c99326e1ef6e/marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73", size = 50964, upload-time = "2025-12-22T06:53:51.801Z" }, +] + +[[package]] +name = "materialx" +version = "1.39.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/f5/0d20693da8866c95eac48567df8f6da9bb89eefcfaf164b16ebd00b73430/materialx-1.39.4.tar.gz", hash = "sha256:652126537195beac2c63beff7891cb2a0f6b6340ff297831e851154961ec1943", size = 1579442, upload-time = "2025-09-15T19:12:14.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/e6/884ec686f84a1544b51b1cfa4426d4bf0505f2dab6b68538caf61399c590/materialx-1.39.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:96dad106174630079c85d52b2aff7ec532904fb1b9969d770ee527ea74a9c57c", size = 4844030, upload-time = "2025-09-15T19:11:43.377Z" }, + { url = "https://files.pythonhosted.org/packages/eb/bd/ef77d87f86710b8666effe712ee2d9f47d7040f595d277db7634262ca90b/materialx-1.39.4-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:3822e33cf83d89e451c6f818d50eb7736c56a8e14bf8cf70d05a2ba129976012", size = 9157924, upload-time = "2025-09-15T19:11:45.934Z" }, + { url = "https://files.pythonhosted.org/packages/53/c2/3904da5186887143b27d42cb9170a9ab11b9f6c78abd964ba80c4da1c1a9/materialx-1.39.4-cp310-cp310-win_amd64.whl", hash = "sha256:5a72babf94361b5f67e68a73e7e086217b6562011830e2fcef089e7d7caea2c8", size = 4890717, upload-time = "2025-09-15T19:11:48.26Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a1/1afe4f790f0dd0888014b710d48e02f4a4e2ff292f3fd21016d8494e22a7/materialx-1.39.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:80a15166826033287895d950e48a05702ba7d607623fb351bfa0ac6f4e8db3f5", size = 4857065, upload-time = "2025-09-15T19:11:50.385Z" }, + { url = "https://files.pythonhosted.org/packages/97/86/4cdfd1e87856022da422301f6128d3285224bc9ec3a7539e9943b676ac2e/materialx-1.39.4-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:0eadce374157a2079c2f6b86caae41a87981228f269e9468003a8e860ee514d4", size = 9174096, upload-time = "2025-09-15T19:11:52.417Z" }, + { url = "https://files.pythonhosted.org/packages/1d/86/fb0c0f5c7a867a4d4513660a0e01995219a1c817e219339412fd310303c8/materialx-1.39.4-cp311-cp311-win_amd64.whl", hash = "sha256:139670a2e00f70d9e1609cbdc5164be2f2553e02ef472e70e62f37b44ea3df1f", size = 4899166, upload-time = "2025-09-15T19:11:54.539Z" }, + { url = "https://files.pythonhosted.org/packages/12/4c/b06dfe28bcf1f2876581b450394faf13ec7d57a504443b13dacb5ddc708a/materialx-1.39.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:128bad7be5d816175b57d21afb59c16c550749aca5e352af1a31a5984478982b", size = 4856825, upload-time = "2025-09-15T19:11:56.443Z" }, + { url = "https://files.pythonhosted.org/packages/45/d0/bade303a54b498ac644baf5895b57247cab3810776be2a8e98fa79081923/materialx-1.39.4-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0249deb21ca499910297a24ab317d7c70451fadcea4b080c63fa9879739207ed", size = 9164496, upload-time = "2025-09-15T19:11:58.303Z" }, + { url = "https://files.pythonhosted.org/packages/e1/55/4204ec0a25678d71736b8015bff9593b6300ee832d3a572663a8403818fa/materialx-1.39.4-cp312-cp312-win_amd64.whl", hash = "sha256:889fb004f804ba65dff5d3087f3d48541048548dc39c832f1014592e1044c9fa", size = 4911350, upload-time = "2025-09-15T19:12:00.782Z" }, + { url = "https://files.pythonhosted.org/packages/f3/11/452712dbe124e2d9ae2cfc49a400646f5b9841b2e106b3825d451c0678a3/materialx-1.39.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:91d3f66ff2b65d7d8945f6ce086551985b755753d0eb8b3fa95a9072d1155e89", size = 4857623, upload-time = "2025-09-15T19:12:02.539Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6f/799b4ab8b5dc115203c28dd293bad47d201673d667c06c99ec0757b5b945/materialx-1.39.4-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:77306def1c4039f77b2ebdecfda7bce8203a17a2eb6d9b471abe2ddd3573f0f5", size = 9165503, upload-time = "2025-09-15T19:12:04.645Z" }, + { url = "https://files.pythonhosted.org/packages/2e/7b/1a16fac0a11cbb3aeb81841ddef67cabd5d4c0070218a284153384dc5ebc/materialx-1.39.4-cp313-cp313-win_amd64.whl", hash = "sha256:b52b7bb85704dfbc6f76ddce71421a0bf124782b70d9a39acce6973bcf91d056", size = 4911723, upload-time = "2025-09-15T19:12:07.631Z" }, +] + [[package]] name = "matplotlib" version = "3.10.8" @@ -998,7 +1055,7 @@ name = "matplotlib-inline" version = "0.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "traitlets" }, + { name = "traitlets", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } wheels = [ @@ -1052,6 +1109,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, ] +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + [[package]] name = "narwhals" version = "2.19.0" @@ -1249,11 +1315,11 @@ name = "ocp-gordon" version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cadquery-ocp-proxy" }, + { name = "cadquery-ocp-proxy", marker = "python_full_version < '3.14'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.14'" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/72/ed/3d8955df5d412bd6e711e7c0673d6420efe6ce3a106f0dbc50fc5cc82b3e/ocp_gordon-0.2.0.tar.gz", hash = "sha256:3ce1f1fb589e891534d0c1fd7553a518733336308837c4b5e5164d77df1cf5c9", size = 118840, upload-time = "2026-01-09T00:33:56.107Z" } wheels = [ @@ -1265,14 +1331,58 @@ name = "ocpsvg" version = "0.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cadquery-ocp" }, - { name = "svgelements" }, + { name = "cadquery-ocp", marker = "python_full_version < '3.14'" }, + { name = "svgelements", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/af/e2/3ba2ad53395f91c72070cb3b8b033c5d9f9ba2ae6f61e869ef2a5f7f8ade/ocpsvg-0.5.0.tar.gz", hash = "sha256:5cd8dbec8bf590d373a82aaebeab241838185aab04ee2859f33b9d7956bbfba6", size = 54195, upload-time = "2025-02-21T15:54:12.333Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/e1/7b/0cf408c8c2bdf10685a284253e0004e6c672a9dbd23070d0889f4b54b284/ocpsvg-0.5.0-py3-none-any.whl", hash = "sha256:68cafdc3d681a1707530360baf2d51cfd58414b7d439f42eafbd31e842cf295e", size = 20789, upload-time = "2025-02-21T15:54:10.405Z" }, ] +[[package]] +name = "openexr" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/c7/d6bb16bd2cd2d9c1e7a76790801f3794648a31163f275cebd7b2022dc896/openexr-3.4.9.tar.gz", hash = "sha256:9262e186472f16b489c05467d075baf47de3ff408cce0227fc02f410da704e5c", size = 25582278, upload-time = "2026-04-03T22:41:05.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/7e/4ae2315a4b997bbc10c87147011fd6c6b6b6cd3ead7738ab85f3455f56a8/openexr-3.4.9-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:41eea246d607509809055ceaa8d43e260161f8a7c8b022bc1774025e64bdee82", size = 2145465, upload-time = "2026-04-03T22:39:42.754Z" }, + { url = "https://files.pythonhosted.org/packages/c8/bb/efddad81e728e88550340976975932ed04b24202dc463eb73d0457295b78/openexr-3.4.9-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:1589d4cef0efa7ea10662b8e85e3cde9b245f8cdd12a0acca3fcabffeae1e4d3", size = 1138519, upload-time = "2026-04-03T22:39:44.693Z" }, + { url = "https://files.pythonhosted.org/packages/03/b9/785217a6da61ebd4216fc025122e3c54147aee81117bab2c64add1cd1849/openexr-3.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:56d765598f9350c2b25a1726dd790528429f31b968d11e530bf6b27498c21683", size = 1010774, upload-time = "2026-04-03T22:39:46.185Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e3/2e087f01faba46b9fc0a8a7f4dc40973dd6b82cada614bc8336ef122817a/openexr-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f8107f4cdcb400305547c8e057a3b419f11752a16936d9a99e7d4c0cf125593", size = 1161618, upload-time = "2026-04-03T22:39:47.984Z" }, + { url = "https://files.pythonhosted.org/packages/72/42/a5b87ff7a1e5ba3c1bda4c8eb8c8b199e7e7f9cafbeb478d0928571b2873/openexr-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9b5a27e0acdb5e9ade0f161435d1362732f9fd4acf066622a993d11491359761", size = 1291867, upload-time = "2026-04-03T22:39:49.593Z" }, + { url = "https://files.pythonhosted.org/packages/77/a6/4ed19c2305f959e7dc5d46a024bf3db44d316e63c27e166387a3fa1afc09/openexr-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:032fdb8f78874b4fec21078a741acc8f414995c96296cd19a191ee6c4a05a857", size = 2149596, upload-time = "2026-04-03T22:39:51.275Z" }, + { url = "https://files.pythonhosted.org/packages/4f/fb/c65602dc78c8f57bb0558af40546a4f905862e32a1735011b38126dd859f/openexr-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b54936434965a3ad6ed33d833644f42ef63d4f59fd117e217b8db8869f80560c", size = 2329797, upload-time = "2026-04-03T22:39:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/9d/02/c4eea1963964a557693c27fd3e51eab369ca80f0783619b238b95c5756ee/openexr-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:bc9a960f37660324246da8a4137a67d457cbc068a16460e707a48a772539469b", size = 732875, upload-time = "2026-04-03T22:39:54.784Z" }, + { url = "https://files.pythonhosted.org/packages/34/38/df33dc3ec7a22fe94bd927137cbedd2800ecb5c829f003258a78c8a81ea9/openexr-3.4.9-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:0ee041a1b70ade86d54c83d168e51e72353d5e857b90e117deab8080f055d196", size = 2148493, upload-time = "2026-04-03T22:39:56.284Z" }, + { url = "https://files.pythonhosted.org/packages/35/f2/3ed6b9c3e742c6200d8d5ee83ad9eea8035fe653794b0d0494ca50b73c70/openexr-3.4.9-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:a195d198f0c80bd5234f37315d86df5ee51753b3ea17ff43e9723640232a5445", size = 1139970, upload-time = "2026-04-03T22:39:58.199Z" }, + { url = "https://files.pythonhosted.org/packages/79/a9/84e764804f84450ed313305c6c1137f4046a25d6759f20da050ebf523f56/openexr-3.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:426398d8772e3a49c7d932bdf4aef5894d31b391bac3f015bd19345ef0a5f660", size = 1011970, upload-time = "2026-04-03T22:39:59.725Z" }, + { url = "https://files.pythonhosted.org/packages/8b/07/683568856389acca16972ab8190dcc46e736b9ffec035b069c6174494ee0/openexr-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fac9fd4e165461ac0138de48cf682f3730f3e35c76747d21aa85b3ebfda802bd", size = 1162879, upload-time = "2026-04-03T22:40:01.244Z" }, + { url = "https://files.pythonhosted.org/packages/2b/5a/d84c31ecf12e5f37917ac43bbe0ac982cb8593c573bc0548a35b13f96bba/openexr-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:865df41542a1cad1389849c1640ac592571764359f1d089bfe57dfc3709430fb", size = 1292944, upload-time = "2026-04-03T22:40:02.743Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/1b4d9f8949dd188980ea9af12f857496775260e00cab1965f5ca0ef186e4/openexr-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:345b346d4115590160998bbb6399b89906d3753a4cdaf52c16b381bda3f276d0", size = 2150454, upload-time = "2026-04-03T22:40:04.191Z" }, + { url = "https://files.pythonhosted.org/packages/31/de/6a1e616e351d424831f7d1804e1f79595a70f8795ff63487dc81978b8316/openexr-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d7f5a44585daccae4e2787b2f7af59ac00838764f09a0a1ad5cd3451b520f76d", size = 2330749, upload-time = "2026-04-03T22:40:06.174Z" }, + { url = "https://files.pythonhosted.org/packages/c6/95/840b48252cf28b3430187581facc399154ace45b4a13ca4372ed0ee91347/openexr-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:f7fae5fe9164f6153c3ca3235440bba38b0c1f01a9fc5e86c63b2c76647f0678", size = 733537, upload-time = "2026-04-03T22:40:08.102Z" }, + { url = "https://files.pythonhosted.org/packages/99/f5/724ffb39cc4bec7f8c205b041c0eab4fce219e4ca14c641324f2d32b35ed/openexr-3.4.9-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:3a590eec7d0088e7d15cb307f0b4cb04cdb11e3a6a455818cd2f00fe63a38286", size = 2152583, upload-time = "2026-04-03T22:40:09.601Z" }, + { url = "https://files.pythonhosted.org/packages/98/d2/e04d82d4e816e285de7f29e9e2496946d671aa8f8c640fd3cf681c799af0/openexr-3.4.9-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:fcbb5074a4f239f6d5c147671c09840cd06762aa1253c6a55ea5be2b1de13956", size = 1143106, upload-time = "2026-04-03T22:40:11.215Z" }, + { url = "https://files.pythonhosted.org/packages/46/be/e86167c1588f3738b58728e1d4d33140f5dc168bf4ad5dd61bec457c54c3/openexr-3.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b0eb5bfdb076805f09d93cc1264aff62345b0174c8ca0be6be2e90b6788049d", size = 1017974, upload-time = "2026-04-03T22:40:13.127Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a6/1247078abf55c86056b3be1dee59c05b2b1c5abcedc2b353343fa8a99098/openexr-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9e7144b64e663abd441f96e2b3f1bcd5774d982d3eaab705f6dc6180361e3e01", size = 1162574, upload-time = "2026-04-03T22:40:14.52Z" }, + { url = "https://files.pythonhosted.org/packages/91/b6/d00a899567c779413d00e7fa60dc252075c8f645342802ffec3888d571c9/openexr-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee08fb7e386e4f41f15b628479d20eeaf9b7145af6c57cfd1d20149c1cd93aef", size = 1292780, upload-time = "2026-04-03T22:40:16.481Z" }, + { url = "https://files.pythonhosted.org/packages/87/52/4911f0e6f9b1e5a964ca8e82f3b108bd1b8c004fbb9a26e06db18a7a45c8/openexr-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f1e7f1e66d727278670903d83e9b432b593412969e741ecd6ba29895b4289944", size = 2151835, upload-time = "2026-04-03T22:40:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b0/6cc3cf1aae0361ffe394b683852b7029a794854183a5176dec2468f34729/openexr-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:93c52c495b39fec399a820d717073bde0a8e589b05eb68456993ad9199f418b1", size = 2333724, upload-time = "2026-04-03T22:40:20.422Z" }, + { url = "https://files.pythonhosted.org/packages/80/7e/4a86426c7036b5309b6262c7193f48261bc8d6aeac1bd7274b9e143dda16/openexr-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:392d90282cfd3185302455562434b5e627a0cf01f645892691712c6f576b8beb", size = 735151, upload-time = "2026-04-03T22:40:22.303Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f7/5d4403fd5562f91c02f76f852a707852a256436f2cf9c36246ca3c6edf51/openexr-3.4.9-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:639f612f6a7007dda6b87ad161223531f0006b231a2d07ef6d0c18ef8d877eef", size = 2152506, upload-time = "2026-04-03T22:40:24.084Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f9/a46113ce35579f1cbe910b43be9f53dea658369edf8d5cbb2bf30e95db2b/openexr-3.4.9-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:495f0b0cef6850d3810bb615f2363ee483809fc068f39d77d15106f76f9f24cb", size = 1142985, upload-time = "2026-04-03T22:40:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/33/95/be6cc1f595ff48b5750d3d387f58a13e0cfa8d6ba198b1a2188c71a96b9b/openexr-3.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7b59d5b29101a8ee59a183d2751dd99b8a6278c3dba2be3fef530ec2ddad02fd", size = 1017957, upload-time = "2026-04-03T22:40:27.464Z" }, + { url = "https://files.pythonhosted.org/packages/9a/96/f3abc015cced773df5ca003493591cb7330b6ad53207ab96126105d73c9d/openexr-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6f73e3d7ac89713008bf02356c9138e43b90a94b94c15d966a843285177b45bc", size = 1162861, upload-time = "2026-04-03T22:40:29.317Z" }, + { url = "https://files.pythonhosted.org/packages/4a/be/03728ca8e1a8eaa8bed27c72aab93de7d85eadf8b29e60b9eeb1482c854d/openexr-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5150ac0ddb328b49cfdbabc66cb2a2bd136bf45022f2de959461e4699dfb8a87", size = 1292825, upload-time = "2026-04-03T22:40:30.855Z" }, + { url = "https://files.pythonhosted.org/packages/f9/49/1eee081a599434b40803f794301fdebe4ff8554a7e52c3d15ae9d179f28c/openexr-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8c1025479b71b4641ef872b2e0086c53a882bbb5bf58a0890760318d67a0802", size = 2151693, upload-time = "2026-04-03T22:40:32.609Z" }, + { url = "https://files.pythonhosted.org/packages/9f/91/4b85a65d4a1bd45023a1125a4b20d48171b14290a13da40e1d5f6d08d6d4/openexr-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:483d2ae18c6b7a4a7e148cc627a066dfdebc03bd80e115d75f403cffcafab3fe", size = 2333559, upload-time = "2026-04-03T22:40:34.472Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f9/04dfadf8eec3a1acb3aa4523c31273eec8cde74a7947929ff591f09e7e20/openexr-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:70f652c517fe38bcccd5a1146266e33e613c735e8cf6013659c0494bbe1485e2", size = 735105, upload-time = "2026-04-03T22:40:35.964Z" }, +] + [[package]] name = "orjson" version = "3.11.8" @@ -1532,7 +1642,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess", marker = "python_full_version >= '3.11' or sys_platform != 'win32'" }, + { name = "ptyprocess", marker = "python_full_version < '3.14' and sys_platform != 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -1715,7 +1825,7 @@ name = "prompt-toolkit" version = "3.0.52" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "wcwidth" }, + { name = "wcwidth", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } wheels = [ @@ -1742,7 +1852,7 @@ wheels = [ [[package]] name = "py-materials" -version = "2.0.5" +version = "2.1.0" source = { editable = "." } dependencies = [ { name = "pint", version = "0.24.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -1756,6 +1866,7 @@ all = [ { name = "periodictable" }, { name = "pymatgen", version = "2025.10.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pymatgen", version = "2026.3.23", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "threejs-materials" }, ] build123d = [ { name = "build123d", marker = "python_full_version < '3.13'" }, @@ -1769,6 +1880,9 @@ matproj = [ { name = "pymatgen", version = "2025.10.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pymatgen", version = "2026.3.23", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] +pbr = [ + { name = "threejs-materials", extra = ["materialx"] }, +] periodictable = [ { name = "periodictable" }, ] @@ -1790,13 +1904,28 @@ requires-dist = [ { name = "pymatgen", marker = "extra == 'matproj'", specifier = ">=2024.0.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0.0" }, + { name = "threejs-materials", marker = "extra == 'all'", specifier = ">=1.0.0" }, + { name = "threejs-materials", extras = ["materialx"], marker = "extra == 'pbr'", specifier = ">=1.0.0" }, { name = "tomli", marker = "python_full_version == '3.10.*'", specifier = ">=1.0.0" }, ] -provides-extras = ["periodictable", "matproj", "build123d", "dev", "all"] +provides-extras = ["periodictable", "matproj", "build123d", "pbr", "dev", "all"] [package.metadata.requires-dev] dev = [{ name = "ruff", specifier = ">=0.15.10" }] +[[package]] +name = "pygltflib" +version = "1.16.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dataclasses-json" }, + { name = "deprecated" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/e8/f8232abdf9c085333689b0a428dcd1d0f83edd1ecafa6ed878a633d8c9d5/pygltflib-1.16.5.tar.gz", hash = "sha256:1f15740d5a7aaf71a5083e285af6b361184958e255659132f4ba8fe4f3d21ea9", size = 43272, upload-time = "2025-07-24T06:35:38.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/d6/7eb8a0e4eb30add2b76c957a41107a5f2ba26472d656e2733728bec0476b/pygltflib-1.16.5-py3-none-any.whl", hash = "sha256:41d3349c59dcf1586faeaee29c967be07ac2bf7cecdb8ae2b527da1f25afdaac", size = 27557, upload-time = "2025-07-24T06:35:37.328Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -2234,9 +2363,9 @@ name = "stack-data" version = "0.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "asttokens" }, - { name = "executing" }, - { name = "pure-eval" }, + { name = "asttokens", marker = "python_full_version < '3.14'" }, + { name = "executing", marker = "python_full_version < '3.14'" }, + { name = "pure-eval", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } wheels = [ @@ -2258,10 +2387,10 @@ version = "1.7.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.14'" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "svgwrite" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.14'" }, + { name = "svgwrite", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3e/a0/06163182bdb00300a23e51b433a6ca5c881761e7dfbef432b4da44e3a84a/svgpathtools-1.7.2.tar.gz", hash = "sha256:5974daba24825e22f284ea10aa980d7d6f77a1ca55d914d80283e3ea8a7ac450", size = 2136092, upload-time = "2025-11-30T19:15:03.446Z" } wheels = [ @@ -2298,6 +2427,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, ] +[[package]] +name = "threejs-materials" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pillow" }, + { name = "pygltflib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/7b/0f9e298de06134e36ffe1e4b365cda5605cc3a11c1b40936b941f357130f/threejs_materials-1.0.4.tar.gz", hash = "sha256:315b9dbb7d5b87daa3735326f758faacf0b906ca6c49b737e91dff8d1768dbff", size = 92571, upload-time = "2026-04-09T12:05:00.183Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/3a/59d0ad302eb4a5ca891b40c8ce1916690737b714d52f100ca696f23150d3/threejs_materials-1.0.4-py3-none-any.whl", hash = "sha256:d38e27dca790996e9599123e554b8d8186e616d458bf308b5d2f64c47451642e", size = 57526, upload-time = "2026-04-09T12:04:58.803Z" }, +] + +[package.optional-dependencies] +materialx = [ + { name = "materialx" }, + { name = "openexr" }, +] + [[package]] name = "tomli" version = "2.4.1" @@ -2391,6 +2540,19 @@ 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-inspect" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" }, +] + [[package]] name = "tzdata" version = "2025.3" @@ -2423,7 +2585,7 @@ name = "vtk" version = "9.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "matplotlib" }, + { name = "matplotlib", marker = "python_full_version < '3.14'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/95/1f/91780093a0cf2afc234063bb9697d1285ccba138c1531700bc2986e65adc/vtk-9.3.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:c41ed344b9cc90ee9dcfc5967815de272985647d0c8e0a57f0e8b4229bc1b0b9", size = 76687544, upload-time = "2024-06-29T03:14:20.271Z" }, @@ -2457,3 +2619,89 @@ sdist = { url = "https://files.pythonhosted.org/packages/fe/f8/53150a5bda7e04284 wheels = [ { url = "https://files.pythonhosted.org/packages/f0/33/12020ba99beaff91682b28dc0bbf0345bbc3244a4afbae7644e4fa348f23/webcolors-24.8.0-py3-none-any.whl", hash = "sha256:fc4c3b59358ada164552084a8ebee637c221e4059267d0f8325b3b560f6c7f0a", size = 15027, upload-time = "2024-08-10T08:52:28.707Z" }, ] + +[[package]] +name = "wrapt" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/d2/387594fb592d027366645f3d7cc9b4d7ca7be93845fbaba6d835a912ef3c/wrapt-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a86d99a14f76facb269dc148590c01aaf47584071809a70da30555228158c", size = 60669, upload-time = "2026-03-06T02:52:40.671Z" }, + { url = "https://files.pythonhosted.org/packages/c9/18/3f373935bc5509e7ac444c8026a56762e50c1183e7061797437ca96c12ce/wrapt-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a819e39017f95bf7aede768f75915635aa8f671f2993c036991b8d3bfe8dbb6f", size = 61603, upload-time = "2026-03-06T02:54:21.032Z" }, + { url = "https://files.pythonhosted.org/packages/c2/7a/32758ca2853b07a887a4574b74e28843919103194bb47001a304e24af62f/wrapt-2.1.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5681123e60aed0e64c7d44f72bbf8b4ce45f79d81467e2c4c728629f5baf06eb", size = 113632, upload-time = "2026-03-06T02:53:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d5/eeaa38f670d462e97d978b3b0d9ce06d5b91e54bebac6fbed867809216e7/wrapt-2.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8b28e97a44d21836259739ae76284e180b18abbb4dcfdff07a415cf1016c3e", size = 115644, upload-time = "2026-03-06T02:54:53.33Z" }, + { url = "https://files.pythonhosted.org/packages/e3/09/2a41506cb17affb0bdf9d5e2129c8c19e192b388c4c01d05e1b14db23c00/wrapt-2.1.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cef91c95a50596fcdc31397eb6955476f82ae8a3f5a8eabdc13611b60ee380ba", size = 112016, upload-time = "2026-03-06T02:54:43.274Z" }, + { url = "https://files.pythonhosted.org/packages/64/15/0e6c3f5e87caadc43db279724ee36979246d5194fa32fed489c73643ba59/wrapt-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dad63212b168de8569b1c512f4eac4b57f2c6934b30df32d6ee9534a79f1493f", size = 114823, upload-time = "2026-03-06T02:54:29.392Z" }, + { url = "https://files.pythonhosted.org/packages/56/b2/0ad17c8248f4e57bedf44938c26ec3ee194715f812d2dbbd9d7ff4be6c06/wrapt-2.1.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d307aa6888d5efab2c1cde09843d48c843990be13069003184b67d426d145394", size = 111244, upload-time = "2026-03-06T02:54:02.149Z" }, + { url = "https://files.pythonhosted.org/packages/ff/04/bcdba98c26f2c6522c7c09a726d5d9229120163493620205b2f76bd13c01/wrapt-2.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c87cf3f0c85e27b3ac7d9ad95da166bf8739ca215a8b171e8404a2d739897a45", size = 113307, upload-time = "2026-03-06T02:54:12.428Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1b/5e2883c6bc14143924e465a6fc5a92d09eeabe35310842a481fb0581f832/wrapt-2.1.2-cp310-cp310-win32.whl", hash = "sha256:d1c5fea4f9fe3762e2b905fdd67df51e4be7a73b7674957af2d2ade71a5c075d", size = 57986, upload-time = "2026-03-06T02:54:26.823Z" }, + { url = "https://files.pythonhosted.org/packages/42/5a/4efc997bccadd3af5749c250b49412793bc41e13a83a486b2b54a33e240c/wrapt-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:d8f7740e1af13dff2684e4d56fe604a7e04d6c94e737a60568d8d4238b9a0c71", size = 60336, upload-time = "2026-03-06T02:54:18Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f5/a2bb833e20181b937e87c242645ed5d5aa9c373006b0467bfe1a35c727d0/wrapt-2.1.2-cp310-cp310-win_arm64.whl", hash = "sha256:1c6cc827c00dc839350155f316f1f8b4b0c370f52b6a19e782e2bda89600c7dc", size = 58757, upload-time = "2026-03-06T02:53:51.545Z" }, + { url = "https://files.pythonhosted.org/packages/c7/81/60c4471fce95afa5922ca09b88a25f03c93343f759aae0f31fb4412a85c7/wrapt-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96159a0ee2b0277d44201c3b5be479a9979cf154e8c82fa5df49586a8e7679bb", size = 60666, upload-time = "2026-03-06T02:52:58.934Z" }, + { url = "https://files.pythonhosted.org/packages/6b/be/80e80e39e7cb90b006a0eaf11c73ac3a62bbfb3068469aec15cc0bc795de/wrapt-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98ba61833a77b747901e9012072f038795de7fc77849f1faa965464f3f87ff2d", size = 61601, upload-time = "2026-03-06T02:53:00.487Z" }, + { url = "https://files.pythonhosted.org/packages/b0/be/d7c88cd9293c859fc74b232abdc65a229bb953997995d6912fc85af18323/wrapt-2.1.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:767c0dbbe76cae2a60dd2b235ac0c87c9cccf4898aef8062e57bead46b5f6894", size = 114057, upload-time = "2026-03-06T02:52:44.08Z" }, + { url = "https://files.pythonhosted.org/packages/ea/25/36c04602831a4d685d45a93b3abea61eca7fe35dab6c842d6f5d570ef94a/wrapt-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c691a6bc752c0cc4711cc0c00896fcd0f116abc253609ef64ef930032821842", size = 116099, upload-time = "2026-03-06T02:54:56.74Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4e/98a6eb417ef551dc277bec1253d5246b25003cf36fdf3913b65cb7657a56/wrapt-2.1.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f3b7d73012ea75aee5844de58c88f44cf62d0d62711e39da5a82824a7c4626a8", size = 112457, upload-time = "2026-03-06T02:53:52.842Z" }, + { url = "https://files.pythonhosted.org/packages/cb/a6/a6f7186a5297cad8ec53fd7578533b28f795fdf5372368c74bd7e6e9841c/wrapt-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:577dff354e7acd9d411eaf4bfe76b724c89c89c8fc9b7e127ee28c5f7bcb25b6", size = 115351, upload-time = "2026-03-06T02:53:32.684Z" }, + { url = "https://files.pythonhosted.org/packages/97/6f/06e66189e721dbebd5cf20e138acc4d1150288ce118462f2fcbff92d38db/wrapt-2.1.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d7b6fd105f8b24e5bd23ccf41cb1d1099796524bcc6f7fbb8fe576c44befbc9", size = 111748, upload-time = "2026-03-06T02:53:08.455Z" }, + { url = "https://files.pythonhosted.org/packages/ef/43/4808b86f499a51370fbdbdfa6cb91e9b9169e762716456471b619fca7a70/wrapt-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:866abdbf4612e0b34764922ef8b1c5668867610a718d3053d59e24a5e5fcfc15", size = 113783, upload-time = "2026-03-06T02:53:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/91/2c/a3f28b8fa7ac2cefa01cfcaca3471f9b0460608d012b693998cd61ef43df/wrapt-2.1.2-cp311-cp311-win32.whl", hash = "sha256:5a0a0a3a882393095573344075189eb2d566e0fd205a2b6414e9997b1b800a8b", size = 57977, upload-time = "2026-03-06T02:53:27.844Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c3/2b1c7bd07a27b1db885a2fab469b707bdd35bddf30a113b4917a7e2139d2/wrapt-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:64a07a71d2730ba56f11d1a4b91f7817dc79bc134c11516b75d1921a7c6fcda1", size = 60336, upload-time = "2026-03-06T02:54:28.104Z" }, + { url = "https://files.pythonhosted.org/packages/ec/5c/76ece7b401b088daa6503d6264dd80f9a727df3e6042802de9a223084ea2/wrapt-2.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:b89f095fe98bc12107f82a9f7d570dc83a0870291aeb6b1d7a7d35575f55d98a", size = 58756, upload-time = "2026-03-06T02:53:16.319Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b6/1db817582c49c7fcbb7df6809d0f515af29d7c2fbf57eb44c36e98fb1492/wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9", size = 61255, upload-time = "2026-03-06T02:52:45.663Z" }, + { url = "https://files.pythonhosted.org/packages/a2/16/9b02a6b99c09227c93cd4b73acc3678114154ec38da53043c0ddc1fba0dc/wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748", size = 61848, upload-time = "2026-03-06T02:53:48.728Z" }, + { url = "https://files.pythonhosted.org/packages/af/aa/ead46a88f9ec3a432a4832dfedb84092fc35af2d0ba40cd04aea3889f247/wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e", size = 121433, upload-time = "2026-03-06T02:54:40.328Z" }, + { url = "https://files.pythonhosted.org/packages/3a/9f/742c7c7cdf58b59085a1ee4b6c37b013f66ac33673a7ef4aaed5e992bc33/wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8", size = 123013, upload-time = "2026-03-06T02:53:26.58Z" }, + { url = "https://files.pythonhosted.org/packages/e8/44/2c3dd45d53236b7ed7c646fcf212251dc19e48e599debd3926b52310fafb/wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c", size = 117326, upload-time = "2026-03-06T02:53:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/74/e2/b17d66abc26bd96f89dec0ecd0ef03da4a1286e6ff793839ec431b9fae57/wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c", size = 121444, upload-time = "2026-03-06T02:54:09.5Z" }, + { url = "https://files.pythonhosted.org/packages/3c/62/e2977843fdf9f03daf1586a0ff49060b1b2fc7ff85a7ea82b6217c1ae36e/wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1", size = 116237, upload-time = "2026-03-06T02:54:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/27fc67914e68d740bce512f11734aec08696e6b17641fef8867c00c949fc/wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2", size = 120563, upload-time = "2026-03-06T02:53:20.412Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9f/b750b3692ed2ef4705cb305bd68858e73010492b80e43d2a4faa5573cbe7/wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0", size = 58198, upload-time = "2026-03-06T02:53:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b2/feecfe29f28483d888d76a48f03c4c4d8afea944dbee2b0cd3380f9df032/wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63", size = 60441, upload-time = "2026-03-06T02:52:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/44/e1/e328f605d6e208547ea9fd120804fcdec68536ac748987a68c47c606eea8/wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf", size = 58836, upload-time = "2026-03-06T02:53:22.053Z" }, + { url = "https://files.pythonhosted.org/packages/4c/7a/d936840735c828b38d26a854e85d5338894cda544cb7a85a9d5b8b9c4df7/wrapt-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd6f4d67befa6fe2abdffcbd3de2d82dfc6fb8a6d850407c53332709d030b", size = 61259, upload-time = "2026-03-06T02:53:41.922Z" }, + { url = "https://files.pythonhosted.org/packages/5e/88/9a9b9a90ac8ca11c2fdb6a286cb3a1fc7dd774c00ed70929a6434f6bc634/wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e", size = 61851, upload-time = "2026-03-06T02:52:48.672Z" }, + { url = "https://files.pythonhosted.org/packages/03/a9/5b7d6a16fd6533fed2756900fc8fc923f678179aea62ada6d65c92718c00/wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb", size = 121446, upload-time = "2026-03-06T02:54:14.013Z" }, + { url = "https://files.pythonhosted.org/packages/45/bb/34c443690c847835cfe9f892be78c533d4f32366ad2888972c094a897e39/wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca", size = 123056, upload-time = "2026-03-06T02:54:10.829Z" }, + { url = "https://files.pythonhosted.org/packages/93/b9/ff205f391cb708f67f41ea148545f2b53ff543a7ac293b30d178af4d2271/wrapt-2.1.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:162e4e2ba7542da9027821cb6e7c5e068d64f9a10b5f15512ea28e954893a267", size = 117359, upload-time = "2026-03-06T02:53:03.623Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3d/1ea04d7747825119c3c9a5e0874a40b33594ada92e5649347c457d982805/wrapt-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f29c827a8d9936ac320746747a016c4bc66ef639f5cd0d32df24f5eacbf9c69f", size = 121479, upload-time = "2026-03-06T02:53:45.844Z" }, + { url = "https://files.pythonhosted.org/packages/78/cc/ee3a011920c7a023b25e8df26f306b2484a531ab84ca5c96260a73de76c0/wrapt-2.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9dd9813825f7ecb018c17fd147a01845eb330254dff86d3b5816f20f4d6aaf8", size = 116271, upload-time = "2026-03-06T02:54:46.356Z" }, + { url = "https://files.pythonhosted.org/packages/98/fd/e5ff7ded41b76d802cf1191288473e850d24ba2e39a6ec540f21ae3b57cb/wrapt-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f8dbdd3719e534860d6a78526aafc220e0241f981367018c2875178cf83a413", size = 120573, upload-time = "2026-03-06T02:52:50.163Z" }, + { url = "https://files.pythonhosted.org/packages/47/c5/242cae3b5b080cd09bacef0591691ba1879739050cc7c801ff35c8886b66/wrapt-2.1.2-cp313-cp313-win32.whl", hash = "sha256:5c35b5d82b16a3bc6e0a04349b606a0582bc29f573786aebe98e0c159bc48db6", size = 58205, upload-time = "2026-03-06T02:53:47.494Z" }, + { url = "https://files.pythonhosted.org/packages/12/69/c358c61e7a50f290958809b3c61ebe8b3838ea3e070d7aac9814f95a0528/wrapt-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f8bc1c264d8d1cf5b3560a87bbdd31131573eb25f9f9447bb6252b8d4c44a3a1", size = 60452, upload-time = "2026-03-06T02:53:30.038Z" }, + { url = "https://files.pythonhosted.org/packages/8e/66/c8a6fcfe321295fd8c0ab1bd685b5a01462a9b3aa2f597254462fc2bc975/wrapt-2.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:3beb22f674550d5634642c645aba4c72a2c66fb185ae1aebe1e955fae5a13baf", size = 58842, upload-time = "2026-03-06T02:52:52.114Z" }, + { url = "https://files.pythonhosted.org/packages/da/55/9c7052c349106e0b3f17ae8db4b23a691a963c334de7f9dbd60f8f74a831/wrapt-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fc04bc8664a8bc4c8e00b37b5355cffca2535209fba1abb09ae2b7c76ddf82b", size = 63075, upload-time = "2026-03-06T02:53:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/09/a8/ce7b4006f7218248dd71b7b2b732d0710845a0e49213b18faef64811ffef/wrapt-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a9b9d50c9af998875a1482a038eb05755dfd6fe303a313f6a940bb53a83c3f18", size = 63719, upload-time = "2026-03-06T02:54:33.452Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e5/2ca472e80b9e2b7a17f106bb8f9df1db11e62101652ce210f66935c6af67/wrapt-2.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d3ff4f0024dd224290c0eabf0240f1bfc1f26363431505fb1b0283d3b08f11d", size = 152643, upload-time = "2026-03-06T02:52:42.721Z" }, + { url = "https://files.pythonhosted.org/packages/36/42/30f0f2cefca9d9cbf6835f544d825064570203c3e70aa873d8ae12e23791/wrapt-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3278c471f4468ad544a691b31bb856374fbdefb7fee1a152153e64019379f015", size = 158805, upload-time = "2026-03-06T02:54:25.441Z" }, + { url = "https://files.pythonhosted.org/packages/bb/67/d08672f801f604889dcf58f1a0b424fe3808860ede9e03affc1876b295af/wrapt-2.1.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8914c754d3134a3032601c6984db1c576e6abaf3fc68094bb8ab1379d75ff92", size = 145990, upload-time = "2026-03-06T02:53:57.456Z" }, + { url = "https://files.pythonhosted.org/packages/68/a7/fd371b02e73babec1de6ade596e8cd9691051058cfdadbfd62a5898f3295/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff95d4264e55839be37bafe1536db2ab2de19da6b65f9244f01f332b5286cfbf", size = 155670, upload-time = "2026-03-06T02:54:55.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/9fe0095dfdb621009f40117dcebf41d7396c2c22dca6eac779f4c007b86c/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:76405518ca4e1b76fbb1b9f686cff93aebae03920cc55ceeec48ff9f719c5f67", size = 144357, upload-time = "2026-03-06T02:54:24.092Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/ec7b4a254abbe4cde9fa15c5d2cca4518f6b07d0f1b77d4ee9655e30280e/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c0be8b5a74c5824e9359b53e7e58bef71a729bacc82e16587db1c4ebc91f7c5a", size = 150269, upload-time = "2026-03-06T02:53:31.268Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6b/2fabe8ebf148f4ee3c782aae86a795cc68ffe7d432ef550f234025ce0cfa/wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd", size = 59894, upload-time = "2026-03-06T02:54:15.391Z" }, + { url = "https://files.pythonhosted.org/packages/ca/fb/9ba66fc2dedc936de5f8073c0217b5d4484e966d87723415cc8262c5d9c2/wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f", size = 63197, upload-time = "2026-03-06T02:54:41.943Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1c/012d7423c95d0e337117723eb8ecf73c622ce15a97847e84cf3f8f26cd7e/wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679", size = 60363, upload-time = "2026-03-06T02:54:48.093Z" }, + { url = "https://files.pythonhosted.org/packages/39/25/e7ea0b417db02bb796182a5316398a75792cd9a22528783d868755e1f669/wrapt-2.1.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1370e516598854e5b4366e09ce81e08bfe94d42b0fd569b88ec46cc56d9164a9", size = 61418, upload-time = "2026-03-06T02:53:55.706Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0f/fa539e2f6a770249907757eaeb9a5ff4deb41c026f8466c1c6d799088a9b/wrapt-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6de1a3851c27e0bd6a04ca993ea6f80fc53e6c742ee1601f486c08e9f9b900a9", size = 61914, upload-time = "2026-03-06T02:52:53.37Z" }, + { url = "https://files.pythonhosted.org/packages/53/37/02af1867f5b1441aaeda9c82deed061b7cd1372572ddcd717f6df90b5e93/wrapt-2.1.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:de9f1a2bbc5ac7f6012ec24525bdd444765a2ff64b5985ac6e0692144838542e", size = 120417, upload-time = "2026-03-06T02:54:30.74Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b7/0138a6238c8ba7476c77cf786a807f871672b37f37a422970342308276e7/wrapt-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:970d57ed83fa040d8b20c52fe74a6ae7e3775ae8cff5efd6a81e06b19078484c", size = 122797, upload-time = "2026-03-06T02:54:51.539Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ad/819ae558036d6a15b7ed290d5b14e209ca795dd4da9c58e50c067d5927b0/wrapt-2.1.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3969c56e4563c375861c8df14fa55146e81ac11c8db49ea6fb7f2ba58bc1ff9a", size = 117350, upload-time = "2026-03-06T02:54:37.651Z" }, + { url = "https://files.pythonhosted.org/packages/8b/2d/afc18dc57a4600a6e594f77a9ae09db54f55ba455440a54886694a84c71b/wrapt-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:57d7c0c980abdc5f1d98b11a2aa3bb159790add80258c717fa49a99921456d90", size = 121223, upload-time = "2026-03-06T02:54:35.221Z" }, + { url = "https://files.pythonhosted.org/packages/b9/5b/5ec189b22205697bc56eb3b62aed87a1e0423e9c8285d0781c7a83170d15/wrapt-2.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:776867878e83130c7a04237010463372e877c1c994d449ca6aaafeab6aab2586", size = 116287, upload-time = "2026-03-06T02:54:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/f7/2d/f84939a7c9b5e6cdd8a8d0f6a26cabf36a0f7e468b967720e8b0cd2bdf69/wrapt-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fab036efe5464ec3291411fabb80a7a39e2dd80bae9bcbeeca5087fdfa891e19", size = 119593, upload-time = "2026-03-06T02:54:16.697Z" }, + { url = "https://files.pythonhosted.org/packages/0b/fe/ccd22a1263159c4ac811ab9374c061bcb4a702773f6e06e38de5f81a1bdc/wrapt-2.1.2-cp314-cp314-win32.whl", hash = "sha256:e6ed62c82ddf58d001096ae84ce7f833db97ae2263bff31c9b336ba8cfe3f508", size = 58631, upload-time = "2026-03-06T02:53:06.498Z" }, + { url = "https://files.pythonhosted.org/packages/65/0a/6bd83be7bff2e7efaac7b4ac9748da9d75a34634bbbbc8ad077d527146df/wrapt-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:467e7c76315390331c67073073d00662015bb730c566820c9ca9b54e4d67fd04", size = 60875, upload-time = "2026-03-06T02:53:50.252Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c0/0b3056397fe02ff80e5a5d72d627c11eb885d1ca78e71b1a5c1e8c7d45de/wrapt-2.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:da1f00a557c66225d53b095a97eace0fc5349e3bfda28fa34ffae238978ee575", size = 59164, upload-time = "2026-03-06T02:53:59.128Z" }, + { url = "https://files.pythonhosted.org/packages/71/ed/5d89c798741993b2371396eb9d4634f009ff1ad8a6c78d366fe2883ea7a6/wrapt-2.1.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:62503ffbc2d3a69891cf29beeaccdb4d5e0a126e2b6a851688d4777e01428dbb", size = 63163, upload-time = "2026-03-06T02:52:54.873Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8c/05d277d182bf36b0a13d6bd393ed1dec3468a25b59d01fba2dd70fe4d6ae/wrapt-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7e6cd120ef837d5b6f860a6ea3745f8763805c418bb2f12eeb1fa6e25f22d22", size = 63723, upload-time = "2026-03-06T02:52:56.374Z" }, + { url = "https://files.pythonhosted.org/packages/f4/27/6c51ec1eff4413c57e72d6106bb8dec6f0c7cdba6503d78f0fa98767bcc9/wrapt-2.1.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3769a77df8e756d65fbc050333f423c01ae012b4f6731aaf70cf2bef61b34596", size = 152652, upload-time = "2026-03-06T02:53:23.79Z" }, + { url = "https://files.pythonhosted.org/packages/db/4c/d7dd662d6963fc7335bfe29d512b02b71cdfa23eeca7ab3ac74a67505deb/wrapt-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a76d61a2e851996150ba0f80582dd92a870643fa481f3b3846f229de88caf044", size = 158807, upload-time = "2026-03-06T02:53:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/b4/4d/1e5eea1a78d539d346765727422976676615814029522c76b87a95f6bcdd/wrapt-2.1.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6f97edc9842cf215312b75fe737ee7c8adda75a89979f8e11558dfff6343cc4b", size = 146061, upload-time = "2026-03-06T02:52:57.574Z" }, + { url = "https://files.pythonhosted.org/packages/89/bc/62cabea7695cd12a288023251eeefdcb8465056ddaab6227cb78a2de005b/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4006c351de6d5007aa33a551f600404ba44228a89e833d2fadc5caa5de8edfbf", size = 155667, upload-time = "2026-03-06T02:53:39.422Z" }, + { url = "https://files.pythonhosted.org/packages/e9/99/6f2888cd68588f24df3a76572c69c2de28287acb9e1972bf0c83ce97dbc1/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a9372fc3639a878c8e7d87e1556fa209091b0a66e912c611e3f833e2c4202be2", size = 144392, upload-time = "2026-03-06T02:54:22.41Z" }, + { url = "https://files.pythonhosted.org/packages/40/51/1dfc783a6c57971614c48e361a82ca3b6da9055879952587bc99fe1a7171/wrapt-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3144b027ff30cbd2fca07c0a87e67011adb717eb5f5bd8496325c17e454257a3", size = 150296, upload-time = "2026-03-06T02:54:07.848Z" }, + { url = "https://files.pythonhosted.org/packages/6c/38/cbb8b933a0201076c1f64fc42883b0023002bdc14a4964219154e6ff3350/wrapt-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:3b8d15e52e195813efe5db8cec156eebe339aaf84222f4f4f051a6c01f237ed7", size = 60539, upload-time = "2026-03-06T02:54:00.594Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/e5176e4b241c9f528402cebb238a36785a628179d7d8b71091154b3e4c9e/wrapt-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:08ffa54146a7559f5b8df4b289b46d963a8e74ed16ba3687f99896101a3990c5", size = 63969, upload-time = "2026-03-06T02:54:39Z" }, + { url = "https://files.pythonhosted.org/packages/5c/99/79f17046cf67e4a95b9987ea129632ba8bcec0bc81f3fb3d19bdb0bd60cd/wrapt-2.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:72aaa9d0d8e4ed0e2e98019cea47a21f823c9dd4b43c7b77bba6679ffcca6a00", size = 60554, upload-time = "2026-03-06T02:53:14.132Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" }, +]