diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6a578f66..89ab6dec 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -22,28 +22,52 @@ jobs:
- run: pipx run check-manifest
lint:
- runs-on: ubuntu-latest
+ # We lint on Windows so we can lint wx code.
+ runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
with:
enable-cache: true
python-version: "3.13"
- - uses: tox-dev/action-pre-commit-uv@v1
- with:
- extra_args: --all-files --verbose
+ - run: uvx pre-commit run --all-files --verbose
test:
- name: test ${{ matrix.os }} py${{ matrix.python-version }} ${{ matrix.gfx }}
+ name: test ${{ matrix.os }} py${{ matrix.python-version }} ${{ matrix.gfx }} ${{ matrix.canvas }}
runs-on: ${{ matrix.os }}
- env:
- UV_FROZEN: 1
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
- python-version: ["3.10", "3.11", "3.12", "3.13"]
+ python-version: ["3.10", "3.13"]
gfx: [pygfx, vispy]
+ canvas: [pyqt, jupyter, wx]
+ exclude:
+ # glfw.GLFWError: (65545) b'NSGL: Failed to find a suitable pixel format'
+ # (Under the hood GLFW gives vispy the OpenGL context.)
+ - os: macos-latest
+ canvas: jupyter
+ gfx: vispy
+ # wxpython does not build wheels for ubuntu or macos-latest py3.10
+ - os: ubuntu-latest
+ canvas: wx
+ - os: macos-latest
+ canvas: wx
+ python-version: "3.10"
+ # FIXME: On CI: AttributeError: 'Shared' object has no attribute '_device'. Did you mean: 'device'?
+ # Related to pygfx v0.13.0
+ # Tests pass locally
+ - os: windows-latest
+ gfx: pygfx
+ include:
+ - python-version: "3.11"
+ gfx: pygfx
+ canvas: pyqt
+ os: ubuntu-latest
+ - python-version: "3.12"
+ gfx: pygfx
+ canvas: pyqt
+ os: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -61,7 +85,7 @@ jobs:
sudo apt install -y libegl1-mesa-dev libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers
- name: Install dependencies
- run: uv sync --no-dev --group test --extra ${{ matrix.gfx }} ${{ matrix.python-version != '3.10' && '--extra imgui' || '' }}
+ run: uv sync --no-dev --group test --extra ${{ matrix.gfx }} --extra ${{matrix.canvas}} ${{ matrix.python-version != '3.10' && '--extra imgui' || '' }} ${{ matrix.canvas == 'pyqt' && '--group testqt' || '' }}
- name: Test
shell: bash
@@ -70,7 +94,7 @@ jobs:
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
- name: covreport-${{ matrix.os }}-py${{ matrix.python-version }}-${{ matrix.gfx }}
+ name: covreport-${{ matrix.os }}-py${{ matrix.python-version }}-${{ matrix.gfx }}-${{ matrix.canvas }}
path: ./.coverage*
include-hidden-files: true
diff --git a/.gitignore b/.gitignore
index 13afad07..b24f8d65 100644
--- a/.gitignore
+++ b/.gitignore
@@ -109,3 +109,5 @@ ENV/
# IDE settings
.vscode/
.idea/
+
+uv.lock
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 61959a0d..d938a9d8 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -27,26 +27,26 @@ repos:
language: system
types_or: [python, pyi]
require_serial: true
- entry: uv run ruff format --force-exclude
+ entry: uv run --active ruff format --force-exclude
- id: ruff-check
name: ruff check
language: system
types_or: [python, pyi]
require_serial: true
- entry: uv run ruff check --force-exclude
+ entry: uv run --active ruff check --force-exclude
args: [--output-format=full, --show-fixes, --exit-non-zero-on-fix]
- id: mypy
name: mypy
language: system
types_or: [python, pyi]
require_serial: true
- entry: uv run mypy
+ entry: uv run --active mypy
- id: pyright
name: pyright
language: system
types_or: [python, pyi]
require_serial: true
- entry: uv run pyright
+ entry: uv run --active pyright
- repo: https://github.com/crate-ci/typos
rev: v1.34.0
diff --git a/examples/basic_scene.py b/examples/basic_scene.py
index da80ded4..b467d2eb 100644
--- a/examples/basic_scene.py
+++ b/examples/basic_scene.py
@@ -2,6 +2,7 @@
import numpy as np
import scenex as snx
+from scenex.utils.controllers import PanZoomController
try:
from scenex.imgui import add_imgui_controls
@@ -28,6 +29,7 @@
),
]
),
+ camera=snx.Camera(controller=PanZoomController(), interactive=True),
)
# example of adding an object to a scene
@@ -41,6 +43,7 @@
# snx.use("vispy")
snx.show(view)
+
if add_imgui_controls is not None:
add_imgui_controls(view)
snx.run()
diff --git a/examples/basic_text.py b/examples/basic_text.py
new file mode 100644
index 00000000..9c379e03
--- /dev/null
+++ b/examples/basic_text.py
@@ -0,0 +1,17 @@
+import cmap
+
+import scenex as snx
+from scenex.utils.controllers import PanZoomController
+
+view = snx.View(
+ scene=snx.Scene(
+ children=[snx.Text(text="Hello, Scenex!", color=cmap.Color("cyan"), size=24)]
+ ),
+ camera=snx.Camera(controller=PanZoomController(), interactive=True),
+)
+
+
+# Show and position camera
+snx.use("pygfx")
+snx.show(view)
+snx.run()
diff --git a/examples/basic_volume.py b/examples/basic_volume.py
index 6ace26f7..f06c4fc6 100644
--- a/examples/basic_volume.py
+++ b/examples/basic_volume.py
@@ -1,6 +1,9 @@
import numpy as np
import scenex as snx
+from scenex.model._transform import Transform
+from scenex.utils import projections
+from scenex.utils.controllers import OrbitController
try:
from imageio.v2 import volread
@@ -16,11 +19,28 @@
snx.Volume(
data=data,
clims=(data.min(), data.max()),
- ),
+ )
]
),
- camera=snx.Camera(type="perspective"),
+ camera=snx.Camera(interactive=True),
)
+snx.use("vispy")
snx.show(view)
+
+# Orbit around the center of the volume
+orbit_center = np.mean(np.asarray(view.scene.bounding_box), axis=0)
+
+# Place the camera along the x axis, looking at the orbit center
+view.camera.transform = Transform().translated(orbit_center).translated((300, 0, 0))
+view.camera.look_at(orbit_center, up=(0, 0, 1))
+# Perspective projection for 3D
+view.camera.projection = projections.perspective(
+ fov=70,
+ near=1,
+ far=1_000_000, # Just need something big
+)
+view.camera.set_event_filter(OrbitController(orbit_center))
+
+
snx.run()
diff --git a/examples/blending.py b/examples/blending.py
new file mode 100644
index 00000000..5e197390
--- /dev/null
+++ b/examples/blending.py
@@ -0,0 +1,96 @@
+"""An example demonstrating different blend modes.
+
+Unaltered, each channel of the volume is blended additively, resulting in transparency.
+
+By clicking on the volume, the blend mode will alternate to an opaque strategy. In this
+case, fragments written by the nuclei channel will always be visible over fragments
+written by the membranes channel (because its draw order is higher).
+"""
+
+import cmap
+import numpy as np
+
+import scenex as snx
+import scenex.model
+from scenex.app.events import Event, MousePressEvent
+from scenex.model._transform import Transform
+from scenex.utils import projections
+from scenex.utils.controllers import OrbitController
+
+try:
+ from imageio.v2 import volread
+
+ url = "https://gitlab.com/scikit-image/data/-/raw/2cdc5ce89b334d28f06a58c9f0ca21aa6992a5ba/cells3d.tif"
+ data = np.asarray(volread(url)).astype(np.uint16)[:, :, :, :]
+except ImportError:
+ data = np.random.randint(0, 2, (3, 2, 3, 3)).astype(np.uint16)
+
+data1 = data[:, 0, :, :]
+volume1 = snx.Volume(
+ data=data1,
+ cmap=cmap.Colormap("green"),
+ blending=scenex.model.BlendMode.ADDITIVE,
+ clims=(data1.min(), data1.max()),
+ opacity=0.7,
+ order=1,
+ name="Cell membranes",
+ interactive=True,
+)
+data2 = data[:, 1, :, :]
+volume2 = snx.Volume(
+ data=data2,
+ blending=scenex.model.BlendMode.ADDITIVE,
+ cmap=cmap.Colormap("magenta"),
+ clims=(data2.min(), data2.max()),
+ opacity=0.7,
+ order=2,
+ name="Cell nuclei",
+ interactive=True,
+ transform=Transform().translated((0, 0, 30)),
+)
+
+view = snx.View(
+ scene=snx.Scene(
+ children=[volume1, volume2],
+ interactive=True,
+ ),
+ camera=snx.Camera(interactive=True),
+)
+
+blend_modes = list(scenex.model.BlendMode)
+
+
+def change_blend_mode(event: Event, node: snx.Node) -> bool:
+ """Change the blend mode of a volume when it is clicked."""
+ if isinstance(event, MousePressEvent):
+ idx = blend_modes.index(node.blending)
+ next_idx = (idx + 1) % len(blend_modes)
+
+ print(f"Changing blend mode to {blend_modes[next_idx]}")
+
+ volume1.blending = blend_modes[next_idx]
+ volume2.blending = blend_modes[next_idx]
+ return False
+
+
+volume1.set_event_filter(change_blend_mode)
+
+
+snx.use("vispy")
+snx.show(view)
+
+# Orbit around the center of the volume
+orbit_center = np.mean(np.asarray(view.scene.bounding_box), axis=0)
+
+# Place the camera along the x axis, looking at the orbit center
+view.camera.transform = Transform().translated(orbit_center).translated((300, 0, 0))
+view.camera.look_at(orbit_center, up=(0, 0, 1))
+# Perspective projection for 3D
+view.camera.projection = projections.perspective(
+ fov=70,
+ near=1,
+ far=1_000_000, # Just need something big
+)
+view.camera.set_event_filter(OrbitController(orbit_center))
+
+snx.run()
diff --git a/examples/event_filters.py b/examples/event_filters.py
new file mode 100644
index 00000000..a79fd233
--- /dev/null
+++ b/examples/event_filters.py
@@ -0,0 +1,58 @@
+import cmap
+import numpy as np
+
+import scenex as snx
+from scenex.app.events import Event, MouseEnterEvent, MouseLeaveEvent, MouseMoveEvent
+
+img = snx.Image(
+ data=np.zeros((200, 200)).astype(np.uint8),
+ cmap=cmap.Colormap("viridis"),
+ clims=(0, 255),
+ opacity=0.7,
+ interactive=True,
+)
+
+view = snx.View(scene=snx.Scene(children=[img]))
+
+
+def _view_filter(event: Event) -> bool:
+ """Example event drawing a square that reacts to the cursor."""
+ if isinstance(event, MouseMoveEvent):
+ intersections = event.world_ray.intersections(view.scene)
+ if not intersections:
+ # Clear the image if the mouse is not over it
+ img.data = np.zeros((200, 200), dtype=np.uint8)
+ return True
+ for node, distance in intersections:
+ if not isinstance(node, snx.Image):
+ continue
+ intersection = event.world_ray.point_at_distance(distance)
+ data = np.zeros((200, 200), dtype=np.uint8)
+ x = int(intersection[0])
+ min_x = max(0, x - 5)
+ max_x = min(data.shape[0], x + 5)
+
+ y = int(intersection[1])
+ min_y = max(0, y - 5)
+ max_y = min(data.shape[1], y + 5)
+
+ data[min_x:max_x, min_y:max_y] = 255
+ node.data = data
+ if isinstance(event, MouseEnterEvent):
+ # Restore original colormap and clear the image when mouse enters
+ img.data = np.zeros((200, 200), dtype=np.uint8)
+ if isinstance(event, MouseLeaveEvent):
+ # Add a bright border when mouse leaves the view
+ data = np.zeros((200, 200), dtype=np.uint8)
+ data[0:3, :] = 255 # Top border
+ data[-3:, :] = 255 # Bottom border
+ data[:, 0:3] = 255 # Left border
+ data[:, -3:] = 255 # Right border
+ img.data = data
+ return True
+
+
+view.set_event_filter(_view_filter)
+
+snx.show(view)
+snx.run()
diff --git a/examples/notebook.ipynb b/examples/notebook.ipynb
new file mode 100644
index 00000000..773d03cb
--- /dev/null
+++ b/examples/notebook.ipynb
@@ -0,0 +1,146 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "aa695816",
+ "metadata": {
+ "jupyter": {
+ "source_hidden": true
+ }
+ },
+ "outputs": [],
+ "source": [
+ "# /// script\n",
+ "# requires-python = \">=3.10\"\n",
+ "# dependencies = [\n",
+ "# \"imageio[tifffile]\",\n",
+ "# \"scenex[jupyter,pygfx]\",\n",
+ "# ]\n",
+ "# ///"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "461399b0-e02d-43d9-9ede-c1aa6c180338",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "1db93420dd844ff0b73ef66d9d114f1d",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "RFBOutputContext()"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "f76cec7a2a2b42b180d72183bf7dd84a",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/html": [
+ "

snapshot
"
+ ],
+ "text/plain": [
+ "JupyterRenderCanvas(css_height='500.0px', css_width='500.0px')"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "import scenex as snx\n",
+ "from scenex.utils.controllers import PanZoomController\n",
+ "import numpy as np\n",
+ "import cmap\n",
+ "\n",
+ "image = snx.Image(\n",
+ " data=np.random.randint(0, 255, (200, 200)).astype(np.uint8),\n",
+ " cmap=cmap.Colormap(\"viridis\"),\n",
+ " transform=snx.Transform().scaled((1.3, 0.5)).translated((-40, 20)),\n",
+ " clims=(0, 255),\n",
+ " opacity=0.7,\n",
+ ")\n",
+ "points = snx.Points(\n",
+ " coords=np.random.randint(0, 200, (100, 2)).astype(np.uint8),\n",
+ " size=5,\n",
+ " face_color=cmap.Color(\"coral\"),\n",
+ " edge_color=cmap.Color(\"purple\"),\n",
+ " transform=snx.Transform().translated((0, -50)),\n",
+ " interactive=True,\n",
+ ")\n",
+ "\n",
+ "view = snx.View(\n",
+ " blending=\"default\",\n",
+ " scene=snx.Scene(\n",
+ " children=[image, points]\n",
+ " ),\n",
+ " camera=snx.Camera(controller=PanZoomController(), interactive=True),\n",
+ ")\n",
+ "\n",
+ "canvas = snx.show(view)\n",
+ "snx.run()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "fb96f975-aa05-4d8a-9f85-f4316293e05d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from IPython.display import display\n",
+ "import random\n",
+ "\n",
+ "random.seed(0xdeadbeef)\n",
+ "colors = [\"blue\", \"red\", \"yellow\", \"green\"]\n",
+ "\n",
+ "def foo(event, node) -> bool:\n",
+ " if isinstance(node, snx.Points):\n",
+ " node.face_color = colors[random.randint(0, len(colors) - 1)]\n",
+ " return False\n",
+ "\n",
+ "view.scene.children[1].set_event_filter(foo)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "54bdd9e2-f229-4afb-85d1-f84462270a39",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.13.1"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/examples/rgb.py b/examples/rgb.py
new file mode 100644
index 00000000..2522da02
--- /dev/null
+++ b/examples/rgb.py
@@ -0,0 +1,62 @@
+"""
+Demonstrates displaying an RGB image.
+
+Pressing the mouse buttons cycles through the R, G, and B channels
+Releasing the mouse button returns to the full RGB image.
+"""
+
+import cmap
+import numpy as np
+
+import scenex as snx
+import scenex.app.events as events
+from scenex.utils.controllers import PanZoomController
+
+try:
+ from imageio.v3 import imread
+
+ # FIXME: Why is the image upside down?
+ data = np.asarray(imread("imageio:astronaut.png")).astype(np.uint8)
+except Exception:
+ data = np.zeros((256, 256, 3), dtype=np.uint8)
+
+ # R,G,B are simple
+ for i in range(256):
+ data[i, :, 0] = i # Red
+ data[i, :, 2] = 255 - i # Blue
+ for j in range(256):
+ data[:, j, 1] = j # Green
+
+img = snx.Image(data=data, clims=(0, 255), interactive=True)
+
+view = snx.View(
+ scene=snx.Scene(
+ children=[
+ img,
+ ]
+ ),
+ camera=snx.Camera(controller=PanZoomController(), interactive=True),
+)
+
+idx = 0
+cmaps = ["red", "green", "blue"]
+
+
+def _event_filter(event: events.Event) -> bool:
+ if isinstance(event, events.MousePressEvent):
+ for node, _distance in event.world_ray.intersections(view.scene):
+ if node == img:
+ global idx
+ img.data = data[:, :, idx % 3]
+ img.cmap = cmap.Colormap(cmaps[idx % 3])
+ idx += 1
+ elif isinstance(event, events.MouseReleaseEvent):
+ img.data = data
+ img.cmap = cmap.Colormap("red")
+ return True # Don't block the event
+
+
+view.set_event_filter(_event_filter)
+
+snx.show(view)
+snx.run()
diff --git a/pyproject.toml b/pyproject.toml
index 2a5f41e4..ea63e00c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -32,10 +32,28 @@ classifiers = [
"Programming Language :: Python :: 3.13",
"Typing :: Typed",
]
-dependencies = ["cmap>=0.5", "numpy>=1.24", "psygnal>=0.11.1", "pydantic>=2.10"]
+dependencies = ["cmap>=0.5", "numpy>=1.24", "psygnal>=0.11.1", "pydantic>=2.10", "pylinalg"]
[project.optional-dependencies]
-pygfx = ["pygfx>=0.9.0"]
+jupyter = [
+ "ipywidgets >=8.0.5",
+ "jupyter >=1.1",
+ "jupyter_rfb >=0.3.3",
+ # Note that this dep is only needed for Vispy, something like simplejpeg would work fine if just pygfx.
+ "glfw",
+ # Otherwise jupyter_rfb will use PNG, which is apparently slower
+ "simplejpeg",
+]
+pyqt = [
+ "pyqt6 >=6.4,!=6.6",
+ "pyqt6 >=6.5.3; python_version >= '3.12'",
+ "qtpy >=2",
+ "superqt[iconify] >=0.7.2",
+]
+wx= [
+ "wxpython >=4.2.2",
+]
+pygfx = ["pygfx>=0.13.0"]
vispy = ["vispy>=0.15.0", "pyopengl"]
imgui = [
# 1.6.3 breaks type checking, 1.92 not working with pygfx
@@ -43,7 +61,8 @@ imgui = [
]
[dependency-groups]
-test = ["pytest>=8", "pytest-cov>=6", "PyQt6"]
+test = ["pytest>=8", "pytest-cov>=6"]
+testqt = [{ include-group = "test" }, "pytest-qt >=4.4"]
docs = [
"mike>=2.1.3",
"mkdocs>=1.6.1",
@@ -56,7 +75,7 @@ docs = [
dev = [
{ include-group = "test" },
{ include-group = "docs" },
- "scenex[pygfx,vispy,imgui]",
+ "scenex[pygfx,vispy,imgui,jupyter,pyqt, wx]",
"imageio[tifffile] >=2.20",
"ipython",
"mypy",
@@ -122,14 +141,14 @@ disallow_subclassing_any = false
show_error_codes = true
pretty = true
plugins = ["pydantic.mypy"]
-untyped_calls_exclude = ["rendercanvas"]
+untyped_calls_exclude = ["rendercanvas", "IPython", "pytestqt"]
[[tool.mypy.overrides]]
module = ["rendercanvas.*"]
follow_untyped_imports = true
[[tool.mypy.overrides]]
-module = ["pygfx.*", "vispy.*", "wgpu.*"]
+module = ["pygfx.*", "vispy.*", "wgpu.*", "glfw.*", "pylinalg.*", "qtpy.*", "ipywidgets.*", "IPython.*", "jupyter", "jupyter_rfb.*", "wx.*", "pytestqt.*"]
ignore_missing_imports = true
[tool.pydantic-mypy]
diff --git a/src/scenex/__init__.py b/src/scenex/__init__.py
index d116e8ed..b783fa81 100644
--- a/src/scenex/__init__.py
+++ b/src/scenex/__init__.py
@@ -16,6 +16,7 @@
from .model._nodes.node import Node
from .model._nodes.points import Points
from .model._nodes.scene import Scene
+from .model._nodes.text import Text
from .model._nodes.volume import Volume
from .model._transform import Transform
from .model._view import View
@@ -28,6 +29,7 @@
"Node",
"Points",
"Scene",
+ "Text",
"Transform",
"View",
"Volume",
diff --git a/src/scenex/adaptors/_auto.py b/src/scenex/adaptors/_auto.py
index 6b21084d..4ab4fe93 100644
--- a/src/scenex/adaptors/_auto.py
+++ b/src/scenex/adaptors/_auto.py
@@ -6,6 +6,8 @@
from contextlib import suppress
from typing import TYPE_CHECKING, Any, Literal, TypeAlias, TypeGuard, cast, get_args
+from scenex.app import app
+
if TYPE_CHECKING:
from collections.abc import Iterator
@@ -41,7 +43,7 @@ def get_adaptor_registry(backend: KnownBackend | str | None = None) -> AdaptorRe
def get_all_adaptors(obj: Any) -> Iterator[Adaptor]:
- """Get all adaptors for the given object."""
+ """Get all loaded adaptors for the given object."""
for mod_name in ["scenex.adaptors._vispy", "scenex.adaptors._pygfx"]:
if mod := sys.modules.get(mod_name):
reg = cast("AdaptorRegistry", mod.adaptors)
@@ -83,4 +85,4 @@ def use(backend: KnownBackend | None = None) -> None:
def run() -> None:
"""Enter the native GUI event loop."""
- get_adaptor_registry().start_event_loop()
+ app().run()
diff --git a/src/scenex/adaptors/_base.py b/src/scenex/adaptors/_base.py
index aa2f5658..5f6c9dfd 100644
--- a/src/scenex/adaptors/_base.py
+++ b/src/scenex/adaptors/_base.py
@@ -22,6 +22,7 @@
TImage = TypeVar("TImage", bound="model.Image", covariant=True)
TVolume = TypeVar("TVolume", bound="model.Volume", covariant=True)
TPoints = TypeVar("TPoints", bound="model.Points", covariant=True)
+TText = TypeVar("TText", bound="model.Text", covariant=True)
TCanvas = TypeVar("TCanvas", bound="model.Canvas", covariant=True)
TView = TypeVar("TView", bound="model.View", covariant=True)
TLayout = TypeVar("TLayout", bound="model.Layout", covariant=True)
@@ -41,10 +42,6 @@ class Adaptor(ABC, Generic[TModel, TNative]):
def __init__(self, obj: TModel) -> None:
"""All backend adaptor objects receive the object they are adapting."""
- @abstractmethod
- def _snx_get_native(self) -> TNative:
- """Return the native object for the ."""
-
def handle_event(self, info: EmissionInfo) -> None:
"""Receive info from psygnal callback and convert to adaptor call."""
signal_name = info.signal.name
@@ -99,6 +96,8 @@ def _snx_set_interactive(self, arg: bool, /) -> None: ...
@abstractmethod
def _snx_set_transform(self, arg: model.Transform, /) -> None: ...
@abstractmethod
+ def _snx_set_blending(self, arg: model.BlendMode, /) -> None: ...
+ @abstractmethod
def _snx_add_node(self, node: model.Node) -> None: ...
@abstractmethod
@@ -120,11 +119,7 @@ class CameraAdaptor(NodeAdaptor[TCamera, TNative]):
@abstractmethod
def _snx_set_type(self, arg: model.CameraType, /) -> None: ...
@abstractmethod
- def _snx_set_zoom(self, arg: float, /) -> None: ...
- @abstractmethod
- def _snx_set_center(self, arg: tuple[float, ...], /) -> None: ...
- @abstractmethod
- def _snx_zoom_to_fit(self, arg: float, /) -> None: ...
+ def _snx_set_projection(self, arg: model.Transform, /) -> None: ...
class ImageAdaptor(NodeAdaptor[TImage, TNative]):
@@ -168,6 +163,17 @@ def _snx_set_scaling(self, arg: model.ScalingMode, /) -> None: ...
def _snx_set_antialias(self, arg: float, /) -> None: ...
+class TextAdaptor(NodeAdaptor[TText, TNative]):
+ """Protocol for a backend Text adaptor object."""
+
+ @abstractmethod
+ def _snx_set_text(self, arg: str, /) -> None: ...
+ @abstractmethod
+ def _snx_set_color(self, arg: model.Color, /) -> None: ...
+ @abstractmethod
+ def _snx_set_size(self, arg: int, /) -> None: ...
+
+
class CanvasAdaptor(SupportsVisibility[TCanvas, TNative]):
"""Protocol defining the interface for a Canvas adaptor."""
@@ -177,6 +183,10 @@ def _snx_set_width(self, arg: int, /) -> None: ...
def _snx_set_height(self, arg: int, /) -> None: ...
@abstractmethod
def _snx_set_background_color(self, arg: model.Color | None, /) -> None: ...
+ @abstractmethod
+ def _snx_get_native(self) -> Any:
+ """Returns an object understood by the backend widget toolkit."""
+
@abstractmethod
def _snx_set_title(self, arg: str, /) -> None: ...
@abstractmethod
diff --git a/src/scenex/adaptors/_pygfx/__init__.py b/src/scenex/adaptors/_pygfx/__init__.py
index 801e9992..bd3cb0d6 100644
--- a/src/scenex/adaptors/_pygfx/__init__.py
+++ b/src/scenex/adaptors/_pygfx/__init__.py
@@ -7,6 +7,7 @@
from ._node import Node
from ._points import Points
from ._scene import Scene
+from ._text import Text
from ._view import View
from ._volume import Volume
@@ -17,6 +18,7 @@
"Node",
"Points",
"Scene",
+ "Text",
"View",
"Volume",
"adaptors",
diff --git a/src/scenex/adaptors/_pygfx/_adaptor_registry.py b/src/scenex/adaptors/_pygfx/_adaptor_registry.py
index 828955d9..29415ca1 100644
--- a/src/scenex/adaptors/_pygfx/_adaptor_registry.py
+++ b/src/scenex/adaptors/_pygfx/_adaptor_registry.py
@@ -4,11 +4,6 @@
class PygfxAdaptorRegistry(AdaptorRegistry):
- def start_event_loop(self) -> None:
- from rendercanvas.auto import loop
-
- loop.run()
-
def get_adaptor_class(self, obj: Any) -> type:
from scenex.adaptors import _pygfx
diff --git a/src/scenex/adaptors/_pygfx/_camera.py b/src/scenex/adaptors/_pygfx/_camera.py
index 011ec339..5e470a47 100644
--- a/src/scenex/adaptors/_pygfx/_camera.py
+++ b/src/scenex/adaptors/_pygfx/_camera.py
@@ -1,18 +1,17 @@
from __future__ import annotations
import logging
-from typing import TYPE_CHECKING, Any, cast
+from typing import TYPE_CHECKING, Any
-import numpy as np
import pygfx
from scenex.adaptors._base import CameraAdaptor
-from ._adaptor_registry import get_adaptor
from ._node import Node
if TYPE_CHECKING:
from scenex import model
+ from scenex.model import Transform
logger = logging.getLogger("scenex.adaptors.pygfx")
@@ -20,27 +19,15 @@
class Camera(Node, CameraAdaptor):
"""Adaptor for pygfx camera."""
- _pygfx_node: pygfx.PerspectiveCamera
+ _pygfx_node: pygfx.Camera
pygfx_controller: pygfx.Controller
def __init__(self, camera: model.Camera, **backend_kwargs: Any) -> None:
self._camera_model = camera
- if camera.type == "panzoom":
- self._pygfx_node = pygfx.OrthographicCamera()
- self.pygfx_controller = pygfx.PanZoomController(self._pygfx_node)
- elif camera.type == "perspective":
- # this type ignore is because PerspectiveCamera lacks hints
- self._pygfx_node = pygfx.PerspectiveCamera(70, 4 / 3) # pyright: ignore reportArgumentType]
- self.pygfx_controller = pygfx.OrbitController(self._pygfx_node)
+ self._pygfx_node = pygfx.Camera()
self._pygfx_node.local.scale_y = -1 # don't think this is working...
- def _snx_set_zoom(self, zoom: float) -> None:
- logger.warning("'Camera._snx_set_zoom' not implemented for pygfx")
-
- def _snx_set_center(self, arg: tuple[float, ...]) -> None:
- logger.warning("'Camera._snx_set_center' not implemented for pygfx")
-
def _snx_set_type(self, arg: model.CameraType) -> None:
logger.warning("'Camera._snx_set_type' not implemented for pygfx")
@@ -49,33 +36,5 @@ def _view_size(self) -> tuple[float, float] | None:
logger.warning("'Camera._view_size' not implemented for pygfx")
return None
- def update_controller(self) -> None:
- # This is called by the View Adaptor in the `_visit` method
- # ... which is in turn called by the Canvas backend adaptor's `_animate` method
- # i.e. the main render loop.
- self.pygfx_controller.update_camera(self._pygfx_node)
-
- def set_viewport(self, viewport: pygfx.Viewport) -> None:
- # This is used by the Canvas backend adaptor...
- # and should perhaps be moved to the View Adaptor
- self.pygfx_controller.add_default_event_handlers(viewport, self._pygfx_node)
-
- def _snx_zoom_to_fit(self, margin: float) -> None:
- # reset camera to fit all objects
- if not (scene := self._camera_model.parent):
- logger.warning("Camera has no parent scene, cannot zoom to fit")
- return
-
- gfx_scene = cast("pygfx.Scene", get_adaptor(scene)._snx_get_native())
- cam = self._pygfx_node
-
- if (bb := gfx_scene.get_world_bounding_box()) is not None:
- cam.show_object(gfx_scene)
- width, height, _depth = np.ptp(bb, axis=0)
- if width < 0.01:
- width = 1
- if height < 0.01:
- height = 1
- cam.width = width
- cam.height = height
- cam.zoom = 1 - margin
+ def _snx_set_projection(self, arg: Transform) -> None:
+ self._pygfx_node.projection_matrix = arg.root # pyright: ignore[reportAttributeAccessIssue]
diff --git a/src/scenex/adaptors/_pygfx/_canvas.py b/src/scenex/adaptors/_pygfx/_canvas.py
index c3fb4642..a83d5a2e 100644
--- a/src/scenex/adaptors/_pygfx/_canvas.py
+++ b/src/scenex/adaptors/_pygfx/_canvas.py
@@ -3,8 +3,9 @@
from typing import TYPE_CHECKING, Any, TypeGuard, cast
from scenex.adaptors._base import CanvasAdaptor
+from scenex.app import GuiFrontend, app, determine_app
-from ._adaptor_registry import adaptors
+from ._adaptor_registry import get_adaptor
if TYPE_CHECKING:
import numpy as np
@@ -24,48 +25,97 @@ def supports_hide_show(obj: Any) -> TypeGuard[SupportsHideShow]:
return hasattr(obj, "show") and hasattr(obj, "hide")
+def _rendercanvas_class() -> BaseRenderCanvas:
+ """Obtains the appropriate class for the current GUI backend.
+
+ Explicit since PyGFX's backend selection process may be different from ours.
+ """
+ frontend = determine_app()
+
+ if frontend == GuiFrontend.QT:
+ from qtpy.QtCore import QSize # pyright: ignore[reportMissingImports]
+ from rendercanvas.qt import QRenderWidget
+
+ class _QRenderWidget(QRenderWidget):
+ def sizeHint(self) -> QSize:
+ return QSize(self.width(), self.height())
+
+ # Init Qt Application - otherwise we can't create the widget
+ app()
+ return _QRenderWidget() # type: ignore[no-untyped-call]
+
+ if frontend == GuiFrontend.JUPYTER:
+ import rendercanvas.jupyter
+
+ return rendercanvas.jupyter.JupyterRenderCanvas()
+ if frontend == GuiFrontend.WX:
+ import rendercanvas.wx
+
+ return rendercanvas.wx.WxRenderCanvas()
+
+ raise ValueError("No suitable render canvas found")
+
+
class Canvas(CanvasAdaptor):
"""Canvas interface for pygfx Backend."""
def __init__(self, canvas: model.Canvas, **backend_kwargs: Any) -> None:
- from rendercanvas.auto import RenderCanvas
-
- self._wgpu_canvas = RenderCanvas()
- # Qt RenderCanvas calls show() in its __init__ method, so we need to hide it
- if supports_hide_show(self._wgpu_canvas):
- self._wgpu_canvas.hide()
+ self._canvas = canvas
+ self._wgpu_canvas = _rendercanvas_class()
+ # FIXME: This seems to not work on my laptop, without external monitors.
+ # The physical canvas size is still 625, 625...
self._wgpu_canvas.set_logical_size(canvas.width, canvas.height)
self._wgpu_canvas.set_title(canvas.title)
- self._views = canvas.views
-
- def _snx_get_native(self) -> BaseRenderCanvas:
+ self._views: list[model.View] = []
+ for view in canvas.views:
+ self._snx_add_view(view)
+ self._filter = app().install_event_filter(self._snx_get_native(), canvas)
+
+ def _snx_get_native(self) -> Any:
+ if subwdg := getattr(self._wgpu_canvas, "_subwidget", None):
+ # wx backend has a _subwidget attribute that is the actual widget
+ return subwdg
return self._wgpu_canvas
def _snx_set_visible(self, arg: bool) -> None:
- # show the qt canvas we patched earlier in __init__
- if supports_hide_show(self._wgpu_canvas):
- self._wgpu_canvas.show()
+ app().show(self, arg)
self._wgpu_canvas.request_draw(self._draw)
def _draw(self) -> None:
for view in self._views:
- adaptor = cast("View", adaptors.get_adaptor(view, create=True))
- adaptor._draw()
+ cast("View", get_adaptor(view))._draw()
def _snx_add_view(self, view: model.View) -> None:
- pass
- # adaptor = cast("View", view.backend_adaptor())
- # adaptor._pygfx_cam.set_viewport(self._viewport)
- # self._views.append(adaptor)
+ # This logic should go in the canvas node, I think
+ if view in self._views:
+ return
+ self._views.append(view)
+
+ view_adaptor = cast("View", get_adaptor(view))
+ view_adaptor._set_pygfx_canvas(
+ self._wgpu_canvas, self._canvas.width, self._canvas.height
+ )
def _snx_set_width(self, arg: int) -> None:
- _, height = cast("tuple[float, float]", self._wgpu_canvas.get_logical_size())
- self._wgpu_canvas.set_logical_size(arg, height)
+ width, height = cast(
+ "tuple[float, float]", self._wgpu_canvas.get_logical_size()
+ )
+ # FIXME: For some reason, on wx the size has already been updated, and
+ # updating it again causes erratic resizing behavior
+ if width != arg:
+ with app().block_events(self._snx_get_native()):
+ self._wgpu_canvas.set_logical_size(arg, height)
def _snx_set_height(self, arg: int) -> None:
- width, _ = cast("tuple[float, float]", self._wgpu_canvas.get_logical_size())
- self._wgpu_canvas.set_logical_size(width, arg)
+ width, height = cast(
+ "tuple[float, float]", self._wgpu_canvas.get_logical_size()
+ )
+ # FIXME: For some reason, on wx the size has already been updated, and
+ # updating it again causes erratic resizing behavior
+ if height != arg:
+ with app().block_events(self._snx_get_native()):
+ self._wgpu_canvas.set_logical_size(width, arg)
def _snx_set_background_color(self, arg: Color | None) -> None:
# not sure if pygfx has both a canavs and view background color...
diff --git a/src/scenex/adaptors/_pygfx/_image.py b/src/scenex/adaptors/_pygfx/_image.py
index cd06f402..e41c1190 100644
--- a/src/scenex/adaptors/_pygfx/_image.py
+++ b/src/scenex/adaptors/_pygfx/_image.py
@@ -24,15 +24,19 @@ class Image(Node, ImageAdaptor):
_pygfx_node: pygfx.Image
_material: pygfx.ImageBasicMaterial
+ _geometry: pygfx.Geometry
def __init__(self, image: model.Image, **backend_kwargs: Any) -> None:
- self._material = pygfx.ImageBasicMaterial(clim=image.clims)
- self._pygfx_node = pygfx.Image(None, self._material)
self._model = image
+ self._material = pygfx.ImageBasicMaterial(clim=image.clims)
self._snx_set_data(image.data)
+ self._pygfx_node = pygfx.Image(self._geometry, self._material)
def _snx_set_cmap(self, arg: Colormap) -> None:
- self._material.map = arg.to_pygfx()
+ if np.asarray(self._model.data).ndim == 3:
+ self._material.map = None
+ else:
+ self._material.map = arg.to_pygfx()
def _snx_set_clims(self, arg: tuple[float, float] | None) -> None:
self._material.clim = arg
@@ -64,4 +68,10 @@ def _create_texture(self, data: ArrayLike | None) -> pygfx.Texture:
def _snx_set_data(self, data: ArrayLike) -> None:
self._texture = self._create_texture(data)
- self._pygfx_node.geometry = pygfx.Geometry(grid=self._texture)
+ self._geometry = pygfx.Geometry(grid=self._texture)
+ if hasattr(self, "_pygfx_node"):
+ self._pygfx_node.geometry = self._geometry
+ if np.asarray(data).ndim == 3:
+ self._material.map = None
+ else:
+ self._material.map = self._model.cmap.to_pygfx()
diff --git a/src/scenex/adaptors/_pygfx/_node.py b/src/scenex/adaptors/_pygfx/_node.py
index 970ef297..f901a90b 100644
--- a/src/scenex/adaptors/_pygfx/_node.py
+++ b/src/scenex/adaptors/_pygfx/_node.py
@@ -1,8 +1,10 @@
from __future__ import annotations
import logging
-from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast
+import warnings
+from typing import TYPE_CHECKING, Generic, TypeVar, cast
+from scenex import model
from scenex.adaptors._base import NodeAdaptor, TNode
from ._adaptor_registry import get_adaptor
@@ -10,7 +12,6 @@
if TYPE_CHECKING:
import pygfx
- from scenex import model
from scenex.model import Transform
logger = logging.getLogger("scenex.adaptors.pygfx")
@@ -18,6 +19,12 @@
TMat = TypeVar("TMat", bound="pygfx.Material")
TGeo = TypeVar("TGeo", bound="pygfx.Geometry")
+BLEND_MODES = {
+ model.BlendMode.OPAQUE: "solid",
+ model.BlendMode.ALPHA: "auto",
+ model.BlendMode.ADDITIVE: "add",
+}
+
class Node(NodeAdaptor[TNode, TObj], Generic[TNode, TObj, TMat, TGeo]):
"""Node adaptor for pygfx Backend."""
@@ -27,9 +34,6 @@ class Node(NodeAdaptor[TNode, TObj], Generic[TNode, TObj, TMat, TGeo]):
_geometry: TGeo
_name: str
- def _snx_get_native(self) -> Any:
- return self._pygfx_node
-
def _snx_set_name(self, arg: str) -> None:
# not sure pygfx has a name attribute...
# TODO: for that matter... do we need a name attribute?
@@ -63,10 +67,25 @@ def _snx_set_transform(self, arg: Transform) -> None:
# pygfx uses a transposed matrix relative to the model
self._pygfx_node.local.matrix = arg.root.T
+ def _snx_set_blending(self, arg: model.BlendMode) -> None:
+ material = self._pygfx_node.material
+ if material is None:
+ # FIXME this node is a scene
+ return
+ # pygfx only supports node blend modes for versions >=0.13
+ if hasattr(material, "alpha_mode"):
+ material.alpha_mode = BLEND_MODES[arg] # pyright: ignore
+ else:
+ warnings.warn(
+ "Node blending not supported by this version of pygfx - ignoring",
+ RuntimeWarning,
+ stacklevel=2,
+ )
+
def _snx_add_node(self, node: model.Node) -> None:
# create if it doesn't exist
adaptor = cast("Node", get_adaptor(node))
- self._pygfx_node.add(adaptor._snx_get_native())
+ self._pygfx_node.add(adaptor._pygfx_node)
def _snx_force_update(self) -> None:
pass
diff --git a/src/scenex/adaptors/_pygfx/_text.py b/src/scenex/adaptors/_pygfx/_text.py
new file mode 100644
index 00000000..f568143d
--- /dev/null
+++ b/src/scenex/adaptors/_pygfx/_text.py
@@ -0,0 +1,43 @@
+"""Vispy Text adaptor for SceneX Text node."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any
+
+import pygfx
+
+from scenex.adaptors._base import TextAdaptor
+
+from ._node import Node
+
+if TYPE_CHECKING:
+ import cmap
+
+ from scenex.model import Text as TextModel
+
+
+class Text(Node, TextAdaptor):
+ """pygfx backend text adaptor."""
+
+ _material: pygfx.TextMaterial
+ _pygfx_node: pygfx.Text
+
+ def __init__(self, text: TextModel, **backend_kwargs: Any) -> None:
+ self._model = text
+ self._material = pygfx.TextMaterial(color=text.color.hex)
+ # create a pygfx Text visual
+ self._pygfx_node = pygfx.Text(
+ text=text.text,
+ material=self._material,
+ font_size=text.size,
+ screen_space=True,
+ )
+
+ def _snx_set_text(self, arg: str) -> None:
+ self._pygfx_node.set_text(arg)
+
+ def _snx_set_color(self, arg: cmap.Color) -> None:
+ self._material.color = arg.hex
+
+ def _snx_set_size(self, arg: int) -> None:
+ self._pygfx_node.font_size = arg
diff --git a/src/scenex/adaptors/_pygfx/_view.py b/src/scenex/adaptors/_pygfx/_view.py
index 8b278fad..1147d14f 100644
--- a/src/scenex/adaptors/_pygfx/_view.py
+++ b/src/scenex/adaptors/_pygfx/_view.py
@@ -15,7 +15,7 @@
from scenex import model
- from . import _camera, _canvas, _scene
+ from . import _camera, _scene
logger = logging.getLogger("scenex.adaptors.pygfx")
@@ -30,15 +30,17 @@ class View(ViewAdaptor):
_pygfx_cam: pygfx.Camera
def __init__(self, view: model.View, **backend_kwargs: Any) -> None:
- canvas_adaptor = cast("_canvas.Canvas", get_adaptor(view.canvas))
- wgpu_canvas = canvas_adaptor._snx_get_native()
- self._renderer = pygfx.renderers.WgpuRenderer(wgpu_canvas)
+ self._model = view
+ self._renderer: pygfx.renderers.WgpuRenderer | None = None
self._snx_set_scene(view.scene)
self._snx_set_camera(view.camera)
# TODO: this is needed... but breaks tests until we deal with Layout better.
# self._snx_set_background_color(view.layout.background_color)
+ def _set_pygfx_canvas(self, canvas: Any, x: int, y: int) -> None:
+ self._renderer = pygfx.renderers.WgpuRenderer(canvas)
+
def _snx_get_native(self) -> pygfx.Viewport:
return pygfx.Viewport(self._renderer)
@@ -52,18 +54,40 @@ def _snx_set_scene(self, scene: model.Scene) -> None:
def _snx_set_camera(self, cam: model.Camera) -> None:
self._cam_adaptor = cast("_camera.Camera", get_adaptor(cam))
self._pygfx_cam = self._cam_adaptor._pygfx_node
- self._cam_adaptor.pygfx_controller.register_events(self._renderer)
def _draw(self) -> None:
- renderer = self._renderer
- renderer.render(self._pygfx_scene, self._pygfx_cam)
- renderer.request_draw()
+ if self._renderer:
+ rect = self._model.layout.content_rect
+ # FIXME: On Qt, for HiDPI screens, the logical screen size (the rect
+ # variable above) can, through rounding error during resizing, become
+ # slightly larger than the physical size, which causes pygfx to error.
+ # This code "fixes" it but I think we could do better...maybe upstream?
+ ratio = self._renderer.physical_size[1] / self._renderer.logical_size[1] # pyright:ignore
+ if rect[2] * ratio > self._renderer.physical_size[0]:
+ # content rect is too wide for the canvas - adjust width
+ new_width = int(self._renderer.physical_size[0] / ratio)
+ rect = (rect[0], rect[1], new_width, rect[3])
+ if rect[3] * ratio > self._renderer.physical_size[1]:
+ # content rect is too tall for the canvas - adjust height
+ new_height = int(self._renderer.physical_size[1] / ratio)
+ rect = (rect[0], rect[1], rect[2], new_height)
+ # End FIXME
+
+ self._renderer.render(self._pygfx_scene, self._pygfx_cam, rect=rect)
+ self._renderer.request_draw()
def _snx_set_position(self, arg: tuple[float, float]) -> None:
logger.warning("View.set_position not implemented for pygfx")
def _snx_set_size(self, arg: tuple[float, float] | None) -> None:
- logger.warning("View.set_size not implemented for pygfx")
+ if arg is None:
+ logger.warning(
+ "Ignoring View.set_size(None): Don't know how to handle this..."
+ )
+ else:
+ r = self._snx_get_native().rect
+ self._snx_get_native().rect = (r[0], r[1], arg[0], arg[1])
+ # FIXME: Camera projection transform should also be updated...
def _snx_set_background_color(self, color: Color | None) -> None:
colors = (color.rgba,) if color is not None else ()
diff --git a/src/scenex/adaptors/_pygfx/_volume.py b/src/scenex/adaptors/_pygfx/_volume.py
index 91e7ba79..f46ec842 100644
--- a/src/scenex/adaptors/_pygfx/_volume.py
+++ b/src/scenex/adaptors/_pygfx/_volume.py
@@ -56,6 +56,8 @@ def _create_texture(self, data: np.ndarray) -> pygfx.Texture:
def _snx_set_data(self, data: ArrayLike) -> None:
self._texture = self._create_texture(np.asanyarray(data))
self._geometry = pygfx.Geometry(grid=self._texture)
+ if hasattr(self, "_pygfx_node"):
+ self._pygfx_node.geometry = self._geometry
def _snx_set_render_mode(
self,
@@ -67,6 +69,13 @@ def _snx_set_render_mode(
kwargs["interpolation"] = interpolation
elif self._material is not None:
kwargs["interpolation"] = self._material.interpolation
+ kwargs["clim"] = self._material.clim
+ kwargs["map"] = self._material.map
+ kwargs["gamma"] = self._material.gamma
+ kwargs["opacity"] = self._material.opacity
+ # alpha_mode parameter introduced in pygfx 0.13.0
+ if hasattr(self._material, "alpha_mode"):
+ kwargs["alpha_mode"] = self._material.alpha_mode
if data == "mip":
self._material = pygfx.VolumeMipMaterial(**kwargs)
diff --git a/src/scenex/adaptors/_registry.py b/src/scenex/adaptors/_registry.py
index 7915efca..1c9acac8 100644
--- a/src/scenex/adaptors/_registry.py
+++ b/src/scenex/adaptors/_registry.py
@@ -33,10 +33,6 @@ class AdaptorRegistry:
def __init__(self) -> None:
self._objects: dict[str, _base.Adaptor] = {}
- def start_event_loop(self) -> None:
- """Run the Application event loop."""
- raise NotImplementedError("This method should be implemented by the backend.")
-
def all(self) -> Iterator[_base.Adaptor]:
"""Return an iterator over all adaptors in the registry."""
yield from self._objects.values()
diff --git a/src/scenex/adaptors/_vispy/__init__.py b/src/scenex/adaptors/_vispy/__init__.py
index 1277e7ce..5892c284 100644
--- a/src/scenex/adaptors/_vispy/__init__.py
+++ b/src/scenex/adaptors/_vispy/__init__.py
@@ -7,6 +7,7 @@
from ._node import Node
from ._points import Points
from ._scene import Scene
+from ._text import Text
from ._view import View
from ._volume import Volume
@@ -17,6 +18,7 @@
"Node",
"Points",
"Scene",
+ "Text",
"View",
"Volume",
"adaptors",
diff --git a/src/scenex/adaptors/_vispy/_adaptor_registry.py b/src/scenex/adaptors/_vispy/_adaptor_registry.py
index ed2b5b9d..ff33ab48 100644
--- a/src/scenex/adaptors/_vispy/_adaptor_registry.py
+++ b/src/scenex/adaptors/_vispy/_adaptor_registry.py
@@ -4,11 +4,6 @@
class VispyAdaptorRegistry(AdaptorRegistry):
- def start_event_loop(self) -> None:
- from vispy import app
-
- app.run()
-
def get_adaptor_class(self, obj: Any) -> type:
from scenex.adaptors import _vispy
diff --git a/src/scenex/adaptors/_vispy/_camera.py b/src/scenex/adaptors/_vispy/_camera.py
index c5195ae3..f310de68 100644
--- a/src/scenex/adaptors/_vispy/_camera.py
+++ b/src/scenex/adaptors/_vispy/_camera.py
@@ -2,16 +2,16 @@
from typing import TYPE_CHECKING, Any
-import numpy as np
+import vispy.geometry
import vispy.scene
from scenex.adaptors._base import CameraAdaptor
+from scenex.model import Transform
from ._node import Node
if TYPE_CHECKING:
from scenex import model
- from scenex.model import Transform
class Camera(Node, CameraAdaptor):
@@ -21,37 +21,65 @@ class Camera(Node, CameraAdaptor):
def __init__(self, camera: model.Camera, **backend_kwargs: Any) -> None:
self._camera_model = camera
- if camera.type == "panzoom":
- self._vispy_node = vispy.scene.PanZoomCamera()
- self._vispy_node.interactive = True
- elif camera.type == "perspective":
- # TODO: These settings were copied from the pygfx camera.
- # Unify these values?
- self._vispy_node = vispy.scene.ArcballCamera(70)
- self._snx_zoom_to_fit(0.1)
-
- def _snx_set_zoom(self, zoom: float) -> None:
- self._vispy_node.zoom_factor = zoom
-
- def _snx_set_center(self, arg: tuple[float, ...]) -> None:
- self._vispy_node.center = arg
+ # The camera model contains:
+ # A projection transform, mapping local space to NDC
+ # A parent transform, mapping parent space to local space
+ #
+ # TODO: We may need a utility to get a transform mapping world space to local
+ # space.
+ #
+ # The BaseCamera.transform field should map world space to canvas position.
+ #
+ # To construct this transform from our camera model, we need:
+ # 1) A transform from world space to local space:
+ # Note that this is usually the inverse of the model's transform matrix
+ self._transform = Transform()
+ # 2) A transform from local space to NDC:
+ self._projection = Transform()
+ # 3) A transform from NDC to canvas position:
+ self._from_NDC = Transform()
+
+ self._vispy_node = vispy.scene.BaseCamera()
+
+ def _set_view(self, w: float, h: float) -> None:
+ # FIXME: For some reason, width can quickly become zero in NDV when switching
+ # channel mode...
+ if w == 0 or h == 0:
+ return
+ # map [-1, -1] to [0, h]
+ # map [1, 1] to [w, 0]
+ self._from_NDC = Transform().translated((1, -1)).scaled((w / 2, -h / 2, 1))
+
+ self._update_vispy_node_tform()
def _snx_set_type(self, arg: model.CameraType) -> None:
raise NotImplementedError()
def _snx_set_transform(self, arg: Transform) -> None:
- if isinstance(self._vispy_node, vispy.scene.PanZoomCamera):
- self._vispy_node.tf_mat = vispy.scene.transforms.MatrixTransform(
- np.asarray(arg)
- )
- else:
- super()._snx_set_transform(arg)
+ # Note that the scenex transform is inverted here.
+ # Scenex transforms map local coordinates into parent coordinates,
+ # but our vispy node's transform must go the opposite way, from world
+ # coordinates into parent coordinates.
+ #
+ # FIXME: Note the discrepancy between world and parent coordinates. World
+ # coordinates are needed for the vispy transform node, but the current transform
+ # only converts local to parent space. This will likely be a source of bugs for
+ # more complicated scenes. There's also a TODO above about fixing this.
+ self._transform = arg.inv()
+ self._update_vispy_node_tform()
+
+ def _snx_set_projection(self, arg: Transform) -> None:
+ self._projection = arg
+ # Recompute the vispy node transform since it's a product of multiple scenex
+ # Transforms
+ self._update_vispy_node_tform()
+
+ def _update_vispy_node_tform(self) -> None:
+ mat = self._transform @ self._projection.T @ self._from_NDC
+ self._vispy_node.transform = vispy.scene.transforms.MatrixTransform(mat.root)
+ self._vispy_node.view_changed()
def _view_size(self) -> tuple[float, float] | None:
"""Return the size of first parent viewbox in pixels."""
raise NotImplementedError
-
- def _snx_zoom_to_fit(self, margin: float) -> None:
- # reset camera to fit all objects
- self._vispy_node.set_range()
diff --git a/src/scenex/adaptors/_vispy/_canvas.py b/src/scenex/adaptors/_vispy/_canvas.py
index 8f0049b8..da1753ba 100644
--- a/src/scenex/adaptors/_vispy/_canvas.py
+++ b/src/scenex/adaptors/_vispy/_canvas.py
@@ -5,6 +5,7 @@
import numpy as np
from scenex.adaptors._base import CanvasAdaptor
+from scenex.app import app
from ._adaptor_registry import get_adaptor
@@ -14,6 +15,8 @@
from scenex import model
+ from ._view import View
+
class SupportsHideShow(BaseRenderCanvas):
def show(self) -> None: ...
def hide(self) -> None: ...
@@ -27,7 +30,7 @@ class Canvas(CanvasAdaptor):
"""Canvas interface for vispy Backend."""
def __init__(self, canvas: model.Canvas, **backend_kwargs: Any) -> None:
- from vispy.scene import Grid, SceneCanvas
+ from vispy.scene import Grid, SceneCanvas, VisualNode
self._canvas = SceneCanvas(
title=canvas.title, size=(canvas.width, canvas.height)
@@ -36,29 +39,36 @@ def __init__(self, canvas: model.Canvas, **backend_kwargs: Any) -> None:
if supports_hide_show(self._canvas.native):
self._canvas.native.hide()
self._grid = cast("Grid", self._canvas.central_widget.add_grid())
+ self._views: list[model.View] = []
for view in canvas.views:
self._snx_add_view(view)
- self._views = canvas.views
+ self._filter = app().install_event_filter(self._canvas.native, canvas)
+
+ self._visual_to_node: dict[VisualNode, model.Node | None] = {}
+ self._last_canvas_pos: tuple[float, float] | None = None
def _snx_get_native(self) -> Any:
return self._canvas.native
def _snx_set_visible(self, arg: bool) -> None:
# show the qt canvas we patched earlier in __init__
- if supports_hide_show(self._canvas.native):
- self._canvas.show()
+ app().show(self, arg)
def _draw(self) -> None:
self._canvas.update()
def _snx_add_view(self, view: model.View) -> None:
- self._grid.add_widget(get_adaptor(view)._snx_get_native())
+ if view in self._views:
+ return
+
+ self._grid.add_widget(cast("View", get_adaptor(view))._vispy_viewbox)
+ get_adaptor(view.camera)._set_view(view.layout.width, view.layout.height) # type:ignore
def _snx_set_width(self, arg: int) -> None:
- self._canvas.size = (self._canvas.size[0], arg)
+ self._canvas.size = (arg, self._canvas.size[1])
def _snx_set_height(self, arg: int) -> None:
- self._canvas.size = (arg, self._canvas.size[1])
+ self._canvas.size = (self._canvas.size[0], arg)
def _snx_set_background_color(self, arg: Color | None) -> None:
if arg is None:
diff --git a/src/scenex/adaptors/_vispy/_image.py b/src/scenex/adaptors/_vispy/_image.py
index 02f5c438..5bf59f9a 100644
--- a/src/scenex/adaptors/_vispy/_image.py
+++ b/src/scenex/adaptors/_vispy/_image.py
@@ -36,15 +36,20 @@ def _snx_set_transform(self, arg: Transform) -> None:
def _snx_set_cmap(self, arg: Colormap) -> None:
self._vispy_node.cmap = arg.to_vispy()
+ self._vispy_node.update()
def _snx_set_clims(self, arg: tuple[float, float] | None) -> None:
self._vispy_node.clim = arg
+ self._vispy_node.update()
def _snx_set_gamma(self, arg: float) -> None:
self._vispy_node.gamma = arg
+ self._vispy_node.update()
def _snx_set_interpolation(self, arg: model.InterpolationMode) -> None:
self._vispy_node.interpolation = arg
+ self._vispy_node.update()
def _snx_set_data(self, data: ArrayLike) -> None:
self._vispy_node.set_data(data)
+ self._vispy_node.update()
diff --git a/src/scenex/adaptors/_vispy/_node.py b/src/scenex/adaptors/_vispy/_node.py
index 2ac88656..56131a99 100644
--- a/src/scenex/adaptors/_vispy/_node.py
+++ b/src/scenex/adaptors/_vispy/_node.py
@@ -5,26 +5,29 @@
import numpy as np
import vispy.scene
+from scenex import model
from scenex.adaptors._base import NodeAdaptor, TNode
from ._adaptor_registry import get_adaptor
if TYPE_CHECKING:
- from scenex import model
from scenex.model import Transform
TObj = TypeVar("TObj", bound="vispy.scene.Node")
+BLEND_MODES = {
+ model.BlendMode.OPAQUE: "opaque",
+ model.BlendMode.ALPHA: "translucent",
+ model.BlendMode.ADDITIVE: "additive",
+}
+
class Node(NodeAdaptor[TNode, TObj], Generic[TNode, TObj]):
"""Node adaptor for pygfx Backend."""
_vispy_node: TObj
- def _snx_get_native(self) -> Any:
- return self._vispy_node
-
def _snx_set_name(self, arg: str) -> None:
self._vispy_node.name = arg
@@ -59,10 +62,18 @@ def _snx_set_transform(self, arg: Transform) -> None:
np.asarray(arg)
)
+ def _snx_set_blending(self, arg: model.BlendMode) -> None:
+ if hasattr(self._vispy_node, "set_gl_state"):
+ if arg == model.BlendMode.OPAQUE:
+ # for opaque, we need to disable blending
+ self._vispy_node.set_gl_state(None, blend=False) # pyright: ignore
+ else:
+ self._vispy_node.set_gl_state(BLEND_MODES[arg]) # pyright: ignore
+
def _snx_add_node(self, node: model.Node) -> None:
# create if it doesn't exist
adaptor = cast("Node", get_adaptor(node))
- adaptor._snx_get_native().parent = self._vispy_node
+ adaptor._vispy_node.parent = self._vispy_node
def _snx_force_update(self) -> None:
pass
diff --git a/src/scenex/adaptors/_vispy/_points.py b/src/scenex/adaptors/_vispy/_points.py
index e5f19d5f..45d59fad 100644
--- a/src/scenex/adaptors/_vispy/_points.py
+++ b/src/scenex/adaptors/_vispy/_points.py
@@ -38,6 +38,7 @@ def __init__(self, points: model.Points, **backend_kwargs: Any) -> None:
self._model = points
self._vispy_node = vispy.scene.Markers(
pos=np.asarray(points.coords),
+ size=points.size,
symbol=points.symbol,
scaling=points.scaling, # pyright: ignore
antialias=points.antialias, # pyright: ignore
@@ -96,3 +97,4 @@ def _update_vispy_data(self) -> None:
edge_color=edge_color,
edge_width=self._model.edge_width,
)
+ self._vispy_node.update()
diff --git a/src/scenex/adaptors/_vispy/_text.py b/src/scenex/adaptors/_vispy/_text.py
new file mode 100644
index 00000000..2e0d58e7
--- /dev/null
+++ b/src/scenex/adaptors/_vispy/_text.py
@@ -0,0 +1,44 @@
+"""Vispy Text adaptor for SceneX Text node."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any
+
+import vispy.visuals
+from vispy import scene
+
+from scenex.adaptors._base import TextAdaptor
+
+from ._node import Node
+
+if TYPE_CHECKING:
+ import cmap
+
+ from scenex.model import BlendMode
+ from scenex.model import Text as TextModel
+
+
+class Text(Node, TextAdaptor):
+ """vispy backend text adaptor."""
+
+ _vispy_node: vispy.visuals.TextVisual
+
+ def __init__(self, text: TextModel, **backend_kwargs: Any) -> None:
+ self._model = text
+ # create a vispy Text visual
+ self._vispy_node = scene.Text(
+ text=text.text, color=text.color.hex, font_size=text.size
+ )
+
+ def _snx_set_text(self, arg: str) -> None:
+ self._vispy_node.text = arg
+
+ def _snx_set_color(self, arg: cmap.Color) -> None:
+ self._vispy_node.color = arg.hex
+
+ def _snx_set_size(self, arg: int) -> None:
+ self._vispy_node.font_size = arg
+
+ def _snx_set_blending(self, arg: BlendMode) -> None:
+ # Blending text makes it look very blocky
+ pass
diff --git a/src/scenex/adaptors/_vispy/_view.py b/src/scenex/adaptors/_vispy/_view.py
index 50e0ff2e..7b3b0f53 100644
--- a/src/scenex/adaptors/_vispy/_view.py
+++ b/src/scenex/adaptors/_vispy/_view.py
@@ -6,9 +6,9 @@
import numpy as np
import vispy
import vispy.app
-import vispy.color
import vispy.scene
import vispy.scene.subscene
+from vispy.geometry import Rect
from scenex.adaptors._base import ViewAdaptor
@@ -33,11 +33,20 @@ class View(ViewAdaptor):
_vispy_camera: vispy.scene.BaseCamera
def __init__(self, view: model.View, **backend_kwargs: Any) -> None:
+ self._model = view
self._vispy_viewbox = vispy.scene.ViewBox()
self._snx_set_camera(view.camera)
self._snx_set_scene(view.scene)
+ view.layout.events.all.connect(self._on_layout_changed)
+
+ def _on_layout_changed(self, event: Any) -> None:
+ rect = Rect(self._model.layout.content_rect)
+ self._vispy_viewbox.rect = rect
+ self._vispy_viewbox.update()
+ self._cam_adaptor._set_view(rect.width, rect.height)
+
def _snx_get_native(self) -> Any:
return self._vispy_viewbox
@@ -71,6 +80,10 @@ def _snx_set_camera(self, cam: model.Camera) -> None:
self._vispy_camera = self._cam_adaptor._vispy_node
if hasattr(self, "_vispy_viewbox"):
self._vispy_viewbox.camera = self._vispy_camera
+ # Vispy camera transforms need knowledge of viewbox
+ # (specifically, its size)
+ self._vispy_viewbox.update()
+ self._cam_adaptor._set_view(*self._vispy_viewbox.size)
def _draw(self) -> None:
self._vispy_viewbox.update()
diff --git a/src/scenex/app/__init__.py b/src/scenex/app/__init__.py
new file mode 100644
index 00000000..fecfeb04
--- /dev/null
+++ b/src/scenex/app/__init__.py
@@ -0,0 +1,8 @@
+"""The Scenex App Abstraction."""
+
+from ._auto import App, GuiFrontend, app, determine_app
+
+# Note that this package is designed to fully encapsulate app logic.
+# It is designed to be cleanly extractable to a separate library if needed.
+
+__all__ = ["App", "GuiFrontend", "app", "determine_app"]
diff --git a/src/scenex/app/_auto.py b/src/scenex/app/_auto.py
new file mode 100644
index 00000000..c2716eee
--- /dev/null
+++ b/src/scenex/app/_auto.py
@@ -0,0 +1,190 @@
+from __future__ import annotations
+
+import importlib
+import os
+import sys
+from concurrent.futures import Executor, Future, ThreadPoolExecutor
+from contextlib import contextmanager
+from enum import Enum
+from functools import cache
+from typing import TYPE_CHECKING, cast
+
+if TYPE_CHECKING:
+ from collections.abc import Callable, Iterator
+ from typing import Any
+
+ from typing_extensions import ParamSpec, TypeVar
+
+ from scenex.adaptors._base import CanvasAdaptor
+ from scenex.app.events._events import EventFilter
+ from scenex.model import Canvas
+
+ T = TypeVar("T")
+ P = ParamSpec("P")
+
+
+GUI_ENV_VAR = "SCENEX_WIDGET_BACKEND"
+"""Preferred GUI frontend. If not set, the first available GUI frontend is used."""
+_APP: App | None = None
+"""Singleton instance of the current (GUI) application. Once set it shouldn't change."""
+
+
+class GuiFrontend(str, Enum):
+ """Enum of available GUI frontends.
+
+ Attributes
+ ----------
+ JUPYTER : str
+ [JUPYTER](https://jupyter.org/)
+ QT : str
+ [PyQt5/PySide2/PyQt6/PySide6](https://doc.qt.io)
+ WX : str
+ [WX](https://wxpython.org/)
+ """
+
+ JUPYTER = "jupyter"
+ QT = "qt"
+ WX = "wx"
+
+
+GUI_PROVIDERS: dict[GuiFrontend, tuple[str, str]] = {
+ GuiFrontend.WX: ("scenex.app._wx", "WxAppWrap"),
+ GuiFrontend.QT: ("scenex.app._qt", "QtAppWrap"),
+ # Note that Jupyter should go last because it is a guess based on IPython
+ # which may be installed with the other frameworks as well.
+ GuiFrontend.JUPYTER: ("scenex.app._jupyter", "JupyterAppWrap"),
+}
+
+
+class App:
+ """
+ Base class for application wrappers.
+
+ TODO: Where should this live? Probably doesn't belong in this repo...
+ """
+
+ def create_app(self) -> Any:
+ """Create the application instance, if not already created."""
+ raise NotImplementedError("Must be implemented by subclasses.")
+
+ def run(self) -> None:
+ """Run the application."""
+ raise NotImplementedError("Must be implemented by subclasses.")
+
+ def show(self, canvas: CanvasAdaptor, visible: bool) -> None:
+ """Show or hide the canvas."""
+ raise NotImplementedError("Must be implemented by subclasses.")
+
+ def install_event_filter(self, canvas: Any, model_canvas: Canvas) -> EventFilter:
+ raise NotImplementedError("Must be implemented by subclasses.")
+
+ def process_events(self) -> None:
+ """Process events."""
+ raise NotImplementedError("Must be implemented by subclasses.")
+
+ def call_in_main_thread(
+ self, func: Callable[P, T], *args: P.args, **kwargs: P.kwargs
+ ) -> Future[T]:
+ """Call `func` in the main gui thread."""
+ future: Future[T] = Future()
+ future.set_result(func(*args, **kwargs))
+ return future
+
+ def call_later(self, msec: int, func: Callable[[], None]) -> None:
+ """Call `func` after `msec` milliseconds."""
+ raise NotImplementedError("Must be implemented by subclasses.")
+
+ def get_executor(self) -> Executor:
+ """Return an executor for running tasks in the background."""
+ return _thread_pool_executor()
+
+ @contextmanager
+ def block_events(self, window: Any) -> Iterator[None]:
+ """Context manager to block events for a window."""
+ raise NotImplementedError("Must be implemented by subclasses.")
+
+
+@cache
+def _thread_pool_executor() -> ThreadPoolExecutor:
+ return ThreadPoolExecutor(max_workers=2)
+
+
+def _running_apps() -> Iterator[GuiFrontend]:
+ """Return an iterator of running GUI applications."""
+ for mod_name in ("PyQt5", "PySide2", "PySide6", "PyQt6"):
+ if mod := sys.modules.get(f"{mod_name}.QtWidgets"):
+ print(f"Found {mod}")
+ if (
+ qapp := getattr(mod, "QApplication", None)
+ ) and qapp.instance() is not None:
+ yield GuiFrontend.QT
+ # wx
+ if (wx := sys.modules.get("wx")) and wx.App.Get() is not None:
+ yield GuiFrontend.WX
+
+ # Jupyter notebook
+ if (ipy := sys.modules.get("IPython")) and (shell := ipy.get_ipython()):
+ if shell.__class__.__name__ == "ZMQInteractiveShell":
+ yield GuiFrontend.JUPYTER
+
+
+def _load_app(module: str, cls_name: str) -> App:
+ mod = importlib.import_module(module)
+ cls = getattr(mod, cls_name)
+ return cast("App", cls())
+
+
+def determine_app() -> GuiFrontend:
+ """Returns the [`GuiFrontend`][scenex.app.GuiFrontend].
+
+ This is determined first by the `NDV_GUI_FRONTEND` environment variable, after which
+ known GUI providers are tried in order until one is found that is either already
+ running, or available.
+ """
+ running = list(_running_apps())
+
+ # Try 1: Load a frontend explicitly requested by the user
+ requested = os.getenv(GUI_ENV_VAR, "").lower()
+ valid = {x.value for x in GuiFrontend}
+ if requested:
+ if requested not in valid:
+ raise ValueError(
+ f"Invalid GUI frontend: {requested!r}. Valid options: {valid}"
+ )
+ return GuiFrontend(requested)
+
+ # Try 2: Utilize an existing, running app
+ for key in GUI_PROVIDERS.keys():
+ if key in running:
+ return key
+
+ # Try 3: Load an existing app
+ errors: list[tuple[str, BaseException]] = []
+ for key, provider in GUI_PROVIDERS.items():
+ try:
+ _load_app(*provider)
+ return key
+ except Exception as e:
+ errors.append((key, e))
+
+ raise RuntimeError( # pragma: no cover
+ f"Could not find an appropriate GUI frontend: {valid!r}. Tried:\n\n"
+ + "\n".join(f"- {key}: {err}" for key, err in errors)
+ )
+
+
+def app() -> App:
+ """Return the active [`GuiFrontend`][ndv.views.GuiFrontend].
+
+ This is determined first by the `NDV_GUI_FRONTEND` environment variable, after which
+ known GUI providers are tried in order until one is found that is either already
+ running, or available.
+ """
+ global _APP
+ if _APP is not None:
+ return _APP
+
+ # ensure the app is created for explicitly requested frontends
+ _APP = _load_app(*GUI_PROVIDERS[determine_app()])
+ _APP.create_app()
+ return _APP
diff --git a/src/scenex/app/_jupyter.py b/src/scenex/app/_jupyter.py
new file mode 100644
index 00000000..940038c4
--- /dev/null
+++ b/src/scenex/app/_jupyter.py
@@ -0,0 +1,223 @@
+from __future__ import annotations
+
+from contextlib import contextmanager
+from types import MethodType
+from typing import TYPE_CHECKING, Any, cast
+
+from IPython import display
+from jupyter_rfb import RemoteFrameBuffer
+
+from scenex.app._auto import App
+from scenex.app.events._events import (
+ EventFilter,
+ MouseButton,
+ MouseDoublePressEvent,
+ MouseEnterEvent,
+ MouseLeaveEvent,
+ MouseMoveEvent,
+ MousePressEvent,
+ MouseReleaseEvent,
+ ResizeEvent,
+ WheelEvent,
+)
+
+if TYPE_CHECKING:
+ from collections.abc import Callable, Iterator
+
+ from scenex import Canvas
+ from scenex.adaptors._base import CanvasAdaptor
+
+
+class JupyterEventFilter(EventFilter):
+ def __init__(self, canvas: Any, model_canvas: Canvas) -> None:
+ if not isinstance(canvas, RemoteFrameBuffer):
+ raise TypeError(
+ f"Expected canvas to be RemoteFrameBuffer, got {type(canvas)}"
+ )
+ self._canvas = canvas
+ self._model_canvas = model_canvas
+ self._active_button: MouseButton = MouseButton.NONE
+
+ self._old_event = self._canvas.handle_event
+
+ def _create_handler(
+ filter: JupyterEventFilter,
+ ) -> Callable[[RemoteFrameBuffer, dict], None]:
+ def _handle_event(self: RemoteFrameBuffer, ev: dict) -> None:
+ etype = ev["event_type"]
+ if etype == "pointer_move":
+ filter._active_button = MouseButton.NONE
+ if btn := ev.get("button", None):
+ filter._active_button |= JupyterEventFilter.mouse_btn(btn)
+ elif btns := ev.get("buttons", None):
+ for b in btns:
+ filter._active_button |= JupyterEventFilter.mouse_btn(b)
+ canvas_pos = (ev["x"], ev["y"])
+ if world_ray := filter._model_canvas.to_world(canvas_pos):
+ filter._model_canvas.handle(
+ MouseMoveEvent(
+ canvas_pos=canvas_pos,
+ world_ray=world_ray,
+ buttons=filter._active_button,
+ )
+ )
+ elif etype == "pointer_down":
+ canvas_pos = (ev["x"], ev["y"])
+ btn = JupyterEventFilter.mouse_btn(ev["button"])
+ filter._active_button |= btn
+ if world_ray := filter._model_canvas.to_world(canvas_pos):
+ filter._model_canvas.handle(
+ MousePressEvent(
+ canvas_pos=canvas_pos,
+ world_ray=world_ray,
+ buttons=btn,
+ )
+ )
+ elif etype == "double_click":
+ btn = JupyterEventFilter.mouse_btn(ev["button"])
+ canvas_pos = (ev["x"], ev["y"])
+ if world_ray := filter._model_canvas.to_world(canvas_pos):
+ # FIXME: in Jupyter, a double_click event is not a pointer
+ # event. In other words, there will be no release following.
+ # This could cause unintended behavior. See
+ # https://github.com/vispy/jupyter_rfb/blob/62831dd5a87bc19b4fd5f921d802ed21141e61ec/js/lib/widget.js#L270
+ filter._model_canvas.handle(
+ MouseDoublePressEvent(
+ canvas_pos=canvas_pos,
+ world_ray=world_ray,
+ buttons=btn,
+ )
+ )
+ elif etype == "pointer_up":
+ canvas_pos = (ev["x"], ev["y"])
+ btn = JupyterEventFilter.mouse_btn(ev["button"])
+ filter._active_button &= ~btn
+ if world_ray := filter._model_canvas.to_world(canvas_pos):
+ filter._model_canvas.handle(
+ MouseReleaseEvent(
+ canvas_pos=canvas_pos,
+ world_ray=world_ray,
+ buttons=btn,
+ )
+ )
+ elif etype == "pointer_enter":
+ canvas_pos = (ev["x"], ev["y"])
+ filter._active_button = MouseButton.NONE
+ if btn := ev.get("button", None):
+ filter._active_button |= JupyterEventFilter.mouse_btn(btn)
+ elif btns := ev.get("buttons", None):
+ for b in btns:
+ filter._active_button |= JupyterEventFilter.mouse_btn(b)
+ if world_ray := filter._model_canvas.to_world(canvas_pos):
+ filter._model_canvas.handle(
+ MouseEnterEvent(
+ canvas_pos=canvas_pos,
+ world_ray=world_ray,
+ buttons=filter._active_button,
+ )
+ )
+ elif etype == "pointer_leave":
+ filter._model_canvas.handle(MouseLeaveEvent())
+ elif etype == "wheel":
+ canvas_pos = (ev["x"], ev["y"])
+ if world_ray := filter._model_canvas.to_world(canvas_pos):
+ filter._model_canvas.handle(
+ WheelEvent(
+ canvas_pos=canvas_pos,
+ world_ray=world_ray,
+ buttons=filter._active_button,
+ # Note that Jupyter_rfb uses a different y convention
+ angle_delta=(ev["dx"], -ev["dy"]),
+ )
+ )
+ elif etype == "resize":
+ filter._model_canvas.handle(
+ ResizeEvent(
+ width=ev["width"],
+ height=ev["height"],
+ )
+ )
+
+ return _handle_event
+
+ self._canvas.handle_event = MethodType(_create_handler(self), self._canvas)
+
+ @classmethod
+ def mouse_btn(cls, btn: Any) -> MouseButton:
+ if btn == 0:
+ return MouseButton.NONE
+ if btn == 1:
+ return MouseButton.LEFT
+ if btn == 2:
+ return MouseButton.RIGHT
+ if btn == 3:
+ return MouseButton.MIDDLE
+
+ raise Exception(f"Jupyter mouse button {btn} is unknown")
+
+ def uninstall(self) -> None:
+ self._canvas.handle_event = self._old_event
+
+
+class JupyterAppWrap(App):
+ """Provider for Jupyter notebook."""
+
+ def __init__(self) -> None:
+ self._visible_canvases: set[CanvasAdaptor] = set()
+
+ # def is_running(self) -> bool:
+ # if ipy_shell := self._ipython_shell():
+ # return bool(ipy_shell.__class__.__name__ == "ZMQInteractiveShell")
+ # return False
+
+ def create_app(self) -> Any:
+ # if not self.is_running() and not os.getenv("PYTEST_CURRENT_TEST"):
+ # # if we got here, it probably means that someone used
+ # # NDV_GUI_FRONTEND=jupyter without actually being in a jupyter notebook
+ # # we allow it in tests, but not in normal usage.
+ # raise RuntimeError( # pragma: no cover
+ # "Jupyter is not running a notebook shell. Cannot create app."
+ # )
+
+ # No app creation needed...
+ # but make sure we can actually import the stuff we need
+ import ipywidgets # noqa: F401
+ import jupyter # noqa: F401
+
+ def run(self) -> None:
+ """Run the application."""
+ # No explicit run method needed for Jupyter
+ pass
+
+ def install_event_filter(self, canvas: Any, model_canvas: Canvas) -> EventFilter:
+ return JupyterEventFilter(canvas, model_canvas)
+
+ def show(self, canvas: CanvasAdaptor, visible: bool) -> None:
+ native_canvas = cast("RemoteFrameBuffer", canvas._snx_get_native())
+ if canvas not in self._visible_canvases:
+ self._visible_canvases.add(canvas)
+ display.display(native_canvas)
+ native_canvas.layout.display = "flex" if visible else "none"
+
+ def process_events(self) -> None:
+ """Process events for the application."""
+ pass
+
+ def call_later(self, msec: int, func: Callable[[], None]) -> None:
+ """Call `func` after `msec` milliseconds."""
+ # generic implementation using python threading
+
+ from threading import Timer
+
+ Timer(msec / 1000, func).start()
+
+ @contextmanager
+ def block_events(self, window: Any) -> Iterator[None]:
+ """Context manager to block events for a window."""
+ if hasattr(window, "handle_event"):
+ old = window.handle_event
+ window.handle_event = lambda *args, **kwargs: None
+ yield
+ window.handle_event = old
+ else:
+ yield
diff --git a/src/scenex/app/_qt.py b/src/scenex/app/_qt.py
new file mode 100644
index 00000000..ce2ab5ac
--- /dev/null
+++ b/src/scenex/app/_qt.py
@@ -0,0 +1,258 @@
+from __future__ import annotations
+
+import sys
+from concurrent.futures import Future
+from contextlib import contextmanager
+from typing import TYPE_CHECKING, Any, ClassVar, cast
+
+from qtpy.QtCore import (
+ QCoreApplication,
+ QEvent,
+ QMetaObject,
+ QObject,
+ Qt,
+ QThread,
+ QTimer,
+)
+from qtpy.QtGui import QEnterEvent, QMouseEvent, QResizeEvent, QWheelEvent
+from qtpy.QtWidgets import QApplication, QWidget
+from superqt.utils import signals_blocked
+
+from scenex.app._auto import App
+from scenex.app.events import (
+ EventFilter,
+ MouseButton,
+ MouseDoublePressEvent,
+ MouseEnterEvent,
+ MouseLeaveEvent,
+ MouseMoveEvent,
+ MousePressEvent,
+ MouseReleaseEvent,
+ ResizeEvent,
+ WheelEvent,
+)
+
+# NOTE: PyQt and PySide have different names for the Slot decorator
+# but they're more or less interchangeable
+try:
+ from qtpy.QtCore import Slot as slot # type: ignore[attr-defined]
+except ImportError:
+ try:
+ from qtpy.QtCore import pyqtSlot as slot
+ except ImportError as e:
+ raise Exception("Could not import Slot or pyqtSlot from qtpy.QtCore") from e
+
+
+if TYPE_CHECKING:
+ from collections.abc import Callable, Iterator
+ from typing import Any
+
+ from scenex import Canvas
+ from scenex.adaptors._base import CanvasAdaptor
+ from scenex.app._auto import P, T
+ from scenex.app.events import Event
+
+
+class QtEventFilter(QObject, EventFilter):
+ def __init__(self, canvas: Any, model_canvas: Canvas) -> None:
+ super().__init__()
+ self._canvas = canvas
+ self._model_canvas = model_canvas
+ self._active_buttons: MouseButton = MouseButton.NONE
+
+ def eventFilter(self, a0: QObject | None = None, a1: QEvent | None = None) -> bool:
+ if isinstance(a0, QWidget) and isinstance(a1, QEvent):
+ if evt := self._convert_event(a1):
+ return self._model_canvas.handle(evt)
+ return False
+
+ def uninstall(self) -> None:
+ self._canvas.removeEventFilter(self)
+
+ def mouse_btn(self, btn: Any) -> MouseButton:
+ if btn == Qt.MouseButton.LeftButton:
+ return MouseButton.LEFT
+ if btn == Qt.MouseButton.RightButton:
+ return MouseButton.RIGHT
+ if btn == Qt.MouseButton.NoButton:
+ return MouseButton.NONE
+
+ raise Exception(f"Qt mouse button {btn} is unknown")
+
+ def _convert_event(self, qevent: QEvent) -> Event | None:
+ """Convert a QEvent to a SceneX Event."""
+ if isinstance(qevent, QMouseEvent | QEnterEvent):
+ pos = qevent.position()
+ canvas_pos = (pos.x(), pos.y())
+ if not (ray := self._model_canvas.to_world(canvas_pos)):
+ return None
+
+ etype = qevent.type()
+ btn = self.mouse_btn(qevent.button())
+ if etype == QEvent.Type.MouseMove:
+ return MouseMoveEvent(
+ canvas_pos=canvas_pos,
+ world_ray=ray,
+ buttons=self._active_buttons,
+ )
+ elif etype == QEvent.Type.MouseButtonDblClick:
+ self._active_buttons |= btn
+ return MouseDoublePressEvent(
+ canvas_pos=canvas_pos,
+ world_ray=ray,
+ buttons=btn,
+ )
+ elif etype == QEvent.Type.MouseButtonPress:
+ self._active_buttons |= btn
+ return MousePressEvent(
+ canvas_pos=canvas_pos,
+ world_ray=ray,
+ buttons=btn,
+ )
+ elif etype == QEvent.Type.MouseButtonRelease:
+ self._active_buttons &= ~btn
+ return MouseReleaseEvent(
+ canvas_pos=canvas_pos,
+ world_ray=ray,
+ buttons=btn,
+ )
+ elif etype == QEvent.Type.Enter:
+ return MouseEnterEvent(
+ canvas_pos=canvas_pos,
+ world_ray=ray,
+ buttons=self._active_buttons,
+ )
+
+ elif qevent.type() == QEvent.Type.Leave:
+ return MouseLeaveEvent()
+
+ elif isinstance(qevent, QWheelEvent):
+ # TODO: Figure out the buttons
+ pos = qevent.position()
+ canvas_pos = (pos.x(), pos.y())
+ if not (ray := self._model_canvas.to_world(canvas_pos)):
+ return None
+ return WheelEvent(
+ canvas_pos=canvas_pos,
+ world_ray=ray,
+ buttons=self._active_buttons,
+ angle_delta=(qevent.angleDelta().x(), qevent.angleDelta().y()),
+ )
+
+ elif isinstance(qevent, QResizeEvent):
+ size = qevent.size()
+ return ResizeEvent(
+ width=size.width(),
+ height=size.height(),
+ )
+
+ return None
+
+
+class QtAppWrap(App):
+ """Provider for PyQt5/PySide2/PyQt6/PySide6."""
+
+ _APP_INSTANCE: ClassVar[Any] = None
+ IPY_MAGIC_KEY = "qt"
+
+ def create_app(self) -> Any:
+ if (qapp := QApplication.instance()) is None:
+ # otherwise create a new QApplication
+ # must be stored in a class variable to prevent garbage collection
+ QtAppWrap._APP_INSTANCE = qapp = QApplication(sys.argv)
+ qapp.setOrganizationName("ndv")
+ qapp.setApplicationName("ndv")
+
+ return qapp
+
+ def run(self) -> None:
+ """Run the Qt event loop."""
+ app = QApplication.instance() or self.create_app()
+
+ for wdg in QApplication.topLevelWidgets():
+ wdg.raise_()
+
+ # if ipy_shell := self._ipython_shell():
+ # # if we're already in an IPython session with %gui qt, don't block
+ # if str(ipy_shell.active_eventloop).startswith("qt"):
+ # return
+
+ app.exec()
+
+ def install_event_filter(self, canvas: Any, model_canvas: Canvas) -> EventFilter:
+ f = QtEventFilter(canvas, model_canvas)
+ cast("QWidget", canvas).installEventFilter(f)
+ return f
+
+ def show(self, canvas: CanvasAdaptor, visible: bool) -> None:
+ cast("QWidget", canvas._snx_get_native()).setVisible(visible)
+
+ def process_events(self) -> None:
+ """Process events for the application."""
+ QApplication.processEvents()
+
+ def call_later(self, msec: int, func: Callable[[], None]) -> None:
+ """Call `func` after `msec` milliseconds."""
+ QTimer.singleShot(msec, Qt.TimerType.PreciseTimer, func)
+
+ def call_in_main_thread(
+ self, func: Callable[P, T], *args: P.args, **kwargs: P.kwargs
+ ) -> Future[T]:
+ return _call_in_main_thread(func, *args, **kwargs)
+
+ @contextmanager
+ def block_events(self, window: Any) -> Iterator[None]:
+ """Context manager to block events for a window."""
+ with signals_blocked(window):
+ yield
+
+
+class MainThreadInvoker(QObject):
+ _current_callable: Callable | None = None
+ _moved: bool = False
+
+ def invoke(
+ self, func: Callable[P, T], *args: P.args, **kwargs: P.kwargs
+ ) -> Future[T]:
+ """Invokes a function in the main thread and returns a Future."""
+ future: Future[T] = Future()
+
+ def wrapper() -> None:
+ try:
+ result = func(*args, **kwargs)
+ future.set_result(result)
+ except Exception as e:
+ future.set_exception(e)
+
+ self._current_callable = wrapper
+ QMetaObject.invokeMethod(
+ self, "_invoke_current", Qt.ConnectionType.QueuedConnection
+ )
+ return future
+
+ @slot() # type: ignore[misc]
+ def _invoke_current(self) -> None:
+ """Invokes the current callable."""
+ if (cb := self._current_callable) is not None:
+ cb()
+ _INVOKERS.discard(self)
+
+
+_INVOKERS = set()
+
+
+def _call_in_main_thread(
+ func: Callable[P, T], *args: P.args, **kwargs: P.kwargs
+) -> Future[T]:
+ if (app := QCoreApplication.instance()) is None:
+ raise RuntimeError("No Qt application instance is running")
+ app_thread = app.thread()
+ if QThread.currentThread() is not app_thread:
+ invoker = MainThreadInvoker()
+ invoker.moveToThread(app_thread)
+ _INVOKERS.add(invoker)
+ return invoker.invoke(func, *args, **kwargs)
+
+ future: Future[T] = Future()
+ future.set_result(func(*args, **kwargs))
+ return future
diff --git a/src/scenex/app/_wx.py b/src/scenex/app/_wx.py
new file mode 100644
index 00000000..ef961938
--- /dev/null
+++ b/src/scenex/app/_wx.py
@@ -0,0 +1,242 @@
+from __future__ import annotations
+
+from concurrent.futures import Future
+from contextlib import contextmanager
+from typing import TYPE_CHECKING, Any
+
+import wx
+
+from scenex.app._auto import App
+from scenex.app.events._events import (
+ EventFilter,
+ MouseButton,
+ MouseEnterEvent,
+ MouseLeaveEvent,
+ MouseMoveEvent,
+ MousePressEvent,
+ MouseReleaseEvent,
+ ResizeEvent,
+ WheelEvent,
+)
+
+if TYPE_CHECKING:
+ from collections.abc import Callable, Iterator
+
+ from scenex import Canvas
+ from scenex.adaptors._base import CanvasAdaptor
+ from scenex.app._auto import P, T
+
+
+class WxEventFilter(EventFilter):
+ def __init__(
+ self,
+ canvas: wx.Window,
+ model_canvas: Canvas,
+ ) -> None:
+ self._canvas = canvas
+ self._model_canvas = model_canvas
+ self._active_button: MouseButton = MouseButton.NONE
+ self._install_events()
+
+ def _install_events(self) -> None:
+ self._canvas.Bind(wx.EVT_LEFT_DOWN, handler=self._on_mouse_down)
+ self._canvas.Bind(wx.EVT_LEFT_UP, handler=self._on_mouse_up)
+ self._canvas.Bind(wx.EVT_RIGHT_DOWN, handler=self._on_mouse_down)
+ self._canvas.Bind(wx.EVT_RIGHT_UP, handler=self._on_mouse_up)
+ self._canvas.Bind(wx.EVT_MIDDLE_DOWN, handler=self._on_mouse_down)
+ self._canvas.Bind(wx.EVT_MIDDLE_UP, handler=self._on_mouse_up)
+ self._canvas.Bind(wx.EVT_MOTION, handler=self._on_mouse_move)
+ self._canvas.Bind(wx.EVT_MOUSEWHEEL, handler=self._on_wheel)
+ self._canvas.Bind(wx.EVT_LEAVE_WINDOW, handler=self._on_leave_window)
+ self._canvas.Bind(wx.EVT_ENTER_WINDOW, handler=self._on_enter_window)
+ self._canvas.Bind(wx.EVT_SIZE, handler=self._on_resize)
+
+ def uninstall(self) -> None:
+ self._canvas.Unbind(wx.EVT_LEFT_DOWN)
+ self._canvas.Unbind(wx.EVT_LEFT_UP)
+ self._canvas.Unbind(wx.EVT_RIGHT_DOWN)
+ self._canvas.Unbind(wx.EVT_RIGHT_UP)
+ self._canvas.Unbind(wx.EVT_MIDDLE_DOWN)
+ self._canvas.Unbind(wx.EVT_MIDDLE_UP)
+ self._canvas.Unbind(wx.EVT_MOTION)
+ self._canvas.Unbind(wx.EVT_MOUSEWHEEL)
+ self._canvas.Unbind(wx.EVT_SIZE)
+
+ def _on_leave_window(self, event: wx.MouseEvent) -> None:
+ self._model_canvas.handle(MouseLeaveEvent())
+ event.Skip()
+
+ def _on_enter_window(self, event: wx.MouseEvent) -> None:
+ pos = event.GetPosition()
+ if ray := self._model_canvas.to_world((pos.x, pos.y)):
+ self._model_canvas.handle(
+ MouseEnterEvent(
+ canvas_pos=(pos.x, pos.y),
+ world_ray=ray,
+ buttons=self._active_button,
+ )
+ )
+ event.Skip()
+
+ def _on_resize(self, event: wx.SizeEvent) -> None:
+ self._model_canvas.handle(
+ ResizeEvent(
+ width=event.GetSize().GetWidth(),
+ height=event.GetSize().GetHeight(),
+ )
+ )
+ event.Skip()
+
+ def _on_mouse_down(self, event: wx.MouseEvent) -> None:
+ btn = self._map_button(event)
+ self._active_button |= btn
+ pos = event.GetPosition()
+ if ray := self._model_canvas.to_world((pos.x, pos.y)):
+ self._model_canvas.handle(
+ MousePressEvent(canvas_pos=(pos.x, pos.y), world_ray=ray, buttons=btn)
+ )
+ event.Skip()
+
+ def _on_mouse_up(self, event: wx.MouseEvent) -> None:
+ btn = self._map_button(event)
+ self._active_button &= ~btn
+ pos = event.GetPosition()
+ if ray := self._model_canvas.to_world((pos.x, pos.y)):
+ self._model_canvas.handle(
+ MouseReleaseEvent(
+ canvas_pos=(pos.x, pos.y),
+ world_ray=ray,
+ buttons=btn,
+ )
+ )
+ event.Skip()
+
+ def _on_mouse_move(self, event: wx.MouseEvent) -> None:
+ pos = event.GetPosition()
+ if ray := self._model_canvas.to_world((pos.x, pos.y)):
+ self._model_canvas.handle(
+ MouseMoveEvent(
+ canvas_pos=(pos.x, pos.y),
+ world_ray=ray,
+ buttons=self._active_button,
+ )
+ )
+ event.Skip()
+
+ def _on_wheel(self, event: wx.MouseEvent) -> None:
+ pos = event.GetPosition()
+ if ray := self._model_canvas.to_world((pos.x, pos.y)):
+ if event.GetWheelAxis() == 0:
+ # Vertical Scroll
+ angle_delta = (0, event.GetWheelRotation())
+ else:
+ # Horizontal Scroll
+ angle_delta = (event.GetWheelRotation(), 0)
+
+ self._model_canvas.handle(
+ WheelEvent(
+ canvas_pos=(pos.x, pos.y),
+ world_ray=ray,
+ buttons=self._active_button,
+ angle_delta=angle_delta,
+ )
+ )
+ event.Skip()
+
+ def _map_button(self, event: wx.MouseEvent) -> MouseButton:
+ if event.LeftDown() or event.LeftUp():
+ return MouseButton.LEFT
+ if event.RightDown() or event.RightUp():
+ return MouseButton.RIGHT
+ if event.MiddleDown() or event.MiddleUp():
+ return MouseButton.MIDDLE
+ return MouseButton.NONE
+
+
+class WxAppWrap(App):
+ """Provider for wxPython."""
+
+ def create_app(self) -> Any:
+ if wx.App.Get():
+ return wx.App.Get()
+ return wx.App(False)
+
+ def run(self) -> None:
+ app = wx.App.Get() or self.create_app()
+
+ # if ipy_shell := self._ipython_shell():
+ # # if we're already in an IPython session with %gui qt, don't block
+ # if str(ipy_shell.active_eventloop).startswith("wx"):
+ # return
+
+ app.MainLoop()
+
+ def install_event_filter(
+ self,
+ canvas: wx.Window,
+ model_canvas: Canvas,
+ ) -> EventFilter:
+ return WxEventFilter(canvas, model_canvas)
+
+ def show(self, canvas: CanvasAdaptor, visible: bool) -> None:
+ canvas._snx_get_native().Show(visible)
+ self.process_events()
+
+ def process_events(self) -> None:
+ """Process events."""
+ wx.SafeYield()
+
+ def call_later(self, msec: int, func: Callable[[], None]) -> None:
+ """Call `func` after `msec` milliseconds."""
+ wx.CallLater(msec, func)
+
+ def call_in_main_thread(
+ self, func: Callable[P, T], *args: P.args, **kwargs: P.kwargs
+ ) -> Future[T]:
+ return call_in_main_thread(func, *args, **kwargs)
+
+ @contextmanager
+ def block_events(self, window: Any) -> Iterator[None]:
+ """Context manager to block events for a window."""
+ with wx.EventBlocker(window):
+ yield
+
+
+class MainThreadInvoker:
+ def __init__(self) -> None:
+ """Utility for invoking functions in the main thread."""
+ # Ensure this is initialized from the main thread
+ if not wx.IsMainThread(): # pyright: ignore[reportCallIssue]
+ raise RuntimeError(
+ "MainThreadInvoker must be initialized in the main thread"
+ )
+
+ def invoke(
+ self, func: Callable[P, T], *args: P.args, **kwargs: P.kwargs
+ ) -> Future[T]:
+ """Invokes a function in the main thread and returns a Future."""
+ future: Future[T] = Future()
+
+ def wrapper() -> None:
+ try:
+ result = func(*args, **kwargs)
+ future.set_result(result)
+ except Exception as e:
+ future.set_exception(e)
+
+ wx.CallAfter(wrapper)
+ return future
+
+
+_MAIN_THREAD_INVOKER = MainThreadInvoker()
+
+
+def call_in_main_thread(
+ func: Callable[P, T], *args: P.args, **kwargs: P.kwargs
+) -> Future[T]:
+ if not wx.IsMainThread(): # pyright: ignore[reportCallIssue]
+ return _MAIN_THREAD_INVOKER.invoke(func, *args, **kwargs)
+
+ future: Future[T] = Future()
+ future.set_result(func(*args, **kwargs))
+ return future
diff --git a/src/scenex/app/events/__init__.py b/src/scenex/app/events/__init__.py
new file mode 100644
index 00000000..223ed74d
--- /dev/null
+++ b/src/scenex/app/events/__init__.py
@@ -0,0 +1,33 @@
+"""The Scenex Event Abstraction."""
+
+from ._events import (
+ Event,
+ EventFilter,
+ MouseButton,
+ MouseDoublePressEvent,
+ MouseEnterEvent,
+ MouseEvent,
+ MouseLeaveEvent,
+ MouseMoveEvent,
+ MousePressEvent,
+ MouseReleaseEvent,
+ Ray,
+ ResizeEvent,
+ WheelEvent,
+)
+
+__all__ = [
+ "Event",
+ "EventFilter",
+ "MouseButton",
+ "MouseDoublePressEvent",
+ "MouseEnterEvent",
+ "MouseEvent",
+ "MouseLeaveEvent",
+ "MouseMoveEvent",
+ "MousePressEvent",
+ "MouseReleaseEvent",
+ "Ray",
+ "ResizeEvent",
+ "WheelEvent",
+]
diff --git a/src/scenex/app/events/_events.py b/src/scenex/app/events/_events.py
new file mode 100644
index 00000000..c5dca0c2
--- /dev/null
+++ b/src/scenex/app/events/_events.py
@@ -0,0 +1,133 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+from enum import IntFlag, auto
+from typing import TYPE_CHECKING, NamedTuple, TypeAlias
+
+if TYPE_CHECKING:
+ from scenex import Node
+
+
+# Note that scenex follows the inheritance pattern for event subtypes.
+# This enables both extensibility, such that new event types can be added easily
+# even downstream, and also structured type checking.
+@dataclass
+class Event:
+ """A general interaction event."""
+
+ pass
+
+
+class MouseButton(IntFlag):
+ """A general mouse interaction event."""
+
+ NONE = 0
+ LEFT = auto()
+ MIDDLE = auto()
+ RIGHT = auto()
+
+
+Intersection: TypeAlias = tuple["Node", float]
+
+
+class Ray(NamedTuple):
+ """A ray passing through the world."""
+
+ origin: tuple[float, float, float]
+ direction: tuple[float, float, float]
+
+ def point_at_distance(self, distance: float) -> tuple[float, float, float]:
+ x = self.origin[0] + self.direction[0] * distance
+ y = self.origin[1] + self.direction[1] * distance
+ z = self.origin[2] + self.direction[2] * distance
+ return (x, y, z)
+
+ def intersections(self, graph: Node) -> list[Intersection]:
+ """
+ Find all intersections of this ray with the given scene graph.
+
+ Returns a list of (node, distance) tuples, sorted by distance.
+ """
+ through: list[Intersection] = []
+ for child in graph.children:
+ if (d := child.passes_through(self)) is not None:
+ through.append((child, d))
+ through.extend(self.intersections(child))
+ return sorted(through, key=lambda inter: inter[1])
+
+
+@dataclass
+class ResizeEvent(Event):
+ """A window resize event."""
+
+ width: int # in pixels
+ height: int # in pixels
+
+
+@dataclass
+class MouseEvent(Event):
+ """Base class for mouse interaction events."""
+
+ canvas_pos: tuple[float, float]
+ world_ray: Ray
+ buttons: MouseButton
+
+
+@dataclass
+class MouseLeaveEvent(Event):
+ """Mouse leave event.
+
+ Note that this does not inherit from MouseEvent, as no position or buttons are
+ """
+
+ pass
+
+
+@dataclass
+class MouseEnterEvent(MouseEvent):
+ """Mouse enter event."""
+
+ pass
+
+
+@dataclass
+class MouseMoveEvent(MouseEvent):
+ """Mouse move event."""
+
+ pass
+
+
+@dataclass
+class MousePressEvent(MouseEvent):
+ """Mouse press event."""
+
+ pass
+
+
+@dataclass
+class MouseReleaseEvent(MouseEvent):
+ """Mouse release event."""
+
+ pass
+
+
+@dataclass
+class MouseDoublePressEvent(MouseEvent):
+ """Mouse double press event."""
+
+ pass
+
+
+@dataclass
+class WheelEvent(MouseEvent):
+ """A mouse interaction event describing wheel movement."""
+
+ angle_delta: tuple[float, float]
+
+
+class EventFilter:
+ def uninstall(self) -> None:
+ """Uninstall the event filter."""
+ raise NotImplementedError("This method should be implemented by subclasses.")
+
+ pass
diff --git a/src/scenex/imgui/_controls.py b/src/scenex/imgui/_controls.py
index 46d0c066..c571b013 100644
--- a/src/scenex/imgui/_controls.py
+++ b/src/scenex/imgui/_controls.py
@@ -12,7 +12,7 @@
) from e
import logging
import types
-from typing import TYPE_CHECKING, Any, Literal, get_args, get_origin
+from typing import TYPE_CHECKING, Any, Literal, cast, get_args, get_origin
import annotated_types
import cmap
@@ -27,6 +27,7 @@
from pydantic.fields import FieldInfo
import scenex as snx
+ from scenex.adaptors._base import CanvasAdaptor
from scenex.model._view import View
logger = logging.getLogger("scenex.imgui")
@@ -39,7 +40,7 @@ def add_imgui_controls(view: View) -> None:
snx_canvas_model = view.canvas
snx_canvas_adaptor = snx_canvas_model._get_adaptors(backend="pygfx")[0]
snx_view_adaptor = view._get_adaptors(backend="pygfx")[0]
- render_canv = snx_canvas_model._get_native()
+ render_canv = cast("CanvasAdaptor", snx_canvas_adaptor)._snx_get_native()
if not (
isinstance(snx_canvas_adaptor, PygfxCanvasAdaptor)
@@ -49,6 +50,8 @@ def add_imgui_controls(view: View) -> None:
raise NotImplementedError(
"Imgui controls can currently only be added to a canvas backed by pygfx."
)
+ if not snx_view_adaptor._renderer:
+ raise RuntimeError("The pygfx renderer has not been initialized yet.")
imgui_renderer = ImguiRenderer(
device=snx_view_adaptor._renderer.device,
diff --git a/src/scenex/model/__init__.py b/src/scenex/model/__init__.py
index 0d0f12d8..385b68d1 100644
--- a/src/scenex/model/__init__.py
+++ b/src/scenex/model/__init__.py
@@ -17,15 +17,17 @@
from ._layout import Layout
from ._nodes.camera import Camera, CameraType
from ._nodes.image import Image, InterpolationMode
-from ._nodes.node import AnyNode, Node
+from ._nodes.node import AnyNode, BlendMode, Node
from ._nodes.points import Points, ScalingMode, SymbolName
from ._nodes.scene import Scene
+from ._nodes.text import Text
from ._nodes.volume import RenderMode, Volume
from ._transform import Transform
from ._view import View
__all__ = [
"AnyNode",
+ "BlendMode",
"Camera",
"CameraType",
"Canvas",
@@ -41,6 +43,7 @@
"ScalingMode",
"Scene",
"SymbolName",
+ "Text",
"Transform",
"View",
"Volume",
diff --git a/src/scenex/model/_base.py b/src/scenex/model/_base.py
index 672f1847..1c2a8243 100644
--- a/src/scenex/model/_base.py
+++ b/src/scenex/model/_base.py
@@ -39,7 +39,11 @@ class EventedBase(BaseModel):
_model_id: uuid.UUID = PrivateAttr(default_factory=uuid.uuid4)
- events: ClassVar[SignalGroupDescriptor] = SignalGroupDescriptor()
+ events: ClassVar[SignalGroupDescriptor] = SignalGroupDescriptor(
+ # NB: Need to avoid propagating events to parents who don't necessarily have the
+ # same fields.
+ connect_child_events=False
+ )
# note, strangely, for mypy reasons,
# this configDict should not include extra="forbid"
@@ -79,15 +83,7 @@ def _get_adaptors(
self, backend: str | None = None, create: bool = False
) -> list["Adaptor"]:
"""Get all adaptors for this model."""
- from scenex.adaptors import get_adaptor_registry, get_all_adaptors
-
- if backend:
- reg = get_adaptor_registry(backend=backend)
- return [reg.get_adaptor(self, create=create)]
- else:
- return list(get_all_adaptors(self))
-
- def _get_native(self, backend: str | None = None, create: bool = False) -> Any:
- """Get the native object for this model."""
- if adaptors := self._get_adaptors(backend=backend, create=create):
- return adaptors[0]._snx_get_native()
+ from scenex.adaptors import get_adaptor_registry
+
+ reg = get_adaptor_registry(backend=backend)
+ return [reg.get_adaptor(self, create=create)]
diff --git a/src/scenex/model/_canvas.py b/src/scenex/model/_canvas.py
index 87889459..d75a0b29 100644
--- a/src/scenex/model/_canvas.py
+++ b/src/scenex/model/_canvas.py
@@ -1,17 +1,28 @@
from __future__ import annotations
+from collections.abc import Sequence
from typing import TYPE_CHECKING, Any, cast
+import numpy as np
+import pylinalg as la
from cmap import Color
-from pydantic import ConfigDict, Field
+from pydantic import ConfigDict, Field, PrivateAttr
+
+from scenex.app.events import (
+ Event,
+ MouseEnterEvent,
+ MouseEvent,
+ MouseLeaveEvent,
+ Ray,
+ ResizeEvent,
+)
from ._base import EventedBase
from ._evented_list import EventedList
from ._view import View # noqa: TC001
if TYPE_CHECKING:
- import numpy as np
-
+ from scenex import Node
from scenex.adaptors._base import CanvasAdaptor
@@ -32,12 +43,67 @@ class Canvas(EventedBase):
title: str = Field(default="", description="The title of the canvas.")
views: EventedList[View] = Field(default_factory=EventedList, frozen=True)
+ # Private state for tracking mouse view transitions
+ _last_mouse_view: View | None = PrivateAttr(default=None)
+
model_config = ConfigDict(extra="forbid")
def model_post_init(self, __context: Any) -> None:
"""Post-initialization hook for the model."""
+ # Update all current views
+ for view in self.views:
+ view._canvas = self
+ # Update all views added later
+ self.views.item_inserted.connect(self._on_view_inserted)
+ self.views.item_changed.connect(self._on_view_changed)
+ self.views.item_removed.connect(self._on_view_removed)
+
+ self.events.width.connect(self._recompute_layout)
+ self.events.height.connect(self._recompute_layout)
+
+ self._recompute_layout()
+
+ def _recompute_layout(self, dont_use: int | None = None) -> None:
+ if not len(self.views):
+ # Nothing to do
+ return
+ # The parameter is EITHER width or height - just use the model values instead
+ width, height = self.size
+ # FIXME: Allow customization
+ x = 0.0
+ dx = float(width) / len(self.views)
+
for view in self.views:
+ view.layout.x = x
+ view.layout.y = 0
+ view.layout.width = dx
+ view.layout.height = height
+ x += dx
+
+ def _on_view_inserted(self, idx: int, view: View) -> None:
+ view._canvas = self
+ self._recompute_layout()
+
+ def _on_view_removed(self, idx: int, view: View) -> None:
+ view._canvas = None
+ self._recompute_layout()
+
+ def _on_view_changed(
+ self,
+ idx: int | slice,
+ old_view: View | Sequence[View],
+ new_view: View | Sequence[View],
+ ) -> None:
+ if not isinstance(old_view, Sequence):
+ old_view = [old_view]
+ for view in old_view:
+ view._canvas = None
+
+ if not isinstance(new_view, Sequence):
+ new_view = [new_view]
+ for view in new_view:
view._canvas = self
+ self._recompute_layout()
@property
def size(self) -> tuple[int, int]:
@@ -54,3 +120,123 @@ def render(self) -> np.ndarray:
if adaptors := self._get_adaptors():
return cast("CanvasAdaptor", adaptors[0])._snx_render()
raise RuntimeError("No adaptor found for Canvas.")
+
+ def handle(self, event: Event) -> bool:
+ """Handle the passed event."""
+ handled = False
+ if isinstance(event, MouseEvent):
+ current_view = self._containing_view(event.canvas_pos)
+
+ # Check if we've moved between views and handle transitions
+ # BEGIN UNTESTED CODE!
+ # TODO: Add a test for this once multiple views are better supported
+ if self._last_mouse_view != current_view:
+ # Send leave event to the previous view
+ if self._last_mouse_view is not None:
+ leave_event = MouseLeaveEvent()
+ self._last_mouse_view.filter_event(leave_event)
+
+ # Send enter event to the new view (if any)
+ if current_view is not None:
+ enter_event = MouseEnterEvent(
+ canvas_pos=event.canvas_pos,
+ world_ray=event.world_ray,
+ buttons=event.buttons,
+ )
+ current_view.filter_event(enter_event)
+
+ self._last_mouse_view = current_view
+ # END UNTESTED CODE!
+
+ # Handle the original mouse event in the current view
+ if current_view is not None:
+ # Give the view a chance to observe the result
+ if current_view.filter_event(event):
+ return True
+
+ intersections = event.world_ray.intersections(current_view.scene)
+ # FIXME: Consider only reporting the first?
+ # Or do we only report until we hit a node with opacity=1?
+ for node, _distance in intersections:
+ # Filter through parent scenes to child
+ if Canvas._filter_through(event, node, node):
+ return True
+ # No nodes in the view handled the event - pass it to the camera
+ if current_view.camera.interactive:
+ handled |= current_view.camera.filter_event(
+ event, current_view.camera
+ )
+ elif isinstance(event, MouseLeaveEvent):
+ # Mouse left the entire canvas
+ if self._last_mouse_view is not None:
+ handled = self._last_mouse_view.filter_event(event)
+ self._last_mouse_view = None
+ elif isinstance(event, ResizeEvent):
+ # TODO: How might some event filter tap into the resize?
+ self.size = (event.width, event.height)
+ return handled
+
+ @staticmethod
+ def _filter_through(event: Any, node: Node, target: Node) -> bool:
+ """Filter the event through the scene graph to the target node."""
+ # TODO: Suppose a scene is not interactive. If the node is interactive, should
+ # it receive the event?
+
+ # First give this node a chance to filter the event.
+
+ if node.interactive and node.filter_event(event, target):
+ # Node filtered out the event, so we stop here.
+ return True
+ if (parent := node.parent) is None:
+ # Node did not filter out the event, and we've reached the top of the graph.
+ return False
+ # Recursively filter the event through node's parent.
+ return Canvas._filter_through(event, parent, target)
+
+ def to_world(self, canvas_pos: tuple[float, float]) -> Ray | None:
+ """Map XY canvas position (pixels) to XYZ coordinate in world space."""
+ # Code adapted from:
+ # https://github.com/pygfx/pygfx/pull/753/files#diff-173d643434d575e67f8c0a5bf2d7ea9791e6e03a4e7a64aa5fa2cf4172af05cdR395
+ view = self._containing_view(canvas_pos)
+ if view is None:
+ return None
+
+ # Get position relative to viewport
+ pos_rel = (
+ canvas_pos[0] - view.layout.x,
+ canvas_pos[1] - view.layout.y,
+ )
+
+ width, height = view.layout.size
+
+ # Convert position to Normalized Device Coordinates (NDC) - i.e., within [-1, 1]
+ x = pos_rel[0] / width * 2 - 1
+ y = -(pos_rel[1] / height * 2 - 1)
+ pos_ndc = (x, y)
+
+ # Note that the camera matrix is the matrix multiplication of:
+ # * The projection matrix, which projects local space (the rectangular
+ # bounds of the perspective camera) into NDC.
+ # * The view matrix, i.e. the transform positioning the camera in the world.
+ # The result is a matrix mapping world coordinates
+ camera_matrix = view.camera.projection @ view.camera.transform.inv().T
+ # Unproject the canvas NDC coordinates into world space.
+ pos_world = la.vec_unproject(pos_ndc, camera_matrix)
+
+ # To find the direction of the ray, we find a unprojected point farther away
+ # and subtract the closer point.
+ pos_world_farther = la.vec_unproject(pos_ndc, camera_matrix, depth=1)
+ direction = pos_world_farther - pos_world
+ direction = direction / np.linalg.norm(direction)
+
+ ray = Ray(
+ origin=tuple(pos_world),
+ direction=tuple(direction),
+ )
+ return ray
+
+ def _containing_view(self, pos: tuple[float, float]) -> View | None:
+ for view in self.views:
+ if pos in view.layout:
+ return view
+ return None
diff --git a/src/scenex/model/_layout.py b/src/scenex/model/_layout.py
index f0433629..78f15cbd 100644
--- a/src/scenex/model/_layout.py
+++ b/src/scenex/model/_layout.py
@@ -74,3 +74,24 @@ def position(self) -> tuple[float, float]:
def size(self) -> tuple[float, float]:
"""Return the width, height of the layout as a tuple."""
return self.width, self.height
+
+ @computed_field # type: ignore [prop-decorator]
+ @property
+ def content_rect(self) -> tuple[float, float, float, float]:
+ """Return the (x, y, width, height) of the content area."""
+ offset = self.padding + self.border_width + self.margin
+ return (
+ self.x + offset,
+ self.y + offset,
+ self.width - 2 * offset,
+ self.height - 2 * offset,
+ )
+
+ def __contains__(self, pos: tuple[float, float]) -> bool:
+ offset = self.padding + self.border_width + self.margin
+
+ left = self.x + offset
+ right = self.x + self.width - offset
+ bottom = self.y + offset
+ top = self.y + self.height - offset
+ return left <= pos[0] and pos[0] <= right and bottom <= pos[1] and pos[1] <= top
diff --git a/src/scenex/model/_nodes/camera.py b/src/scenex/model/_nodes/camera.py
index ceea445b..37547ae7 100644
--- a/src/scenex/model/_nodes/camera.py
+++ b/src/scenex/model/_nodes/camera.py
@@ -1,14 +1,27 @@
from __future__ import annotations
-from typing import Literal
+import math
+from typing import TYPE_CHECKING, Any, Literal
-from pydantic import Field
+import numpy as np
+import pylinalg as la
+from pydantic import Field, PrivateAttr, computed_field
+
+from scenex.model._transform import Transform
+from scenex.utils import projections
from .node import Node
+if TYPE_CHECKING:
+ from collections.abc import Callable
+
+ from scenex.app.events._events import Event, Ray
+ from scenex.model._transform import Transform
+
CameraType = Literal["panzoom", "perspective"]
Position2D = tuple[float, float]
Position3D = tuple[float, float, float]
+Vector3D = tuple[float, float, float]
Position = Position2D | Position3D
@@ -17,17 +30,119 @@ class Camera(Node):
The camera lives in, and is a child of, a scene graph. It defines the view
transformation for the scene, mapping it onto a 2D surface.
+
+ Cameras have two different Transforms. Like all Nodes, it has a transform
+ `transform`, describing how local 3D space is mapped to world 3D space. Its other
+ transform, `projection`, describes how 2D normalized device coordinates
+ {(x, y) | x in [-1, 1], y in [-1, 1]} map to a ray in local 3D space. The inner
+ product of these matrices can convert a 2D canvas position to a 3D ray eminating
+ from the camera node into the world.
+
+ Following OpenGL convention, a scenex camera uses a right-handed coordinate system,
+ where the positive z-axis points out of the screen, the positive x-axis points to
+ the right, and the positive y-axis points up.
"""
+ def __init__(
+ self, controller: Callable[[Event, Node], bool] | None = None, **kwargs: Any
+ ) -> None:
+ super().__init__(**kwargs)
+ self.set_event_filter(controller)
+
node_type: Literal["camera"] = "camera"
- type: CameraType = Field(default="panzoom", description="Camera type.")
interactive: bool = Field(
default=True,
description="Whether the camera responds to user interaction, "
"such as mouse and keyboard events.",
)
- zoom: float = Field(default=1.0, description="Zoom factor of the camera.")
- center: Position = Field(
- default=(0, 0, 0), description="Center position of the view."
+ projection: Transform = Field(
+ default_factory=lambda: projections.orthographic(2, 2, 2),
+ description="Describes how 3D points are mapped to a 2D canvas, "
+ "default is an orthographic projection of a 2x2x2 cube, centered at (0, 0, 0)",
)
+
+ @computed_field # type: ignore[prop-decorator]
+ @property # TODO: Cache?
+ def bounding_box(self) -> None:
+ # Prevent cameras from distorting scene bounding boxes
+ return None
+
+ _filter: Callable[[Event, Node], bool] | None = PrivateAttr(default=None)
+
+ def passes_through(self, ray: Ray) -> float | None:
+ # Cameras are not rendered objects
+ return None
+
+ @property
+ def forward(self) -> Vector3D:
+ """The forward direction of the camera in world space, as a unit vector."""
+ position = self.transform.map((0, 0, 0))[:3]
+ further = self.transform.map((0, 0, -1))[:3]
+ vector = further - position
+ return tuple(vector / np.linalg.norm(vector))
+
+ @forward.setter
+ def forward(self, arg: Vector3D) -> None:
+ """Sets the forward direction of the camera."""
+ # Compute the quaternion needed to rotate from the current forward direction to
+ # the desired forward direction
+ if np.dot(self.forward, arg) == 1:
+ return # No change needed
+ rot_quat = la.quat_from_vecs(self.forward, arg)
+ rot_axis, rot_angle = la.quat_to_axis_angle(rot_quat)
+
+ # Rotate around the camera's current position
+ position = self.transform.map((0, 0, 0))[:3]
+ self.transform = (
+ self.transform.translated(-position)
+ .rotated(rot_angle * 180 / math.pi, rot_axis)
+ .translated(position)
+ )
+
+ @property
+ def up(self) -> Vector3D:
+ """The up direction of the camera in world space, as a unit vector."""
+ return tuple(self.transform.map((0, 1, 0)) - self.transform.map((0, 0, 0)))[:3]
+
+ @up.setter
+ def up(self, arg: Vector3D) -> None:
+ """Sets the up direction of the camera.
+
+ Does not affect the forward direction of the camera so long as the new up
+ direction is perpendicular to the existing forward direction.
+ """
+ if np.dot(self.up, arg) == 1:
+ return # No change needed
+ # Compute the quaternion needed to rotate from the current up direction to
+ # the desired up direction
+ rot_quat = la.quat_from_vecs(self.up, arg)
+ rot_axis, rot_angle = la.quat_to_axis_angle(rot_quat)
+
+ # Rotate around the camera's current position
+ position = self.transform.map((0, 0, 0))[:3]
+ self.transform = (
+ self.transform.translated(-position)
+ .rotated(rot_angle * 180 / math.pi, rot_axis)
+ .translated(position)
+ )
+
+ def look_at(self, target: Position3D, /, *, up: Vector3D | None = None) -> None:
+ """Adjusts the camera to look at a target point in the world.
+
+ Parameters
+ ----------
+ target: Position3D
+ The position in 3D space that the camera should look at.
+ up: Vector3D, optional
+ The up direction for the camera. If provided, this vector must be
+ perpendicular to the forward vector that results from looking at target.
+ """
+ position = self.transform.map((0, 0, 0))[:3]
+ self.forward = tuple(target - position)
+ if up is not None:
+ if np.linalg.norm(up) == 0:
+ raise ValueError("Up vector must be non-zero.")
+ if np.abs(np.dot(self.forward, up)) > 1e-6:
+ raise ValueError("Up vector must be perpendicular to forward vector.")
+ self.up = up
diff --git a/src/scenex/model/_nodes/image.py b/src/scenex/model/_nodes/image.py
index 1b419886..1fe9fc66 100644
--- a/src/scenex/model/_nodes/image.py
+++ b/src/scenex/model/_nodes/image.py
@@ -1,10 +1,16 @@
-from typing import Annotated, Any, Literal
+from __future__ import annotations
+from typing import TYPE_CHECKING, Annotated, Any, Literal
+
+import numpy as np
from annotated_types import Interval
from cmap import Colormap
-from pydantic import Field
+from pydantic import Field, computed_field
+
+from .node import AABB, Node
-from .node import Node
+if TYPE_CHECKING:
+ from scenex.app.events._events import Ray
InterpolationMode = Literal["nearest", "linear", "bicubic"]
@@ -33,3 +39,107 @@ class Image(Node):
interpolation: InterpolationMode = Field(
default="nearest", description="Interpolation mode."
)
+
+ @computed_field # type: ignore[prop-decorator]
+ @property # TODO: Cache?
+ def bounding_box(self) -> AABB:
+ if not hasattr(self.data, "shape"):
+ raise TypeError(f"{self.data} does not have a shape!")
+ shape = self.data.shape
+ mi = [-0.5 for _d in shape] + [0] * (3 - len(shape))
+ ma = [d - 0.5 for d in shape] + [0] * (3 - len(shape))
+ return (tuple(mi), tuple(ma)) # type: ignore
+
+ def passes_through(self, ray: Ray) -> float | None:
+ mi, _ma = self.bounding_box
+ origin = self.transform.map(mi)[:3]
+ u = self.transform.map((self.data.shape[0], 0, 0, 0))[:3]
+ v = self.transform.map((0, self.data.shape[1], 0, 0))[:3]
+ return _passes_through_parallelogram(ray, origin, u, v)
+
+
+def _passes_through_parallelogram(
+ ray: Ray, origin: np.ndarray, u: np.ndarray, v: np.ndarray
+) -> float | None:
+ """Determine whether a ray passes through a parallelogram defined by (origin, u, v).
+
+ Parameters
+ ----------
+ ray : Ray
+ The ray passing through the scene
+ origin : np.ndarray
+ A np.ndarray of shape (3,) representing the origin point of the parallelogram.
+ u : np.ndarray
+ A np.ndarray of shape (3,) representing the direction and length of one edge of
+ the parallelogram.
+ v : np.ndarray
+ A np.ndarray of shape (3,) representing the direction and length of another edge
+ of the parallelogram. Note that u and v should not be parallel.
+
+ Returns
+ -------
+ t: float | None
+ The depth t at which the ray intersects the node, or None if it never
+ intersects.
+ """
+ # Math graciously adapted from:
+ # https://raytracing.github.io/books/RayTracingTheNextWeek.html#quadrilaterals
+
+ # Step 1 - Determine where the ray intersects the image plane
+
+ # The image plane is defined by the normal vector n=(a, b, c) and an offset (d)
+ # such that any point p=(x, y, z) on the plane satisfies np.dot(v, p) = d, or
+ # ax + by + cz + -d = 0.
+
+ # In this case, the normal vector n can be found by the cross product of u and v
+ tformed = np.cross(u, v)
+ normal = tformed / np.linalg.norm(tformed)
+ # And we know that the origin of the image is on the plane. Using that point we can
+ # find d...
+ d = np.dot(normal, origin)
+ # ... and with d we can find the depth t at which the ray would intersect the plane.
+ #
+ # Note that our ray is defined by (ray.origin + ray.direction * t).
+ # This is just np.dot(normal, ray.origin + ray.direction * t) = d,
+ # rearranged to solve for t.
+ ray_normal_inner_product = np.dot(normal, ray.direction)
+ if ray_normal_inner_product == 0:
+ # Plane is parallel to the ray, so no intersection.
+ return None
+ t = (d - np.dot(normal, ray.origin)) / ray_normal_inner_product
+ # With our value of t, we can find the intersection point:
+ intersection = tuple(
+ a + t * b for a, b in zip(ray.origin, ray.direction, strict=False)
+ )
+
+ # Step 2 - Determine whether the ray hits the image.
+
+ # We need to determine whether the planar intersection is within the image
+ # interval bounds. In other words, the intersection point should be within
+ # [0, magnitude(u)) units away from the image origin along the u axis and
+ # [0, magnitude(v)) units away from the image origin along the v axis.
+ #
+ # Note the open right bound: if the ray intersects exactly on the far edge of the
+ # image, we consider that a miss. This approach serves two important purposes:
+ #
+ # 1. Adjacent image handling: When images are tiled side-by-side, this prevents
+ # ambiguity at shared boundaries (only one image reports intersection).
+ #
+ # 2. Array indexing safety: The [0, 1) bounds ensure that subsequent coordinate-to-
+ # array-index mapping never produces out-of-bounds indices. Alternatively we'd
+ # require clamping logic during array access.
+ offset = intersection - origin
+
+ # We use some fancy math derived from the link above to convert offset into...
+ n = np.cross(u, v)
+ w = n / np.dot(n, n)
+ # ...the component of offset in direction of u...
+ alpha = np.dot(w, np.cross(offset, v))
+ # ...and the component of offset in direction of v
+ beta = np.dot(w, np.cross(u, offset))
+
+ # Our ray passes through the image if alpha and beta are within [0, 1)
+ is_inside = alpha >= 0 and alpha < 1 and beta >= 0 and beta < 1
+
+ # If the ray passes through node, return the depth of the intersection.
+ return t if is_inside else None
diff --git a/src/scenex/model/_nodes/node.py b/src/scenex/model/_nodes/node.py
index 3213ec7d..469e5002 100644
--- a/src/scenex/model/_nodes/node.py
+++ b/src/scenex/model/_nodes/node.py
@@ -1,7 +1,11 @@
+from __future__ import annotations
+
import logging
from collections.abc import Iterable, Iterator
-from typing import TYPE_CHECKING, Annotated, Any, ClassVar, Union, cast
+from enum import Enum
+from typing import TYPE_CHECKING, Annotated, Any, ClassVar, TypeAlias, Union, cast
+import numpy as np
from psygnal import Signal
from pydantic import (
ConfigDict,
@@ -19,9 +23,13 @@
from scenex.model._transform import Transform
if TYPE_CHECKING:
+ from collections.abc import Callable, Iterable, Iterator
+
import numpy.typing as npt
from typing_extensions import Self, TypedDict, Unpack
+ from scenex.app.events import Event, Ray
+
from .camera import Camera
from .image import Image
from .points import Points
@@ -30,7 +38,7 @@
class NodeKwargs(TypedDict, total=False):
"""TypedDict for Node kwargs."""
- parent: "Node | None"
+ parent: Node | None
name: str | None
visible: bool
interactive: bool
@@ -47,6 +55,37 @@ class NodeKwargs(TypedDict, total=False):
Union["Image", "Points", "Camera", "Scene"], Field(discriminator="node_type")
]
+# Axis-Aligned Bounding Box
+AABB: TypeAlias = tuple[tuple[float, float, float], tuple[float, float, float]]
+
+
+class BlendMode(Enum):
+ """
+ A set of available blending modes.
+
+ Blending modes determine how the colors of rendered objects are combined with the
+ colors already present in the framebuffer. More practically, if two objects overlap
+ from the camera's perspective in the scene, the blending mode of the new object
+ determines how its colors are combined with those of the object previously rendered.
+
+ Note that the draw order plays a crucial role in blending.
+ """
+
+ OPAQUE = "opaque"
+ """The object's color value, multiplied by its alpha value, overwrites the
+ background color.
+ """
+ ALPHA = "alpha"
+ """
+ The object's color is blended with the background using standard alpha compositing.
+ The resulting color is a weighted combination of the foreground and background,
+ where weights are determined by alpha values.
+ """
+ ADDITIVE = "additive"
+ """The object's color value, multiplied by its alpha value, is added to the
+ background color.
+ """
+
class Node(EventedBase):
"""Base class for all nodes. Also a [`Container[Node]`][collections.abc.Container].
@@ -55,9 +94,9 @@ class Node(EventedBase):
be used in place of Node.
"""
- parent: "Node | None" = Field(default=None, repr=False, exclude=True)
+ parent: Node | None = Field(default=None, repr=False, exclude=True)
# see computed field below
- _children: list["AnyNode"] = PrivateAttr(default_factory=list)
+ _children: list[AnyNode] = PrivateAttr(default_factory=list)
name: str | None = Field(default=None, description="Name of the node.")
visible: bool = Field(default=True, description="Whether this node is visible.")
@@ -78,6 +117,21 @@ class Node(EventedBase):
description="Transform that maps the local coordinate frame to the coordinate "
"frame of the parent.",
)
+ blending: BlendMode = Field(
+ default=BlendMode.OPAQUE,
+ description="Describes how this node interacts with nodes behind it.",
+ )
+
+ _filter: Callable[[Event, Node], bool] | None = PrivateAttr(default=None)
+
+ def set_event_filter(
+ self, callable: Callable[[Event, Node], bool] | None
+ ) -> Callable[[Event, Node], bool] | None:
+ old, self._filter = self._filter, callable
+ return old
+
+ def filter_event(self, event: Event, target: Node) -> bool:
+ return self._filter(event, target) if self._filter else False
model_config = ConfigDict(extra="forbid")
@@ -87,8 +141,8 @@ class Node(EventedBase):
def __init__(
self,
*,
- children: Iterable["Node | dict[str, Any]"] = (),
- **data: "Unpack[NodeKwargs]",
+ children: Iterable[Node | dict[str, Any]] = (),
+ **data: Unpack[NodeKwargs],
) -> None:
# prevent direct instantiation.
# makes it easier to use NodeUnion without having to deal with self-reference.
@@ -104,31 +158,64 @@ def __init__(
@computed_field # type: ignore[prop-decorator]
@property
- def children(self) -> tuple["Node", ...]:
+ def children(self) -> tuple[Node, ...]:
"""Return a tuple of the children of this node."""
return tuple(self._children)
- def add_child(self, child: "AnyNode") -> None:
+ @computed_field # type: ignore[prop-decorator]
+ @property # TODO: Cache?
+ def bounding_box(self) -> AABB | None:
+ bounded_nodes = [c for c in self.children if c.bounding_box]
+ if not bounded_nodes:
+ # If there are no children declaring a bounding box, return None
+ return None
+ node_aabbs = [n.transform.map(n.bounding_box)[:, :3] for n in bounded_nodes] # type:ignore
+ mi = np.vstack([t[0] for t in node_aabbs]).min(axis=0)
+ ma = np.vstack([t[1] for t in node_aabbs]).max(axis=0)
+ # Note the casting is important for pydantic
+ # FIXME: Should just validate in pydantic
+ return (tuple(float(m) for m in mi), tuple(float(m) for m in ma)) # type: ignore
+
+ def add_child(self, child: AnyNode) -> None:
"""Add a child node to this node."""
self._children.append(child)
child.parent = cast("AnyNode", self)
self.child_added.emit(child)
- def remove_child(self, child: "AnyNode") -> None:
+ def remove_child(self, child: AnyNode) -> None:
"""Remove a child node from this node. Does not raise if child is missing."""
if child in self._children:
self._children.remove(child)
child.parent = None
self.child_removed.emit(child)
+ def passes_through(self, ray: Ray) -> float | None:
+ """Returns the depth t at which the provided ray intersects this node.
+
+ The ray, in this case, is defined by R(t) = ray_origin + ray_direction * t,
+ where t>=0
+
+ Parameters
+ ----------
+ ray : Ray
+ The ray passing through the scene
+
+ Returns
+ -------
+ t: float | None
+ The depth t at which the ray intersects the node, or None if it never
+ intersects.
+ """
+ raise RuntimeError("Must be implemented in subclasses")
+
@model_validator(mode="wrap")
@classmethod
def _validate_model(
cls,
value: Any,
- handler: ModelWrapValidatorHandler["Self"],
+ handler: ModelWrapValidatorHandler[Self],
info: ValidationInfo,
- ) -> "Self":
+ ) -> Self:
# Ensures that changing the parent of a node
# also updates the children of the new/old parent.
if isinstance(value, dict):
@@ -140,7 +227,7 @@ def _validate_model(
return result
@staticmethod
- def _update_parent_children(node: "Node", old_parent: "Node | None" = None) -> None:
+ def _update_parent_children(node: Node, old_parent: Node | None = None) -> None:
"""Remove the node from its old_parent and add it to its new parent."""
if (new_parent := node.parent) != old_parent:
if new_parent is not None and node not in new_parent._children:
@@ -165,7 +252,7 @@ def __contains__(self, item: object) -> bool:
# below borrowed from vispy.scene.Node
- def transform_to_node(self, other: "Node") -> Transform:
+ def transform_to_node(self, other: Node) -> Transform:
"""Return Transform that maps from coordinate frame of `self` to `other`.
Note that there must be a _single_ path in the scenegraph that connects
@@ -185,7 +272,7 @@ def transform_to_node(self, other: "Node") -> Transform:
tforms = [n.transform for n in a[:-1]] + [n.transform.inv() for n in b]
return Transform.chain(*tforms[::-1])
- def path_to_node(self, other: "Node") -> tuple[list["Node"], list["Node"]]:
+ def path_to_node(self, other: Node) -> tuple[list[Node], list[Node]]:
"""Return two lists describing the path from this node to another.
Parameters
@@ -229,7 +316,7 @@ def path_to_node(self, other: "Node") -> tuple[list["Node"], list["Node"]]:
down = their_parents[: their_parents.index(common_parent)][::-1]
return (up, down)
- def iter_parents(self) -> Iterator["Node"]:
+ def iter_parents(self) -> Iterator[Node]:
"""Return list of parents starting from this node.
The chain ends at the first node with no parents.
diff --git a/src/scenex/model/_nodes/points.py b/src/scenex/model/_nodes/points.py
index 20d0d879..94efe5e0 100644
--- a/src/scenex/model/_nodes/points.py
+++ b/src/scenex/model/_nodes/points.py
@@ -1,12 +1,16 @@
from __future__ import annotations
-from typing import Annotated, Any, Literal
+from typing import TYPE_CHECKING, Annotated, Any, Literal
+import numpy as np
from annotated_types import Interval
from cmap import Color
-from pydantic import Field
+from pydantic import Field, computed_field
-from .node import Node
+from .node import AABB, Node
+
+if TYPE_CHECKING:
+ from scenex.app.events._events import Ray
SymbolName = Literal[
"disc",
@@ -36,7 +40,7 @@ class Points(Node):
# numpy array of 2D/3D point centers, shape (N, 2) or (N, 3)
coords: Any = Field(default=None, repr=False, exclude=True)
size: Annotated[float, Interval(ge=0.5, le=100)] = Field(
- default=10.0, description="The size of the points."
+ default=10.0, description="The diameter of the points."
)
face_color: Color | None = Field(
default=Color("white"), description="The color of the faces."
@@ -54,3 +58,64 @@ class Points(Node):
)
antialias: float = Field(default=1, description="Anti-aliasing factor, in px.")
+
+ @computed_field # type: ignore[prop-decorator]
+ @property # TODO: Cache?
+ def bounding_box(self) -> AABB:
+ arr = np.asarray(self.coords)
+ return (
+ tuple(float(d) for d in np.min(arr, axis=0)),
+ tuple(float(d) for d in np.max(arr, axis=0)),
+ ) # type: ignore
+
+ def passes_through(self, ray: Ray) -> float | None:
+ # Math graciously adapted from:
+ # https://raytracing.github.io/books/RayTracingInOneWeekend.html#addingasphere/ray-sphereintersection
+
+ # Step 1 - Determine whether the ray passes through any points
+
+ # Convert coords to a 3-dimensional numpy array
+ coords = np.asarray(self.coords)
+ if coords.ndim < len(ray.origin):
+ coords = np.pad(
+ self.coords, ((0, 0), (0, 1)), mode="constant", constant_values=0
+ )
+ # And then transform the points to world space
+ coords = self.transform.map(coords)[:, :3]
+
+ # For each point, determine whether the ray passes through its sphere.
+ #
+ # The sphere is defined by the center n=(t, u, v) and a radius r such that any
+ # point p=(x, y, z) on the plane satisfies (t-x)^2 + (u-y)^2 + (v-z)^2 = r^2.
+ # Note that r is defined in our model as:
+ r = self.size / 2 + (self.edge_width if self.edge_width else 0)
+ # Note that our intersection point p could be any point along our ray, defined
+ # as (ray.origin + ray.direction * t). Substituting this definition into the
+ # sphere equation, yields a quadratic equation at^2 + bt + c = 0, where a, b,
+ # and c have the following definitions:
+ ray_diff = coords - ray.origin
+ a = np.dot(ray.direction, ray.direction)
+ b = -2 * np.dot(ray_diff, ray.direction)
+ c = np.sum(ray_diff * ray_diff, axis=1) - r**2
+
+ # And there is a sphere intersection if the equation's discriminant is
+ # non-negative:
+ discriminants = b**2 - 4 * a * c
+ intersecting_indices = np.where(discriminants >= 0)[0]
+ if not intersecting_indices.size:
+ return None
+
+ # Step 2 - Determine the depth of intersection
+
+ # We have (potentially) multiple points intersected by our ray, described
+ # by the variable t in our ray's definition. Let's focus on those:
+ b = b[intersecting_indices]
+ discriminants = discriminants[intersecting_indices]
+ # We only care about the closest such intersection, i.e. the smallest value of
+ # t. Thus, for each intersecting sphere we compute the one or two values of t
+ # where our ray intersects. Note that t = (-b +- sqrt(discriminant)) / 2a
+ pos_solutions = -b + np.sqrt(discriminants) / (2 * a)
+ neg_solutions = -b - np.sqrt(discriminants) / (2 * a)
+
+ # return the smallest such value:
+ return float(np.hstack((pos_solutions, neg_solutions)).min())
diff --git a/src/scenex/model/_nodes/text.py b/src/scenex/model/_nodes/text.py
new file mode 100644
index 00000000..3e540697
--- /dev/null
+++ b/src/scenex/model/_nodes/text.py
@@ -0,0 +1,41 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Literal
+
+from cmap import Color
+from pydantic import Field, computed_field
+
+from .node import AABB, Node
+
+if TYPE_CHECKING:
+ from scenex.app.events._events import Ray
+
+
+class Text(Node):
+ """A text label placed in world space."""
+
+ node_type: Literal["text"] = "text"
+
+ text: str = Field(default="", description="The displayed text.")
+ color: Color = Field(default=Color("white"), description="Text color.")
+ size: int = Field(default=12, ge=0, description="Font size in pixels.")
+
+ @computed_field # type: ignore[prop-decorator]
+ @property
+ def bounding_box(self) -> AABB:
+ # TODO: Bounding boxes for text are hard.
+ # First, they depend on the font, size, and actual text.
+ # Second, text is completely virtual - its "size" varies as a
+ # function of the camera to maintain size in the canvas space.
+ #
+ # Theoretically, we could compute a bounding box in screen space (leaning on
+ # FreeType) and then unproject that back into world space, but that seems
+ # overly complicated for now.
+ #
+ # Let's just return a point bounding box for now.
+ return ((-1e-6, -1e-6, -1e-6), (1e-6, 1e-6, 1e-6))
+
+ def passes_through(self, ray: Ray) -> float | None:
+ # TODO: This faces similar issues to the bounding box problem.
+ # Theoretically, we could compute intersection in canvas space.
+ return None
diff --git a/src/scenex/model/_nodes/volume.py b/src/scenex/model/_nodes/volume.py
index a4ac643d..9de84e90 100644
--- a/src/scenex/model/_nodes/volume.py
+++ b/src/scenex/model/_nodes/volume.py
@@ -1,8 +1,14 @@
-from typing import Literal
+from __future__ import annotations
-from pydantic import Field
+from typing import TYPE_CHECKING, Literal
-from .image import Image
+from pydantic import Field, computed_field
+
+from .image import Image, _passes_through_parallelogram
+from .node import AABB # noqa: TC001
+
+if TYPE_CHECKING:
+ from scenex.app.events._events import Ray
RenderMode = Literal["iso", "mip"]
@@ -14,3 +20,41 @@ class Volume(Image):
default="mip",
description="The method to use in rendering the volume.",
)
+
+ @computed_field # type: ignore[prop-decorator]
+ @property # TODO: Cache?
+ def bounding_box(self) -> AABB:
+ bb = super().bounding_box
+ # We can reuse the image version, but the first dimension needs to be swapped
+ # To account for the ZYX convention.
+ return ((bb[0][1], bb[0][2], bb[0][0]), (bb[1][1], bb[1][2], bb[1][0]))
+
+ def passes_through(self, ray: Ray) -> float | None:
+ # The ray passes through our volume if it passes through any of the six faces
+ mi, ma = self.bounding_box
+ d, w, h = self.data.shape
+
+ # We can describe each face using a parallelogram using:
+ # A point for the Top, Left, and Front faces
+ tlf = self.transform.map((mi[0], mi[1], mi[2], 1))[:3]
+ # Or a point for the Bottom, Right, and Back faces
+ brb = self.transform.map((ma[0], ma[1], ma[2], 1))[:3]
+ # As well as vectors describing the three edges eminating from tlf
+ u = self.transform.map((w, 0, 0, 0))[:3]
+ v = self.transform.map((0, h, 0, 0))[:3]
+ w = self.transform.map((0, 0, d, 0))[:3]
+
+ faces = [
+ # (origin, edge1, edge2)
+ (tlf, u, v), # front face
+ (tlf, v, w), # left face
+ (tlf, w, u), # top face
+ (brb, -u, -v), # back face
+ (brb, -v, -w), # right face
+ (brb, -w, -u), # bottom face
+ ]
+ # Compute the depths where the ray intersects each face
+ results = [_passes_through_parallelogram(ray, o, e1, e2) for o, e1, e2 in faces]
+ # And return the minimum depth in the case of multiple intersections.
+ depths = [r for r in results if r is not None]
+ return min(depths) if depths else None
diff --git a/src/scenex/model/_transform.py b/src/scenex/model/_transform.py
index 2c68b1b5..8a4de357 100644
--- a/src/scenex/model/_transform.py
+++ b/src/scenex/model/_transform.py
@@ -3,7 +3,7 @@
import functools
import math
from functools import reduce
-from typing import TYPE_CHECKING, Any, ClassVar, cast
+from typing import TYPE_CHECKING, Annotated, Any, ClassVar, cast
import numpy as np
from pydantic import ConfigDict, Field, RootModel
@@ -89,8 +89,8 @@ def _serialize(val: np.ndarray) -> list | None:
class Transform(RootModel):
"""A 4x4 transformation matrix placing a 3D object in 3D space."""
- root: Matrix3D = Field(
- default_factory=lambda: np.eye(4), # type: ignore
+ root: Annotated[np.ndarray, Matrix3D] = Field(
+ default_factory=lambda: np.eye(4),
description="4x4 Transformation matrix.",
)
@@ -116,7 +116,7 @@ def __matmul__(self, other: Transform | ArrayLike) -> Transform:
"""Return the dot product of this transform with another."""
if isinstance(other, Transform):
other = other.root
- return Transform(self.root @ other) # type: ignore
+ return Transform(self.root @ other)
def dot(self, other: Transform | ArrayLike) -> Transform:
"""Return the dot product of this transform with another."""
@@ -131,7 +131,7 @@ def T(self) -> Transform:
def inv(self) -> Transform:
"""Return the inverse of the transform."""
- return Transform(np.linalg.inv(self.root)) # type: ignore
+ return Transform(np.linalg.inv(self.root))
def translated(self, pos: ArrayLike) -> Transform:
"""Return new transform, translated by pos.
diff --git a/src/scenex/model/_view.py b/src/scenex/model/_view.py
index f0adf8d6..11a7e75c 100644
--- a/src/scenex/model/_view.py
+++ b/src/scenex/model/_view.py
@@ -13,9 +13,12 @@
from ._nodes.scene import Scene
if TYPE_CHECKING:
+ from collections.abc import Callable
+
import numpy as np
from scenex.adaptors._base import ViewAdaptor
+ from scenex.app.events import Event
from ._canvas import Canvas
@@ -63,10 +66,77 @@ def canvas(self) -> Canvas:
@canvas.setter
def canvas(self, value: Canvas) -> None:
self._canvas = value
- self._canvas.views.append(self)
+ if self not in value.views:
+ value.views.append(self)
def render(self) -> np.ndarray:
"""Render the view to an array."""
if adaptors := self._get_adaptors():
return cast("ViewAdaptor", adaptors[0])._snx_render()
raise RuntimeError("No adaptor found for View.")
+
+ _filter: Callable[[Event], bool] | None = PrivateAttr(default=None)
+
+ def set_event_filter(
+ self, callable: Callable[[Event], bool] | None
+ ) -> Callable[[Event], bool] | None:
+ """
+ Registers a callable to filter events.
+
+ Parameters
+ ----------
+ callable : Callable[[Event], bool] | None
+ A callable that takes an Event and returns True if the event was handled,
+ False otherwise. Passing None is equivalent to removing any existing filter.
+ By returning True, the callable indicates that the event has been handled
+ and should not be propagated to subsequent handlers.
+
+ Returns
+ -------
+ Callable[[Event], bool] | None
+ The previous event filter, or None if there was no filter.
+
+ Note the name has parity with Node.filter_event, but there's not much filtering
+ going on.
+ """
+ old, self._filter = self._filter, callable
+ return old
+
+ def filter_event(self, event: Event) -> bool:
+ """
+ Filters the event.
+
+ This method allows the larger view to react to events that:
+ 1. Require summarization of multiple smaller event responses.
+ 2. Could not be picked up by a node (e.g. mouse leaving an image).
+
+ Note the name has parity with Node.filter_event, but there's not much filtering
+ going on.
+
+ Parameters
+ ----------
+ event : Event
+ An event occurring in the view.
+
+ Returns
+ -------
+ bool
+ True iff the event should not be propagated to other handlers.
+ """
+ if self._filter:
+ handled = self._filter(event)
+ if not isinstance(handled, bool):
+ # Some widget frameworks (i.e. Qt) get upset when non-booleans are
+ # returned. If the event-filter does not return a boolean, rather than
+ # letting that propagate upwards, we log a warning and return False.
+ logger.warning(
+ f"Event filter {self._filter} did not return a boolean. "
+ "Returning False."
+ )
+ # Return False. We assume that if the user wanted to block future
+ # processing, they'd be less likely to forget a boolean return.
+ # Further, allowing downstream processing is a clear sign to the author
+ # that they forgot to block propagation.
+ handled = False
+ return handled
+ return False
diff --git a/src/scenex/util.py b/src/scenex/util.py
index f42051f8..bdc6b6d4 100644
--- a/src/scenex/util.py
+++ b/src/scenex/util.py
@@ -8,6 +8,8 @@
from typing import TYPE_CHECKING, Any, Protocol
from scenex import model
+from scenex.app import app
+from scenex.utils import projections
if TYPE_CHECKING:
from typing import TypeAlias
@@ -118,14 +120,19 @@ def show(
scene = model.Scene(children=[obj])
view = model.View(scene=scene)
- canvas = model.Canvas(views=[view]) # pyright: ignore[reportArgumentType]
+ canvas = model.Canvas(
+ views=[view], # pyright: ignore[reportArgumentType]
+ # Respect the view size if provided
+ width=view.layout.width, # pyright: ignore[reportArgumentType]
+ height=view.layout.height, # pyright: ignore[reportArgumentType]
+ ) # pyright: ignore[reportArgumentType]
canvas.visible = True
reg = get_adaptor_registry(backend=backend)
reg.get_adaptor(canvas, create=True)
+ app().create_app()
for view in canvas.views:
- cam = reg.get_adaptor(view.camera)
- cam._snx_zoom_to_fit(0.1)
+ projections.zoom_to_fit(view, zoom_factor=0.9)
# logger.debug("SHOW MODEL %s", tree_repr(view.scene))
# native_scene = view.scene._get_native()
diff --git a/src/scenex/utils/controllers.py b/src/scenex/utils/controllers.py
new file mode 100644
index 00000000..9d2b930c
--- /dev/null
+++ b/src/scenex/utils/controllers.py
@@ -0,0 +1,215 @@
+"""Controllers for camera nodes."""
+
+import math
+
+import numpy as np
+import pylinalg as la
+
+from scenex.app.events._events import (
+ Event,
+ MouseButton,
+ MouseEvent,
+ MouseMoveEvent,
+ MousePressEvent,
+ Ray,
+ WheelEvent,
+)
+from scenex.model import Camera, Node
+
+
+class OrbitController:
+ """
+ Orbits a Camera node around a fixed point.
+
+ Rotation direction follows pygfx precedent, where foreground objects (between the
+ camera and the center of rotation) move in the direction of mouse movement i.e.
+ foreground objects move right when the mouse moves right, and up when the mouse
+ moves up.
+
+ Orbit controls define a polar axis (the Z axis in this case), and allow user
+ interaction to adjust the camera's angle around the polar axis (azimuth) and angle
+ to the polar axis (elevation).
+
+ The left mouse button orbits/rotates the camera.
+ Right mouse button: pan.
+ Wheel: zoom to point.
+ """
+
+ def __init__(self, center: tuple[float, float, float] = (0.0, 0.0, 0.0)) -> None:
+ self.center = np.array(center, dtype=float)
+ self.polar_axis = np.array((0.0, 0.0, 1.0), dtype=float)
+ self._last_canvas_pos: tuple[float, float] | None = None
+ self._pan_ray: Ray | None = None
+
+ def __call__(self, event: Event, node: Node) -> bool:
+ """Handle mouse and wheel events to orbit the camera."""
+ # Only operate on INTERACTIVE Camera nodes
+ if not isinstance(node, Camera) or not node.interactive:
+ return False
+
+ handled = False
+ # Orbit on mouse move with left button held
+ if (
+ isinstance(event, MouseMoveEvent)
+ and event.buttons == MouseButton.LEFT
+ and self._last_canvas_pos is not None
+ ):
+ # The process of orbiting is as follows:
+ # 1. Compute the azimuth and elevation changes based on mouse movement.
+ # - Azimuth describes the angle between the the positive X axis and
+ # the projection of the camera's position onto the XY plane.
+ # - Elevation describes the angle between the camera's position and
+ # the positive Z axis.
+ # 2. Ensure these changes are clamped to valid ranges (only really
+ # applies to elevation).
+ # 3. Adjust the current transform by:
+ # a. Translating by the negative of the centerpoint, to take it out of
+ # the computation.
+ # b. Rotating to adjust the elevation. The axis of rotation is defined
+ # by the camera's right vector. Note that this is done before the
+ # azimuth adjustment because that adjustment will alter the
+ # camera's right vector.
+ # c. Rotating to adjust the azimuth. The axis of rotation is always
+ # the positive Z axis.
+ # d. Translating by the centerpoint, to reorient the camera around
+ # that centerpoint.
+
+ # Step 0: Gather transform components, relative to camera center
+ orbit_mat = node.transform.translated(-self.center)
+ position, _rotation, _scale = la.mat_decompose(orbit_mat.T)
+ # TODO: Make this a controller parameter
+ camera_polar = (0, 0, 1)
+ camera_right = np.cross(node.forward, node.up)
+
+ # Step 1
+ d_azimuth = self._last_canvas_pos[0] - event.canvas_pos[0]
+ d_elevation = self._last_canvas_pos[1] - event.canvas_pos[1]
+
+ # Step 2
+ e_bound = float(la.vec_angle(position, (0, 0, 1)) * 180 / math.pi)
+ if e_bound + d_elevation < 0:
+ d_elevation = -e_bound
+ if e_bound + d_elevation > 180:
+ d_elevation = 180 - e_bound
+
+ # Step 3
+ node.transform = (
+ node.transform.translated(-self.center) # 3a
+ .rotated(d_elevation, camera_right) # 3b
+ .rotated(d_azimuth, camera_polar) # 3c
+ .translated(self.center) # 3d
+ )
+
+ # Step n+1: Update last position
+ self._last_canvas_pos = self._last_canvas_pos
+ handled = True
+
+ # Pan on mouse move with right button held
+ elif isinstance(event, MousePressEvent) and event.buttons == MouseButton.RIGHT:
+ self._pan_ray = event.world_ray
+
+ # Pan on mouse move with right button held
+ elif (
+ isinstance(event, MouseMoveEvent)
+ and event.buttons == MouseButton.RIGHT
+ and self._pan_ray is not None
+ ):
+ dr = np.linalg.norm(node.transform.map((0, 0, 0))[:3] - self.center)
+ old_center = self._pan_ray.origin[:3] + np.multiply(
+ dr, self._pan_ray.direction
+ )
+ new_center = event.world_ray.origin[:3] + np.multiply(
+ dr, event.world_ray.direction
+ )
+ diff = np.subtract(old_center, new_center)
+ node.transform = node.transform.translated(diff)
+ self.center += diff
+ handled = True
+
+ elif isinstance(event, WheelEvent):
+ _dx, dy = event.angle_delta
+ if dy:
+ dr = node.transform.map((0, 0, 0))[:3] - self.center
+ zoom = self._zoom_factor(dy)
+ node.transform = node.transform.translated(dr * (zoom - 1))
+ handled = True
+
+ if isinstance(event, MouseEvent):
+ self._last_canvas_pos = event.canvas_pos
+ return handled
+
+ def _zoom_factor(self, delta: float) -> float:
+ # Magnifier stolen from pygfx
+ return 2 ** (-delta * 0.001)
+
+
+class PanZoomController:
+ """
+ Controller for handling pan and zoom interactions with a Camera node.
+
+ This class enables intuitive mouse-based panning and zooming in a 2D scene.
+ It tracks mouse events to allow dragging (panning) the camera view and
+ scroll wheel events to zoom in and out, keeping the cursor position fixed
+ under the mouse during zoom.
+ """
+
+ def __init__(self) -> None:
+ self._drag_pos: tuple[float, float] | None = None
+
+ def __call__(self, event: Event, node: Node) -> bool:
+ """Handle mouse and wheel events to pan/zoom the camera."""
+ # Only operate on INTERACTIVE Camera nodes
+ if not isinstance(node, Camera) or not node.interactive:
+ return False
+
+ handled = False
+
+ # Panning involves keeping a particular position underneath the cursor.
+ # That position is recorded on a left mouse button press.
+ if isinstance(event, MousePressEvent) and MouseButton.LEFT in event.buttons:
+ self._drag_pos = event.world_ray.origin[:2]
+ # Every time the cursor is moved, until the left mouse button is released,
+ # We translate the camera such that the position is back under the cursor
+ # (i.e. under the world ray origin)
+ elif (
+ isinstance(event, MouseMoveEvent)
+ and MouseButton.LEFT in event.buttons
+ and self._drag_pos
+ ):
+ new_pos = event.world_ray.origin[:2]
+ dx = self._drag_pos[0] - new_pos[0]
+ dy = self._drag_pos[1] - new_pos[1]
+ node.transform = node.transform.translated((dx, dy))
+ handled = True
+
+ # Note that while panning adjusts the camera's transform matrix, zooming
+ # adjusts the projection matrix.
+ elif isinstance(event, WheelEvent):
+ # Zoom while keeping the position under the cursor fixed.
+ _dx, dy = event.angle_delta
+ if dy:
+ # Step 1: Adjust the projection matrix to zoom in or out.
+ zoom = self._zoom_factor(dy)
+ node.projection = node.projection.scaled((zoom, zoom, 1.0))
+
+ # Step 2: Adjust the transform matrix to maintain the position
+ # under the cursor. The math is largely borrowed from
+ # https://github.com/pygfx/pygfx/blob/520af2d5bb2038ec309ef645e4a60d502f00d181/pygfx/controllers/_panzoom.py#L164
+
+ # Find the distance between the world ray and the camera
+ zoom_center = np.asarray(event.world_ray.origin)[:2]
+ camera_center = np.asarray(node.transform.map((0, 0)))[:2]
+ # Compute the world distance before the zoom
+ delta_screen1 = zoom_center - camera_center
+ # Compute the world distance after the zoom
+ delta_screen2 = delta_screen1 * zoom
+ # The pan is the difference between the two
+ pan = (delta_screen2 - delta_screen1) / zoom
+ node.transform = node.transform.translated(pan)
+ handled = True
+
+ return handled
+
+ def _zoom_factor(self, delta: float) -> float:
+ # Magnifier stolen from pygfx
+ return 2 ** (delta * 0.001)
diff --git a/src/scenex/utils/projections.py b/src/scenex/utils/projections.py
new file mode 100644
index 00000000..72e84633
--- /dev/null
+++ b/src/scenex/utils/projections.py
@@ -0,0 +1,145 @@
+"""Utilities for creating projection matrices."""
+
+from __future__ import annotations
+
+from math import pi, tan
+from typing import TYPE_CHECKING, Literal
+
+import numpy as np
+
+# FIXME the top import (commented out) does not work due to a circular import
+# Because the Camera model uses orthographic for its default transform.
+# Might want to think about a better organization.
+# from scenex.model import Transform
+from scenex.model._transform import Transform
+
+if TYPE_CHECKING:
+ from scenex.model import View
+
+
+def orthographic(width: float = 1, height: float = 1, depth: float = 1) -> Transform:
+ """Creates an orthographic projection matrix.
+
+ Note that the resulting projection matrix provides no positional offset; this would
+ be out of scope, as such is the job of a camera's transform parameter.
+
+ TODO: Consider passing bounds (i.e. a tuple[float, float]) for each parameter.
+ Unfortunately, though, this would effectively allow positional offsets for width and
+ height.
+
+ Parameters
+ ----------
+ width: float, optional
+ The width of the camera rectangular prism. Default 1 (mirroring the side length
+ of a unit cube).
+ height: float, optional
+ The height of the camera rectangular prism. Default 1 (mirroring the side length
+ of a unit cube).
+ depth: float, optional
+ The depth of the camera rectangular prism. The near and far clipping planes of
+ the resulting matrix become (-depth / 2) and (depth / 2) respectively. Default
+ 1, increase (to render things farther away) or decrease (to increase
+ performance) as needed.
+
+ TODO: Is this a good default? May want to consider some large number (1000?)
+ instead
+
+ Returns
+ -------
+ projection: Transform
+ A Transform matrix creating an orthographic camera view
+ """
+ width = width if width else 1e-6
+ height = height if height else 1e-6
+ depth = depth if depth else 1e-6
+ return Transform().scaled((2 / width, 2 / height, -2 / depth))
+
+
+def perspective(fov: float, near: float, far: float) -> Transform:
+ """Creates a perspective projection matrix.
+
+ Note that the resulting projection matrix provides no positional offset; this would
+ be out of scope, as such is the job of a camera's transform parameter.
+
+ Parameters
+ ----------
+ fov: float
+ The field of view of the camera rectangle.
+ near: float
+ The distance from the camera to the near clipping plane.
+ far: float
+ The distance from the camera to the far clipping plane.
+
+ Returns
+ -------
+ projection: Transform
+ A Transform matrix creating a perspective camera view
+ """
+ if fov == 0:
+ raise ValueError(
+ "Perspective matrices require fov>0. Maybe consider an orthographic matrix?"
+ )
+
+ matrix = np.zeros((4, 4))
+
+ # Computations derived from
+ # https://www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographic-projection-matrix/building-basic-perspective-projection-matrix.html
+ scaling_factor = 1 / (tan(fov / 2 * pi / 180))
+ matrix[0, 0] = scaling_factor
+ matrix[1, 1] = scaling_factor
+
+ z_scale = -1 * far / (far - near)
+ matrix[2, 2] = z_scale
+ z_translation = -1 * far * near / (far - near)
+ matrix[2, 3] = z_translation
+
+ matrix[3, 2] = -1
+ return Transform(root=matrix)
+
+
+# TODO: perspective mode?
+# TODO: Preserve some camera state?
+def zoom_to_fit(
+ view: View,
+ type: Literal["perspective", "orthographic"] = "orthographic",
+ zoom_factor: float = 1.0,
+) -> None:
+ """Adjusts the Camera to fit the entire scene.
+
+ Parameters
+ ----------
+ view: View
+ The view to adjust. Contains the camera, whose parameters will be adjusted, and
+ the scene, whose elements will be considered in the adjustment.
+ type: Literal["perspective", "orthographic"]
+ The type of canvas projection to use. Orthographic by default.
+ zoom_factor: float
+ The amount to zoom the scene after adjusting camera parameters. The default,
+ 1.0, will leave the scene touching the edges of the view. As the zoom factor
+ approaches 0, the scene will linearly decrease in size. As the zoom factor
+ increases beyond 1.0, the bounds of the scene will expand linearly beyond the
+ view.
+ """
+ # TODO: Test whether the camera can affect the bounding box...
+
+ bb = view.scene.bounding_box
+ center = np.mean(bb, axis=0) if bb else (0, 0, 0)
+ w, h, d = np.ptp(bb, axis=0) if bb else (1, 1, 1)
+ if type == "orthographic":
+ view.camera.transform = Transform().translated(center)
+ view.camera.projection = orthographic(w, h, d).scaled([zoom_factor] * 3)
+ elif type == "perspective":
+ # Compute the distance a to the near plane of the frustum using a default fov
+ o = max(w, h) / 2
+ fov = 70
+ a = o / tan(fov * pi / 360) / zoom_factor
+
+ # So that the bounding cube's front plane is mapped to the canvas,
+ # the camera must be a units away from the front plane (at z=(center[2] + d/2))
+ z_bound = center[2] + (d / 2) + a
+ view.camera.transform = Transform().translated((center[0], center[1], z_bound))
+ # Note that the near and far planes are set to reasonable defaults.
+ # TODO: Consider making these parameters
+ view.camera.projection = perspective(fov, near=1, far=1_000_000)
+ else:
+ raise TypeError(f"Unrecognized projection type: {type}")
diff --git a/tests/adaptors/_pygfx/test_camera.py b/tests/adaptors/_pygfx/test_camera.py
new file mode 100644
index 00000000..bfd5b3f6
--- /dev/null
+++ b/tests/adaptors/_pygfx/test_camera.py
@@ -0,0 +1,71 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import numpy as np
+import pygfx.cameras as pygfx
+import pytest
+
+import scenex as snx
+import scenex.adaptors._pygfx as adaptors
+from scenex.adaptors import get_adaptor_registry
+from scenex.model._transform import Transform
+from scenex.utils import projections
+
+if TYPE_CHECKING:
+ from collections.abc import Generator
+
+
+@pytest.fixture
+def camera() -> Generator[tuple[snx.Camera, adaptors.Camera], None, None]:
+ view = snx.View(camera=snx.Camera())
+ adaptor = get_adaptor_registry().get_adaptor(view.camera, create=True)
+ get_adaptor_registry().get_adaptor(view, create=True)
+ assert isinstance(adaptor, adaptors.Camera)
+ # TODO: Do we need to hold on to a view ref?
+ yield (view.camera, adaptor)
+
+
+def test_transform_defaults(camera: tuple[snx.Camera, adaptors.Camera]) -> None:
+ model, adaptor = camera
+
+ node = adaptor._pygfx_node
+ assert isinstance(node, pygfx.Camera)
+ # Centered at [0, 0], top left [-1, -1], bottom right [1, 1]
+ assert model.transform == Transform()
+ assert model.projection == projections.orthographic(2, 2, 2)
+
+ # Assert internal pygfx matrices match the model matrices
+ # Note that pygfx matrices are transposes of scenex matrices
+ assert np.array_equal(node.local.matrix.T, Transform())
+ assert np.array_equal(node.projection_matrix.T, projections.orthographic(2, 2, 2)) # pyright: ignore[reportAttributeAccessIssue]
+
+
+def test_transform_translate(camera: tuple[snx.Camera, adaptors.Camera]) -> None:
+ model, adaptor = camera
+
+ node = adaptor._pygfx_node
+ assert isinstance(node, pygfx.Camera)
+
+ # Move the camera
+ model.transform = Transform().translated((0.5, 0.5))
+
+ # Assert internal pygfx matrices match the expected model matrices
+ # Note that pygfx matrices are transposes of scenex matrices
+ assert np.array_equal(node.local.matrix.T, Transform().translated((0.5, 0.5)))
+ assert np.array_equal(node.projection_matrix.T, projections.orthographic(2, 2, 2)) # pyright: ignore[reportAttributeAccessIssue]
+
+
+def test_transform_scale(camera: tuple[snx.Camera, adaptors.Camera]) -> None:
+ model, adaptor = camera
+
+ node = adaptor._pygfx_node
+ assert isinstance(node, pygfx.Camera)
+
+ # Widen the projection matrix
+ model.projection = projections.orthographic(4, 4, 4)
+
+ # Assert internal pygfx matrices match the expected model matrices
+ # Note that pygfx matrices are transposes of scenex matrices
+ assert np.array_equal(node.local.matrix.T, Transform())
+ assert np.array_equal(node.projection_matrix.T, projections.orthographic(4, 4, 4)) # pyright: ignore[reportAttributeAccessIssue]
diff --git a/tests/adaptors/_pygfx/test_image.py b/tests/adaptors/_pygfx/test_image.py
index 3bfce80b..b4591c3b 100644
--- a/tests/adaptors/_pygfx/test_image.py
+++ b/tests/adaptors/_pygfx/test_image.py
@@ -1,5 +1,6 @@
from __future__ import annotations
+import cmap
import numpy as np
import pytest
@@ -13,6 +14,7 @@
def image() -> snx.Image:
return snx.Image(
data=np.random.randint(0, 255, (100, 100), dtype=np.uint8),
+ cmap=cmap.Colormap("viridis"),
)
@@ -62,3 +64,33 @@ def test_transform(image: snx.Image, adaptor: adaptors.Image) -> None:
bb = adaptor._pygfx_node.get_world_bounding_box()
assert bb is not None
assert np.array_equal(exp_bounds, bb)
+
+
+def test_rgb(image: snx.Image, adaptor: adaptors.Image) -> None:
+ """Tests RGB(A) images are correctly massaged to avoid shading errors."""
+ # Assert a colormap can be used with 2D data
+ image.data = np.random.randint(0, 255, (100, 100), dtype=np.uint8)
+ image.cmap = cmap.Colormap("red")
+ np.testing.assert_array_equal(image.data, adaptor._pygfx_node.geometry.grid.data) # pyright: ignore
+ np.testing.assert_array_equal(
+ cmap.Colormap("red").to_pygfx().texture.data,
+ adaptor._pygfx_node.material.map.texture.data, # pyright: ignore
+ )
+
+ # When the data changes to RGB, the adaptor's material map should be None
+ image.data = np.random.randint(0, 255, (100, 100, 3), dtype=np.uint8)
+ np.testing.assert_array_equal(image.data, adaptor._pygfx_node.geometry.grid.data) # pyright: ignore
+ assert adaptor._pygfx_node.material.map is None # pyright: ignore
+
+ # Even if the cmap is set, it should be ignored
+ image.cmap = cmap.Colormap("blue")
+ np.testing.assert_array_equal(image.data, adaptor._pygfx_node.geometry.grid.data) # pyright: ignore
+ assert adaptor._pygfx_node.material.map is None # pyright: ignore
+
+ # But it should snap to place if we go back to 2D data
+ image.data = np.random.randint(0, 255, (100, 100), dtype=np.uint8)
+ np.testing.assert_array_equal(image.data, adaptor._pygfx_node.geometry.grid.data) # pyright: ignore
+ np.testing.assert_array_equal(
+ cmap.Colormap("blue").to_pygfx().texture.data,
+ adaptor._pygfx_node.material.map.texture.data, # type: ignore
+ )
diff --git a/tests/adaptors/_pygfx/test_text.py b/tests/adaptors/_pygfx/test_text.py
new file mode 100644
index 00000000..6cc0e90b
--- /dev/null
+++ b/tests/adaptors/_pygfx/test_text.py
@@ -0,0 +1,44 @@
+from __future__ import annotations
+
+import cmap
+import numpy as np
+import pytest
+
+import scenex as snx
+import scenex.adaptors._pygfx as adaptors
+from scenex.adaptors._auto import get_adaptor_registry
+
+
+@pytest.fixture
+def text() -> snx.Text:
+ return snx.Text(
+ text="Hello, World!",
+ color=cmap.Color("red"),
+ size=12,
+ )
+
+
+@pytest.fixture
+def adaptor(text: snx.Text) -> adaptors.Text:
+ adaptor = get_adaptor_registry().get_adaptor(text, create=True)
+ assert isinstance(adaptor, adaptors.Text)
+ return adaptor
+
+
+def test_data(text: snx.Text, adaptor: adaptors.Text) -> None:
+ """Tests that changing the model changes the view (the PyGfx node)."""
+ mat = adaptor._pygfx_node.material
+ assert mat is not None
+
+ assert np.array_equal(text.text, adaptor._pygfx_node._text_blocks[0]._input[1])
+ text.text = "Goodbye, World!"
+ assert np.array_equal(text.text, adaptor._pygfx_node._text_blocks[0]._input[1])
+
+ assert np.array_equal(text.size, adaptor._pygfx_node.font_size)
+ text.size = 24
+ assert np.array_equal(text.size, adaptor._pygfx_node.font_size)
+
+ assert text.color is not None
+ assert np.array_equal(text.color.rgba, mat.color.rgba) # pyright: ignore
+ text.color = cmap.Color("blue")
+ assert np.array_equal(text.color.rgba, mat.color.rgba) # pyright: ignore
diff --git a/tests/adaptors/_pygfx/test_volume.py b/tests/adaptors/_pygfx/test_volume.py
index b04c740a..2a03a2d3 100644
--- a/tests/adaptors/_pygfx/test_volume.py
+++ b/tests/adaptors/_pygfx/test_volume.py
@@ -1,12 +1,13 @@
from __future__ import annotations
import numpy as np
+import pygfx
import pytest
import scenex as snx
import scenex.adaptors._pygfx as adaptors
from scenex.adaptors._auto import get_adaptor_registry
-from scenex.model._transform import Transform
+from scenex.model import BlendMode, Transform
@pytest.fixture
@@ -58,3 +59,17 @@ def test_transform(volume: snx.Volume, adaptor: adaptors.Volume) -> None:
bb = adaptor._pygfx_node.get_world_bounding_box()
assert bb is not None
assert np.array_equal(exp_bounds, bb)
+
+
+@pytest.mark.skipif(
+ pygfx.version_info < (0, 13, 0), reason="Requires pygfx 0.13.0 or higher"
+)
+def test_blending(volume: snx.Volume, adaptor: adaptors.Volume) -> None:
+ volume.blending = BlendMode.ADDITIVE
+ assert adaptor._material.alpha_mode == "add"
+
+ volume.blending = BlendMode.ALPHA
+ assert adaptor._material.alpha_mode == "auto"
+
+ volume.blending = BlendMode.OPAQUE
+ assert adaptor._material.alpha_mode == "solid"
diff --git a/tests/adaptors/_vispy/test_camera.py b/tests/adaptors/_vispy/test_camera.py
new file mode 100644
index 00000000..f178b283
--- /dev/null
+++ b/tests/adaptors/_vispy/test_camera.py
@@ -0,0 +1,95 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import numpy as np
+import pytest
+from vispy.scene.cameras import BaseCamera
+
+import scenex as snx
+import scenex.adaptors._vispy as adaptors
+from scenex.adaptors import get_adaptor_registry
+from scenex.model._transform import Transform
+from scenex.utils import projections
+
+if TYPE_CHECKING:
+ from collections.abc import Generator
+
+
+@pytest.fixture
+def camera() -> Generator[tuple[snx.Camera, adaptors.Camera], None, None]:
+ view = snx.View(camera=snx.Camera())
+ adaptor = get_adaptor_registry().get_adaptor(view.camera, create=True)
+ get_adaptor_registry().get_adaptor(view, create=True)
+ assert isinstance(adaptor, adaptors.Camera)
+ # TODO: Do we need to hold on to a view ref?
+ yield (view.camera, adaptor)
+
+
+def test_transform_defaults(camera: tuple[snx.Camera, adaptors.Camera]) -> None:
+ model, adaptor = camera
+ # Centered at [0, 0], top left [-1, -1], bottom right [1, 1]
+ assert model.transform == Transform()
+ assert model.projection == projections.orthographic(2, 2, 2)
+
+ node = adaptor._vispy_node
+ assert isinstance(node, BaseCamera)
+
+ w, h = node.viewbox.size # pyright: ignore[reportOptionalMemberAccess]
+ # Vispy wants to map [-1, 1] to [0, 0]
+ # Vispy wants to map [1, -1] to [w, h]
+ exp_tform_mat = np.asarray(
+ [
+ [w / 2, 0, 0, 0],
+ [0, -h / 2, 0, 0],
+ [0, 0, -1, 0],
+ [w / 2, h / 2, 0, 1],
+ ]
+ )
+ assert np.array_equal(node.transform.matrix, exp_tform_mat) # pyright: ignore[reportAttributeAccessIssue]
+
+
+def test_transform_translate(camera: tuple[snx.Camera, adaptors.Camera]) -> None:
+ model, adaptor = camera
+
+ node = adaptor._vispy_node
+ assert isinstance(node, BaseCamera)
+
+ # Move the camera
+ model.transform = Transform().translated((1, 1))
+
+ w, h = node.viewbox.size # pyright: ignore[reportOptionalMemberAccess]
+ # Vispy wants to map [0, 2] to [0, 0]
+ # Vispy wants to map [2, 0] to [w, h]
+ exp_tform_mat = np.asarray(
+ [
+ [w / 2, 0, 0, 0],
+ [0, -h / 2, 0, 0],
+ [0, 0, -1, 0],
+ [0, h, 0, 1],
+ ]
+ )
+ assert np.array_equal(node.transform.matrix, exp_tform_mat) # pyright: ignore[reportAttributeAccessIssue]
+
+
+def test_transform_scale(camera: tuple[snx.Camera, adaptors.Camera]) -> None:
+ model, adaptor = camera
+
+ node = adaptor._vispy_node
+ assert isinstance(node, BaseCamera)
+
+ # Widen the projection matrix
+ model.projection = projections.orthographic(4, 4, 4)
+
+ w, h = node.viewbox.size # pyright: ignore[reportOptionalMemberAccess]
+ # Vispy wants to map [-2, 2] to [0, 0]
+ # Vispy wants to map [2, -2] to [10, 10]
+ exp_tform_mat = np.asarray(
+ [
+ [w / 4, 0, 0, 0],
+ [0, -h / 4, 0, 0],
+ [0, 0, -1 / 2, 0],
+ [w / 2, h / 2, 0, 1],
+ ]
+ )
+ assert np.array_equal(node.transform.matrix, exp_tform_mat) # pyright: ignore[reportAttributeAccessIssue]
diff --git a/tests/adaptors/_vispy/test_text.py b/tests/adaptors/_vispy/test_text.py
new file mode 100644
index 00000000..63a5ead2
--- /dev/null
+++ b/tests/adaptors/_vispy/test_text.py
@@ -0,0 +1,43 @@
+from __future__ import annotations
+
+import cmap
+import numpy as np
+import pytest
+
+import scenex as snx
+import scenex.adaptors._vispy as adaptors
+from scenex.adaptors._auto import get_adaptor_registry
+
+
+@pytest.fixture
+def text() -> snx.Text:
+ return snx.Text(
+ text="Hello, World!",
+ color=cmap.Color("red"),
+ size=12,
+ )
+
+
+@pytest.fixture
+def adaptor(text: snx.Text) -> adaptors.Text:
+ adaptor = get_adaptor_registry().get_adaptor(text, create=True)
+ assert isinstance(adaptor, adaptors.Text)
+ return adaptor
+
+
+def test_data(text: snx.Text, adaptor: adaptors.Text) -> None:
+ """Tests that changing the model changes the view (the VisPy node)."""
+
+ assert np.array_equal(text.text, adaptor._vispy_node.text)
+ text.text = "Goodbye, World!"
+ assert np.array_equal(text.text, adaptor._vispy_node.text)
+
+ assert np.array_equal(text.size, adaptor._vispy_node.font_size)
+ text.size = 24
+ assert np.array_equal(text.size, adaptor._vispy_node.font_size)
+
+ assert text.color is not None
+ # NOTE: For some reason vispy TextVisual color is an array of colors
+ assert np.array_equal(text.color.rgba, adaptor._vispy_node.color.rgba[0]) # pyright: ignore
+ text.color = cmap.Color("blue")
+ assert np.array_equal(text.color.rgba, adaptor._vispy_node.color.rgba[0]) # pyright: ignore
diff --git a/tests/adaptors/_vispy/test_volume.py b/tests/adaptors/_vispy/test_volume.py
index 93e3f636..36ba196d 100644
--- a/tests/adaptors/_vispy/test_volume.py
+++ b/tests/adaptors/_vispy/test_volume.py
@@ -8,7 +8,7 @@
import scenex as snx
import scenex.adaptors._vispy as adaptors
from scenex.adaptors._auto import get_adaptor_registry
-from scenex.model._transform import Transform
+from scenex.model import BlendMode, Transform
if TYPE_CHECKING:
from vispy.visuals import VolumeVisual
@@ -65,6 +65,24 @@ def test_transform(volume: snx.Volume, adaptor: adaptors.Volume) -> None:
assert np.array_equal(exp_bounds, bb)
+def test_blending(volume: snx.Volume, adaptor: adaptors.Volume) -> None:
+ # Test blending modes
+ from unittest.mock import MagicMock
+
+ # Note that we can't just get the gl state, so we should assert that the correct
+ # settings were set.
+ adaptor._vispy_node.set_gl_state = MagicMock()
+
+ volume.blending = BlendMode.ADDITIVE
+ adaptor._vispy_node.set_gl_state.assert_called_with("additive")
+
+ volume.blending = BlendMode.ALPHA
+ adaptor._vispy_node.set_gl_state.assert_called_with("translucent")
+
+ volume.blending = BlendMode.OPAQUE
+ adaptor._vispy_node.set_gl_state.assert_called_with(None, blend=False)
+
+
def _bounds(node: VolumeVisual) -> np.ndarray:
bounds: np.ndarray = np.ndarray((2, 3), dtype=np.float32)
# Get the bounds of the raw object...
diff --git a/tests/app/test_jupyter.py b/tests/app/test_jupyter.py
new file mode 100644
index 00000000..c4b7845b
--- /dev/null
+++ b/tests/app/test_jupyter.py
@@ -0,0 +1,303 @@
+"""Tests pertaining to Jupyter event generation"""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, cast
+from unittest.mock import MagicMock
+
+import pytest
+
+import scenex as snx
+from scenex.adaptors._auto import determine_backend
+from scenex.app import GuiFrontend, determine_app
+from scenex.app.events import (
+ MouseButton,
+ MouseDoublePressEvent,
+ MouseEnterEvent,
+ MouseLeaveEvent,
+ MouseMoveEvent,
+ MousePressEvent,
+ MouseReleaseEvent,
+ Ray,
+ WheelEvent,
+)
+from scenex.model._transform import Transform
+
+if TYPE_CHECKING:
+ from scenex.adaptors._base import CanvasAdaptor
+
+if determine_app() != GuiFrontend.JUPYTER:
+ pytest.skip(
+ "Skipping Jupyter tests as Jupyter will not be used in this environment",
+ allow_module_level=True,
+ )
+
+# HACK: Enable tests inside vispy
+if determine_backend() == "vispy":
+ import asyncio
+ import os
+
+ os.environ["_VISPY_TESTING_APP"] = "jupyter_rfb"
+ asyncio.set_event_loop(asyncio.new_event_loop())
+
+ os.environ["SCENEX_WIDGET_BACKEND"] = "jupyter"
+
+
+@pytest.fixture
+def evented_canvas() -> snx.Canvas:
+ # IPython.getIPython().run_line_magic("gui", "inline")
+ camera = snx.Camera(transform=Transform(), interactive=True)
+ scene = snx.Scene(children=[])
+ view = snx.View(scene=scene, camera=camera)
+ canvas = snx.Canvas()
+ canvas.views.append(view)
+ return canvas
+
+
+def _validate_ray(maybe_ray: Ray | None) -> Ray:
+ assert maybe_ray is not None
+ return maybe_ray
+
+
+# See jupyter_rfb.events
+NONE = 0
+LEFT_MOUSE = 1
+RIGHT_MOUSE = 2
+
+
+def test_pointer_down(evented_canvas: snx.Canvas) -> None:
+ native = cast(
+ "CanvasAdaptor", evented_canvas._get_adaptors(create=True)[0]
+ )._snx_get_native()
+ mock = MagicMock()
+ evented_canvas.views[0].camera.set_event_filter(mock)
+ press_point = (0, 0)
+ # Press the left button
+ native.handle_event(
+ {
+ "event_type": "pointer_down",
+ "x": press_point[0],
+ "y": press_point[1],
+ "button": LEFT_MOUSE,
+ }
+ )
+ mock.assert_called_once_with(
+ MousePressEvent(
+ canvas_pos=press_point,
+ world_ray=_validate_ray(evented_canvas.to_world(press_point)),
+ buttons=MouseButton.LEFT,
+ ),
+ evented_canvas.views[0].camera,
+ )
+ mock.reset_mock()
+
+ # Now press the right button as well
+ native.handle_event(
+ {
+ "event_type": "pointer_down",
+ "x": press_point[0],
+ "y": press_point[1],
+ "button": RIGHT_MOUSE,
+ }
+ )
+ mock.assert_called_once_with(
+ MousePressEvent(
+ canvas_pos=press_point,
+ world_ray=_validate_ray(evented_canvas.to_world(press_point)),
+ buttons=MouseButton.RIGHT,
+ ),
+ evented_canvas.views[0].camera,
+ )
+
+
+def test_pointer_up(evented_canvas: snx.Canvas) -> None:
+ native = cast(
+ "CanvasAdaptor", evented_canvas._get_adaptors(create=True)[0]
+ )._snx_get_native()
+ mock = MagicMock()
+ evented_canvas.views[0].camera.set_event_filter(mock)
+ press_point = (0, 0)
+ native.handle_event(
+ {
+ "event_type": "pointer_up",
+ "x": press_point[0],
+ "y": press_point[1],
+ "button": LEFT_MOUSE,
+ }
+ )
+ mock.assert_called_once_with(
+ MouseReleaseEvent(
+ canvas_pos=press_point,
+ world_ray=_validate_ray(evented_canvas.to_world(press_point)),
+ buttons=MouseButton.LEFT,
+ ),
+ evented_canvas.views[0].camera,
+ )
+
+
+def test_pointer_move(evented_canvas: snx.Canvas) -> None:
+ native = cast(
+ "CanvasAdaptor", evented_canvas._get_adaptors(create=True)[0]
+ )._snx_get_native()
+ mock = MagicMock()
+ evented_canvas.views[0].camera.set_event_filter(mock)
+ press_point = (0, 0)
+ native.handle_event(
+ {
+ "event_type": "pointer_move",
+ "x": press_point[0],
+ "y": press_point[1],
+ "button": LEFT_MOUSE,
+ }
+ )
+ mock.assert_called_once_with(
+ MouseMoveEvent(
+ canvas_pos=press_point,
+ world_ray=_validate_ray(evented_canvas.to_world(press_point)),
+ buttons=MouseButton.LEFT,
+ ),
+ evented_canvas.views[0].camera,
+ )
+ mock.reset_mock()
+
+ native.handle_event(
+ {
+ "event_type": "pointer_move",
+ "x": press_point[0],
+ "y": press_point[1],
+ "buttons": (LEFT_MOUSE, RIGHT_MOUSE),
+ }
+ )
+ mock.assert_called_once_with(
+ MouseMoveEvent(
+ canvas_pos=press_point,
+ world_ray=_validate_ray(evented_canvas.to_world(press_point)),
+ buttons=MouseButton.LEFT | MouseButton.RIGHT,
+ ),
+ evented_canvas.views[0].camera,
+ )
+
+
+def test_mouse_double_click(evented_canvas: snx.Canvas) -> None:
+ native = cast(
+ "CanvasAdaptor", evented_canvas._get_adaptors(create=True)[0]
+ )._snx_get_native()
+ mock = MagicMock()
+ evented_canvas.views[0].camera.set_event_filter(mock)
+ press_point = (0, 0)
+ native.handle_event(
+ {
+ "event_type": "double_click",
+ "x": press_point[0],
+ "y": press_point[1],
+ "button": LEFT_MOUSE,
+ }
+ )
+ mock.assert_called_once_with(
+ MouseDoublePressEvent(
+ canvas_pos=press_point,
+ world_ray=_validate_ray(evented_canvas.to_world(press_point)),
+ buttons=MouseButton.LEFT,
+ ),
+ evented_canvas.views[0].camera,
+ )
+
+
+def test_wheel(evented_canvas: snx.Canvas) -> None:
+ native = cast(
+ "CanvasAdaptor", evented_canvas._get_adaptors(create=True)[0]
+ )._snx_get_native()
+ mock = MagicMock()
+ evented_canvas.views[0].camera.set_event_filter(mock)
+ press_point = (0, 0)
+ native.handle_event(
+ {
+ "event_type": "wheel",
+ "x": press_point[0],
+ "y": press_point[1],
+ "dx": 0,
+ "dy": -120, # Note that Jupyter_rfb uses a different y convention
+ }
+ )
+ mock.assert_called_once_with(
+ WheelEvent(
+ canvas_pos=press_point,
+ world_ray=_validate_ray(evented_canvas.to_world(press_point)),
+ buttons=MouseButton.NONE,
+ angle_delta=(0, 120),
+ ),
+ evented_canvas.views[0].camera,
+ )
+
+
+def test_resize(evented_canvas: snx.Canvas) -> None:
+ native = cast(
+ "CanvasAdaptor", evented_canvas._get_adaptors(create=True)[0]
+ )._snx_get_native()
+ mock = MagicMock()
+ evented_canvas.views[0].camera.set_event_filter(mock)
+ new_size = (400, 300)
+ assert evented_canvas.width != new_size[0]
+ assert evented_canvas.height != new_size[1]
+ native.handle_event(
+ {
+ "event_type": "resize",
+ "width": new_size[0],
+ "height": new_size[1],
+ }
+ )
+ assert evented_canvas.width == new_size[0]
+ assert evented_canvas.height == new_size[1]
+
+
+def test_pointer_enter(evented_canvas: snx.Canvas) -> None:
+ native = cast(
+ "CanvasAdaptor", evented_canvas._get_adaptors(create=True)[0]
+ )._snx_get_native()
+ view_mock = MagicMock()
+ evented_canvas.views[0].set_event_filter(view_mock)
+ enter_point = (0, 0)
+ native.handle_event(
+ {
+ "event_type": "pointer_enter",
+ "x": enter_point[0],
+ "y": enter_point[1],
+ "button": NONE,
+ }
+ )
+ # Verify MouseEnterEvent was passed to view filter
+ view_mock.assert_called_once_with(
+ MouseEnterEvent(
+ canvas_pos=enter_point,
+ world_ray=_validate_ray(evented_canvas.to_world(enter_point)),
+ buttons=MouseButton.NONE,
+ )
+ )
+
+
+def test_pointer_leave(evented_canvas: snx.Canvas) -> None:
+ native = cast(
+ "CanvasAdaptor", evented_canvas._get_adaptors(create=True)[0]
+ )._snx_get_native()
+ view_mock = MagicMock()
+ evented_canvas.views[0].set_event_filter(view_mock)
+ enter_point = (0, 0)
+ native.handle_event(
+ {
+ "event_type": "pointer_enter",
+ "x": enter_point[0],
+ "y": enter_point[1],
+ "button": NONE,
+ }
+ )
+ view_mock.reset_mock()
+
+ # Now leave
+ native.handle_event(
+ {
+ "event_type": "pointer_leave",
+ }
+ )
+
+ # Verify MouseLeaveEvent was passed to view filter
+ view_mock.assert_called_once_with(MouseLeaveEvent())
diff --git a/tests/app/test_qt.py b/tests/app/test_qt.py
new file mode 100644
index 00000000..438ff59c
--- /dev/null
+++ b/tests/app/test_qt.py
@@ -0,0 +1,264 @@
+"""Tests pertaining to VisPy adaptors"""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, cast
+from unittest.mock import MagicMock
+
+import pytest
+
+import scenex as snx
+from scenex.app import GuiFrontend, app, determine_app
+from scenex.app.events import (
+ MouseButton,
+ MouseDoublePressEvent,
+ MouseEnterEvent,
+ MouseLeaveEvent,
+ MouseMoveEvent,
+ MousePressEvent,
+ MouseReleaseEvent,
+ Ray,
+)
+from scenex.model._transform import Transform
+
+if TYPE_CHECKING:
+ from scenex.adaptors._base import CanvasAdaptor
+
+if determine_app() == GuiFrontend.QT:
+ from qtpy.QtCore import QEvent, QPoint, QPointF, Qt
+ from qtpy.QtGui import QEnterEvent
+ from qtpy.QtWidgets import QApplication
+
+ if TYPE_CHECKING:
+ from pytestqt.qtbot import QtBot # pyright: ignore[reportMissingImports]
+ from qtpy.QtWidgets import QWidget
+else:
+ pytest.skip(
+ "Skipping Qt tests as Qt will not be used in this environment",
+ allow_module_level=True,
+ )
+
+
+@pytest.fixture
+def evented_canvas(qtbot: QtBot) -> snx.Canvas:
+ camera = snx.Camera(transform=Transform(), interactive=True)
+ scene = snx.Scene(children=[])
+ view = snx.View(scene=scene, camera=camera)
+ canvas = snx.Canvas()
+ canvas.views.append(view)
+ native = cast(
+ "CanvasAdaptor", canvas._get_adaptors(create=True)[0]
+ )._snx_get_native()
+ qtbot.addWidget(native)
+ return canvas
+
+
+def _validate_ray(maybe_ray: Ray | None) -> Ray:
+ assert maybe_ray is not None
+ return maybe_ray
+
+
+def test_mouse_press(evented_canvas: snx.Canvas, qtbot: QtBot) -> None:
+ native = cast(
+ "CanvasAdaptor", evented_canvas._get_adaptors(create=True)[0]
+ )._snx_get_native()
+ mock = MagicMock()
+ evented_canvas.views[0].camera.set_event_filter(mock)
+ press_point = (5, 10)
+ # Press the left button
+ qtbot.mousePress(native, Qt.MouseButton.LeftButton, pos=QPoint(*press_point))
+ mock.assert_called_once_with(
+ MousePressEvent(
+ canvas_pos=press_point,
+ world_ray=_validate_ray(evented_canvas.to_world(press_point)),
+ buttons=MouseButton.LEFT,
+ ),
+ evented_canvas.views[0].camera,
+ )
+ mock.reset_mock()
+
+ # Now press the right button
+ qtbot.mousePress(native, Qt.MouseButton.RightButton, pos=QPoint(*press_point))
+ mock.assert_called_once_with(
+ MousePressEvent(
+ canvas_pos=press_point,
+ world_ray=_validate_ray(evented_canvas.to_world(press_point)),
+ buttons=MouseButton.RIGHT,
+ ),
+ evented_canvas.views[0].camera,
+ )
+
+
+def test_mouse_release(evented_canvas: snx.Canvas, qtbot: QtBot) -> None:
+ native = cast(
+ "CanvasAdaptor", evented_canvas._get_adaptors(create=True)[0]
+ )._snx_get_native()
+ mock = MagicMock()
+ evented_canvas.views[0].camera.set_event_filter(mock)
+ press_point = (5, 10)
+ qtbot.mouseRelease(native, Qt.MouseButton.LeftButton, pos=QPoint(*press_point))
+ mock.assert_called_once_with(
+ MouseReleaseEvent(
+ canvas_pos=press_point,
+ world_ray=_validate_ray(evented_canvas.to_world(press_point)),
+ buttons=MouseButton.LEFT,
+ ),
+ evented_canvas.views[0].camera,
+ )
+
+
+def test_mouse_move(evented_canvas: snx.Canvas, qtbot: QtBot) -> None:
+ native = cast(
+ "CanvasAdaptor", evented_canvas._get_adaptors(create=True)[0]
+ )._snx_get_native()
+ mock = MagicMock()
+ evented_canvas.views[0].camera.set_event_filter(mock)
+ press_point = (5, 10)
+ # FIXME: For some reason the mouse press is necessary for processing events?
+ qtbot.mousePress(native, Qt.MouseButton.LeftButton, pos=QPoint(*press_point))
+ qtbot.mousePress(native, Qt.MouseButton.RightButton, pos=QPoint(*press_point))
+ mock.reset_mock()
+ qtbot.mouseMove(native, pos=QPoint(*press_point))
+ mock.assert_called_once_with(
+ MouseMoveEvent(
+ canvas_pos=press_point,
+ world_ray=_validate_ray(evented_canvas.to_world(press_point)),
+ buttons=MouseButton.LEFT | MouseButton.RIGHT,
+ ),
+ evented_canvas.views[0].camera,
+ )
+
+
+def test_mouse_click(evented_canvas: snx.Canvas, qtbot: QtBot) -> None:
+ native = cast(
+ "CanvasAdaptor", evented_canvas._get_adaptors(create=True)[0]
+ )._snx_get_native()
+ mock = MagicMock()
+ evented_canvas.views[0].camera.set_event_filter(mock)
+ press_point = (5, 10)
+ qtbot.mouseClick(native, Qt.MouseButton.LeftButton, pos=QPoint(*press_point))
+ assert mock.call_args_list[0].args == (
+ MousePressEvent(
+ canvas_pos=press_point,
+ world_ray=_validate_ray(evented_canvas.to_world(press_point)),
+ buttons=MouseButton.LEFT,
+ ),
+ evented_canvas.views[0].camera,
+ )
+ assert mock.call_args_list[1].args == (
+ MouseReleaseEvent(
+ canvas_pos=press_point,
+ world_ray=_validate_ray(evented_canvas.to_world(press_point)),
+ buttons=MouseButton.LEFT,
+ ),
+ evented_canvas.views[0].camera,
+ )
+
+
+def test_mouse_double_click(evented_canvas: snx.Canvas, qtbot: QtBot) -> None:
+ native = cast(
+ "CanvasAdaptor", evented_canvas._get_adaptors(create=True)[0]
+ )._snx_get_native()
+ mock = MagicMock()
+ evented_canvas.views[0].camera.set_event_filter(mock)
+ press_point = (5, 10)
+ # Note that in Qt a double click does NOT implicitly imply a release as well.
+ qtbot.mouseDClick(native, Qt.MouseButton.LeftButton, pos=QPoint(*press_point))
+ assert mock.call_args_list[0].args == (
+ MouseDoublePressEvent(
+ canvas_pos=press_point,
+ world_ray=_validate_ray(evented_canvas.to_world(press_point)),
+ buttons=MouseButton.LEFT,
+ ),
+ evented_canvas.views[0].camera,
+ )
+
+
+def test_resize(evented_canvas: snx.Canvas, qtbot: QtBot) -> None:
+ native = cast(
+ "CanvasAdaptor", evented_canvas._get_adaptors(create=True)[0]
+ )._snx_get_native()
+ qtbot.add_widget(native)
+ mock = MagicMock()
+ evented_canvas.views[0].camera.set_event_filter(mock)
+ new_size = (400, 300)
+ assert evented_canvas.width != new_size[0]
+ assert evented_canvas.height != new_size[1]
+ # Note that the widget must be visible for a resize event to fire
+ cast("QWidget", native).setVisible(True)
+ cast("QWidget", native).resize(*new_size)
+ app().process_events()
+ assert evented_canvas.width == new_size[0]
+ assert evented_canvas.height == new_size[1]
+
+
+def test_mouse_enter(evented_canvas: snx.Canvas, qtbot: QtBot) -> None:
+ native = cast(
+ "CanvasAdaptor", evented_canvas._get_adaptors(create=True)[0]
+ )._snx_get_native()
+ qtbot.add_widget(native)
+
+ # Mock view filter to capture enter events
+ mock = MagicMock()
+ evented_canvas.views[0].set_event_filter(mock)
+
+ # Simulate mouse enter event by posting to event queue
+ # Note that qtbot does not have a method for this
+ enter_point = (0, 0)
+ enter_event = QEnterEvent(
+ QPointF(*enter_point), # localPos
+ QPointF(*enter_point), # windowPos
+ QPointF(*enter_point), # screenPos
+ )
+ app = QApplication.instance()
+ assert app is not None
+ app.postEvent(native, enter_event)
+ app.processEvents()
+
+ # Verify MouseEnterEvent was passed to view filter
+ mock.assert_called_once_with(
+ MouseEnterEvent(
+ canvas_pos=enter_point,
+ world_ray=_validate_ray(evented_canvas.to_world(enter_point)),
+ buttons=MouseButton.NONE,
+ )
+ )
+
+
+def test_mouse_leave(evented_canvas: snx.Canvas, qtbot: QtBot) -> None:
+ native = cast(
+ "CanvasAdaptor", evented_canvas._get_adaptors(create=True)[0]
+ )._snx_get_native()
+ qtbot.add_widget(native)
+
+ # Mock view filter to capture leave events
+ view_mock = MagicMock()
+ evented_canvas.views[0].set_event_filter(view_mock)
+
+ # NOTE: We need to first enter to establish the view as active
+ enter_point = (10, 15)
+ enter_event = QEnterEvent(
+ QPointF(*enter_point), QPointF(*enter_point), QPointF(*enter_point)
+ )
+ app = QApplication.instance()
+ assert app is not None
+ app.postEvent(native, enter_event)
+ app.processEvents()
+ view_mock.reset_mock()
+
+ # Now simulate leave event
+ leave_event = QEvent(QEvent.Type.Leave)
+ app.postEvent(native, leave_event)
+
+ # Process events to ensure the event is handled
+ app.processEvents()
+ qtbot.wait(10)
+
+ # Verify MouseLeaveEvent was passed to view filter
+ view_mock.assert_called_once_with(MouseLeaveEvent())
+
+
+# TODO: Implement when Qt new enough
+# https://doc.qt.io/qt-6/qtest.html#wheelEvent
+# def test_wheel(evented_canvas: snx.Canvas):
+# pass
diff --git a/tests/app/test_wx.py b/tests/app/test_wx.py
new file mode 100644
index 00000000..fa3ab1fc
--- /dev/null
+++ b/tests/app/test_wx.py
@@ -0,0 +1,211 @@
+"""Tests pertaining to WxPython canvas events."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, cast
+from unittest.mock import MagicMock
+
+import pytest
+
+import scenex as snx
+from scenex.app import GuiFrontend, determine_app
+from scenex.app.events import (
+ MouseButton,
+ MouseEnterEvent,
+ MouseLeaveEvent,
+ MouseMoveEvent,
+ MousePressEvent,
+ MouseReleaseEvent,
+ Ray,
+ WheelEvent,
+)
+
+if TYPE_CHECKING:
+ from collections.abc import Iterator
+ from typing import Any
+
+ from scenex.adaptors._base import CanvasAdaptor
+
+if determine_app() == GuiFrontend.WX:
+ import wx
+else:
+ pytest.skip(
+ "Skipping WxPython tests as WxPython will not be used in this environment",
+ allow_module_level=True,
+ )
+
+
+@pytest.fixture
+def evented_canvas(basic_view: snx.Scene) -> Iterator[snx.Canvas]:
+ canvas = snx.show(basic_view)
+ yield canvas
+ # FIXME: Probably good to destroy the canvas here - we may need a method for that
+
+
+def _processEvent(evt: wx.PyEventBinder, wdg: wx.Control, **kwargs: Any) -> None:
+ """Simulates a wx event.
+
+ Note that wx.UIActionSimulator is an alternative to this approach.
+ It seems to actually move the cursor around though, which is really annoying :)
+ """
+ if evt == wx.EVT_SIZE:
+ ev = wx.SizeEvent(kwargs["sz"], evt.typeId)
+ else:
+ ev = wx.MouseEvent(evt.typeId)
+ ev.SetPosition(kwargs["pos"])
+ if rot := kwargs.get("rot"):
+ ev.SetWheelRotation(rot[1])
+ ev.SetLeftDown(True)
+
+ wx.PostEvent(wdg.GetEventHandler(), ev)
+ # Borrowed from:
+ # https://github.com/wxWidgets/Phoenix/blob/master/unittests/wtc.py#L41
+ wdg.Show(True)
+ wx.MilliSleep(50)
+ evtLoop = wx.App.Get().GetTraits().CreateEventLoop()
+ wx.EventLoopActivator(evtLoop)
+ evtLoop.YieldFor(wx.EVT_CATEGORY_ALL) # pyright: ignore[reportAttributeAccessIssue]
+
+
+def _validate_ray(maybe_ray: Ray | None) -> Ray:
+ assert maybe_ray is not None
+ return maybe_ray
+
+
+def test_mouse_press(evented_canvas: snx.Canvas) -> None:
+ native = cast(
+ "CanvasAdaptor", evented_canvas._get_adaptors(create=True)[0]
+ )._snx_get_native()
+ mock = MagicMock()
+ evented_canvas.views[0].camera.set_event_filter(mock)
+ press_point = (5, 10)
+ # Press the left button
+ _processEvent(wx.EVT_LEFT_DOWN, native, pos=wx.Point(*press_point))
+ mock.assert_called_once_with(
+ MousePressEvent(
+ canvas_pos=press_point,
+ world_ray=_validate_ray(evented_canvas.to_world(press_point)),
+ buttons=MouseButton.LEFT,
+ ),
+ evented_canvas.views[0].camera,
+ )
+ mock.reset_mock()
+
+ # Now press the right button
+ _processEvent(wx.EVT_RIGHT_DOWN, native, pos=wx.Point(*press_point))
+ mock.assert_called_once_with(
+ MousePressEvent(
+ canvas_pos=press_point,
+ world_ray=_validate_ray(evented_canvas.to_world(press_point)),
+ buttons=MouseButton.RIGHT,
+ ),
+ evented_canvas.views[0].camera,
+ )
+
+
+def test_mouse_release(evented_canvas: snx.Canvas) -> None:
+ native = cast(
+ "CanvasAdaptor", evented_canvas._get_adaptors(create=True)[0]
+ )._snx_get_native()
+ mock = MagicMock()
+ evented_canvas.views[0].camera.set_event_filter(mock)
+ press_point = (5, 10)
+ _processEvent(wx.EVT_LEFT_UP, native, pos=wx.Point(*press_point))
+ mock.assert_called_once_with(
+ MouseReleaseEvent(
+ canvas_pos=press_point,
+ world_ray=_validate_ray(evented_canvas.to_world(press_point)),
+ buttons=MouseButton.LEFT,
+ ),
+ evented_canvas.views[0].camera,
+ )
+
+
+def test_mouse_move(evented_canvas: snx.Canvas) -> None:
+ native = cast(
+ "CanvasAdaptor", evented_canvas._get_adaptors(create=True)[0]
+ )._snx_get_native()
+ mock = MagicMock()
+ evented_canvas.views[0].camera.set_event_filter(mock)
+ press_point = (5, 10)
+ # FIXME: For some reason the mouse press is necessary for processing events?
+ _processEvent(wx.EVT_LEFT_DOWN, native, pos=wx.Point(*press_point))
+ _processEvent(wx.EVT_RIGHT_DOWN, native, pos=wx.Point(*press_point))
+ mock.reset_mock()
+ _processEvent(wx.EVT_MOTION, native, pos=wx.Point(*press_point))
+ mock.assert_called_once_with(
+ MouseMoveEvent(
+ canvas_pos=press_point,
+ world_ray=_validate_ray(evented_canvas.to_world(press_point)),
+ buttons=MouseButton.LEFT | MouseButton.RIGHT,
+ ),
+ evented_canvas.views[0].camera,
+ )
+
+
+def test_mouse_wheel(evented_canvas: snx.Canvas) -> None:
+ native = cast(
+ "CanvasAdaptor", evented_canvas._get_adaptors(create=True)[0]
+ )._snx_get_native()
+ mock = MagicMock()
+ evented_canvas.views[0].camera.set_event_filter(mock)
+ press_point = (5, 10)
+ _processEvent(wx.EVT_MOUSEWHEEL, native, pos=wx.Point(*press_point), rot=(0, 120))
+ mock.assert_called_once_with(
+ WheelEvent(
+ canvas_pos=press_point,
+ world_ray=_validate_ray(evented_canvas.to_world(press_point)),
+ buttons=MouseButton.NONE,
+ angle_delta=(0, 120),
+ ),
+ evented_canvas.views[0].camera,
+ )
+
+
+def test_resize(evented_canvas: snx.Canvas) -> None:
+ native = cast(
+ "CanvasAdaptor", evented_canvas._get_adaptors(create=True)[0]
+ )._snx_get_native()
+ mock = MagicMock()
+ evented_canvas.views[0].camera.set_event_filter(mock)
+ new_size = (400, 300)
+ # Note that the widget must be visible for a resize event to fire
+ _processEvent(wx.EVT_SIZE, native, sz=wx.Size(*new_size))
+ assert evented_canvas.width == new_size[0]
+ assert evented_canvas.height == new_size[1]
+
+
+def test_mouse_enter(evented_canvas: snx.Canvas) -> None:
+ native = cast(
+ "CanvasAdaptor", evented_canvas._get_adaptors(create=True)[0]
+ )._snx_get_native()
+ view_mock = MagicMock()
+ evented_canvas.views[0].set_event_filter(view_mock)
+ enter_point = (0, 15)
+ _processEvent(wx.EVT_ENTER_WINDOW, native, pos=wx.Point(*enter_point))
+
+ # Verify MouseEnterEvent was passed to view filter
+ view_mock.assert_called_once_with(
+ MouseEnterEvent(
+ canvas_pos=enter_point,
+ world_ray=_validate_ray(evented_canvas.to_world(enter_point)),
+ buttons=MouseButton.NONE,
+ )
+ )
+
+
+def test_mouse_leave(evented_canvas: snx.Canvas) -> None:
+ native = cast(
+ "CanvasAdaptor", evented_canvas._get_adaptors(create=True)[0]
+ )._snx_get_native()
+ view_mock = MagicMock()
+ evented_canvas.views[0].set_event_filter(view_mock)
+ # NOTE: We need to first enter to establish the view as active
+ enter_point = (0, 15)
+ _processEvent(wx.EVT_ENTER_WINDOW, native, pos=wx.Point(*enter_point))
+ view_mock.reset_mock()
+
+ # Now leave
+ _processEvent(wx.EVT_LEAVE_WINDOW, native, pos=wx.Point(0, 0))
+ # Verify MouseLeaveEvent was passed to view filter
+ view_mock.assert_called_once_with(MouseLeaveEvent())
diff --git a/tests/model/_nodes/test_camera.py b/tests/model/_nodes/test_camera.py
new file mode 100644
index 00000000..371167e4
--- /dev/null
+++ b/tests/model/_nodes/test_camera.py
@@ -0,0 +1,41 @@
+import numpy as np
+
+import scenex as snx
+
+
+def test_camera_forward_property() -> None:
+ cam = snx.Camera(transform=snx.Transform())
+ # Default forward should be (0, 0, -1)
+ np.testing.assert_allclose(cam.forward, (0, 0, -1), atol=1e-6)
+ # Set forward to (0, 0, 1)
+ cam.forward = (0, 0, 1)
+ # Test that the camera transform maps (0, 0, -1) to our new forward
+ new_fwd = cam.transform.map((0, 0, -1))[:3]
+ new_fwd /= np.linalg.norm(new_fwd)
+ np.testing.assert_allclose(new_fwd, (0, 0, 1), atol=1e-6)
+
+
+def test_camera_up_property() -> None:
+ cam = snx.Camera(transform=snx.Transform())
+ # Default up should be (0, 1, 0)
+ np.testing.assert_allclose(cam.up, (0, 1, 0), atol=1e-6)
+ # Set up to (1, 0, 0)
+ cam.up = (1, 0, 0)
+ # Test that the camera transform maps (0, 1, 0) to our new up
+ new_fwd = cam.transform.map((0, 1, 0))[:3]
+ new_fwd /= np.linalg.norm(new_fwd)
+ np.testing.assert_allclose(new_fwd, (1, 0, 0), atol=1e-6)
+ # Test that the camera forward is still the default
+ new_fwd = cam.transform.map((0, 0, -1))[:3]
+ new_fwd /= np.linalg.norm(new_fwd)
+ np.testing.assert_allclose(new_fwd, (0, 0, -1), atol=1e-6)
+
+
+def test_camera_look_at() -> None:
+ cam = snx.Camera(transform=snx.Transform())
+ # Look at (0, 0, 1) with up (0, 0, 1)
+ cam.look_at((1, 0, 0), up=(0, 0, 1))
+ # Forward should be (1, 0, 0) - (0, 0, 0) = (1, 0, 0)
+ np.testing.assert_allclose(cam.forward, (1, 0, 0), atol=1e-6)
+ # Up should be (0, 0, 1) - (0, 0, 0) = (0, 0, 1)
+ np.testing.assert_allclose(cam.up, (0, 0, 1), atol=1e-6)
diff --git a/tests/model/_nodes/test_canvas.py b/tests/model/_nodes/test_canvas.py
new file mode 100644
index 00000000..2c051cb4
--- /dev/null
+++ b/tests/model/_nodes/test_canvas.py
@@ -0,0 +1,89 @@
+import numpy as np
+
+import scenex as snx
+from scenex.app.events import Ray
+from scenex.utils import projections
+
+
+def test_to_world() -> None:
+ """Tests Canvas.to_world"""
+ # Identity projection, identity transformation
+ camera = snx.Camera(
+ transform=snx.Transform(),
+ projection=projections.orthographic(2, 2, 2),
+ interactive=True,
+ )
+ view = snx.View(scene=snx.Scene(children=[]), camera=camera)
+ canvas = snx.Canvas(width=int(view.layout.width), height=int(view.layout.height))
+ canvas.views.append(view)
+
+ # Test center of canvas
+ canvas_pos = (view.layout.width // 2, view.layout.height // 2)
+ ray = canvas.to_world(canvas_pos)
+ assert ray == Ray(origin=(0, 0, 0), direction=(0, 0, -1))
+
+ # Test top-left corner
+ canvas_pos = (0, 0)
+ ray = canvas.to_world(canvas_pos)
+ assert ray == Ray(origin=(-1, 1, 0), direction=(0, 0, -1))
+
+ # Test outside the view
+ canvas_pos = (view.layout.width * 2, view.layout.height * 2)
+ ray = canvas.to_world(canvas_pos)
+ assert ray is None
+
+
+def test_to_world_translated() -> None:
+ """Tests Canvas.to_world with a translated camera"""
+ # Identity projection, small transformation
+ camera = snx.Camera(
+ transform=snx.Transform().translated((1, 1, 1)),
+ projection=projections.orthographic(2, 2, 2),
+ interactive=True,
+ )
+ view = snx.View(scene=snx.Scene(children=[]), camera=camera)
+ canvas = snx.Canvas(width=int(view.layout.width), height=int(view.layout.height))
+ canvas.views.append(view)
+
+ ray = canvas.to_world((0, 0))
+ assert ray == Ray(origin=(0, 2, 1), direction=(0, 0, -1))
+ # Rotate counter-clockwise around +Z - we see a clockwise rotation
+ # i.e. (-1, 1, 0) moves to the top right corner and (-1, -1, 0) moves to the
+ # top left corner
+ camera.transform = snx.Transform().rotated(90, (0, 0, 1))
+ ray = canvas.to_world((0, 0))
+ # Rounding errors :(
+ assert ray is not None
+ assert np.allclose(ray.origin, (-1, -1, 0), atol=1e-7)
+ assert np.array_equal(ray.direction, (0, 0, -1))
+ camera.transform = snx.Transform()
+
+
+def test_to_world_projection() -> None:
+ """Tests Canvas.to_world with a non-identity camera projection"""
+ # Narrowed projection, identity transformation
+ camera = snx.Camera(
+ transform=snx.Transform(),
+ projection=projections.orthographic(1, 1, 1),
+ interactive=True,
+ )
+ view = snx.View(scene=snx.Scene(children=[]), camera=camera)
+ canvas = snx.Canvas(width=int(view.layout.width), height=int(view.layout.height))
+ canvas.views.append(view)
+
+ ray = canvas.to_world((0, 0))
+ assert ray == Ray(origin=(-0.5, 0.5, 0), direction=(0, 0, -1))
+ camera.projection = snx.Transform()
+
+
+def test_canvas_layout() -> None:
+ """Tests adding an incompatible view to a canvas results in a logical scenario"""
+ # TODO: Is this actually the logical scenario?
+ canvas = snx.Canvas()
+ view = snx.View(scene=snx.Scene(children=[]), camera=snx.Camera())
+ view.layout.width = canvas.width + 100
+ view.layout.height = canvas.height + 100
+ canvas.views.append(view)
+
+ assert view.layout.width == canvas.width
+ assert view.layout.height == canvas.height
diff --git a/tests/model/_nodes/test_image.py b/tests/model/_nodes/test_image.py
new file mode 100644
index 00000000..e3d0694e
--- /dev/null
+++ b/tests/model/_nodes/test_image.py
@@ -0,0 +1,45 @@
+import cmap
+import numpy as np
+import pytest
+
+import scenex as snx
+from scenex.app.events import Ray
+from scenex.model._nodes.image import Image
+
+
+@pytest.fixture
+def image() -> snx.Image:
+ return snx.Image(
+ data=np.random.randint(0, 255, (100, 100), dtype=np.uint8),
+ cmap=cmap.Colormap("gray"),
+ clims=(0, 255),
+ gamma=1.0,
+ interpolation="nearest",
+ )
+
+
+def test_bounding_box(image: snx.Image) -> None:
+ exp_bounding_box = np.asarray(((-0.5, -0.5, 0), (99.5, 99.5, 0)))
+ assert np.array_equal(exp_bounding_box, image.bounding_box)
+
+
+def test_passes_through(image: Image) -> None:
+ # Check a ray that passes through the image hits
+ ray = Ray(origin=(50, 50, 1), direction=(0, 0, -1))
+ assert image.passes_through(ray) == 1
+
+ # Check a ray that grazes the left edge of the image hits
+ ray = Ray(origin=(-0.5, 0, 1), direction=(0, 0, -1))
+ assert image.passes_through(ray) == 1
+
+ # Check a ray that grazes the right edge of the image misses
+ ray = Ray(origin=(99.5, 0, 1), direction=(0, 0, -1))
+ assert image.passes_through(ray) is None
+
+ # Check a ray that does not pass through the image misses
+ ray = Ray(origin=(-50, -50, 1), direction=(0, 0, -1))
+ assert image.passes_through(ray) is None
+
+ # Check a ray that is perpendicular to the image misses
+ ray = Ray(origin=(50, 50, 1), direction=(-1, 0, 0))
+ assert image.passes_through(ray) is None
diff --git a/tests/model/_nodes/test_points.py b/tests/model/_nodes/test_points.py
new file mode 100644
index 00000000..c319ffc8
--- /dev/null
+++ b/tests/model/_nodes/test_points.py
@@ -0,0 +1,20 @@
+import numpy as np
+import pytest
+
+import scenex as snx
+
+
+@pytest.fixture
+def points() -> snx.Points:
+ return snx.Points(
+ coords=np.random.randint(0, 255, (100, 3), dtype=np.uint8),
+ )
+
+
+def test_bounding_box(points: snx.Points) -> None:
+ # This test is a bit tautological, but it does prevent anything crazy from happening
+ # :)
+ exp_bounding_box = np.asarray(
+ (np.min(points.coords, axis=0), np.max(points.coords, axis=0))
+ )
+ assert np.array_equal(exp_bounding_box, points.bounding_box)
diff --git a/tests/model/_nodes/test_scene.py b/tests/model/_nodes/test_scene.py
new file mode 100644
index 00000000..b0a2ad42
--- /dev/null
+++ b/tests/model/_nodes/test_scene.py
@@ -0,0 +1,57 @@
+import numpy as np
+
+import scenex as snx
+from scenex.model._transform import Transform
+
+
+def test_bounding_box() -> None:
+ # An empty scene should have an "empty" bounding box
+ empty_scene = snx.Scene()
+ assert empty_scene.bounding_box is None
+
+ # With children, the bounding box should be defined by the scene
+ points_scene = snx.Scene(
+ children=[snx.Points(coords=np.asarray([[0, 100, 0], [100, 0, 1]]))]
+ )
+ exp_bounding_box = np.asarray(((0, 0, 0), (100, 100, 1)))
+ act_bouning_box = points_scene.bounding_box
+ assert act_bouning_box is not None
+ assert np.array_equal(exp_bounding_box, act_bouning_box)
+
+ # The bounding box should move as their points move.
+ points_scene = snx.Scene(
+ children=[
+ snx.Points(
+ coords=np.asarray([[0, 100, 0], [100, 0, 1]]),
+ transform=Transform().translated((1, 1, 1)),
+ )
+ ]
+ )
+ exp_bounding_box = np.asarray(((1, 1, 1), (101, 101, 2)))
+ act_bouning_box = points_scene.bounding_box
+ assert act_bouning_box is not None
+ assert np.array_equal(exp_bounding_box, act_bouning_box)
+
+ # The bounding box should encapsulate all children
+ points_scene = snx.Scene(
+ children=[
+ snx.Points(
+ coords=np.asarray(
+ [
+ [0, 100, 0],
+ ]
+ ),
+ ),
+ snx.Points(
+ coords=np.asarray(
+ [
+ [100, 0, 1],
+ ]
+ ),
+ ),
+ ]
+ )
+ exp_bounding_box = np.asarray(((0, 0, 0), (100, 100, 1)))
+ act_bouning_box = points_scene.bounding_box
+ assert act_bouning_box is not None
+ assert np.array_equal(exp_bounding_box, act_bouning_box)
diff --git a/tests/model/_nodes/test_text.py b/tests/model/_nodes/test_text.py
new file mode 100644
index 00000000..297752dd
--- /dev/null
+++ b/tests/model/_nodes/test_text.py
@@ -0,0 +1,15 @@
+import cmap
+import numpy as np
+import pytest
+
+import scenex as snx
+
+
+@pytest.fixture
+def text() -> snx.Text:
+ return snx.Text(text="Hello, World!", color=cmap.Color("red"), size=12)
+
+
+def test_bounding_box(text: snx.Text) -> None:
+ exp_bounding_box = np.asarray(((-1e-6, -1e-6, -1e-6), (1e-6, 1e-6, 1e-6)))
+ assert np.array_equal(exp_bounding_box, text.bounding_box)
diff --git a/tests/model/_nodes/test_view.py b/tests/model/_nodes/test_view.py
new file mode 100644
index 00000000..f2d8ed00
--- /dev/null
+++ b/tests/model/_nodes/test_view.py
@@ -0,0 +1,94 @@
+from __future__ import annotations
+
+from unittest.mock import MagicMock
+
+import numpy as np
+
+import scenex as snx
+from scenex.app.events import Event, MouseButton, MouseMoveEvent
+from scenex.utils import projections
+
+
+def test_events() -> None:
+ # Create a view with an image
+ img = snx.Image(data=np.ones((10, 10), dtype=np.uint8), interactive=True)
+ img_filter = MagicMock()
+ img.set_event_filter(img_filter)
+
+ view = snx.View(scene=snx.Scene(children=[img]))
+ view_filter = MagicMock()
+ view_filter.return_value = False
+ view.set_event_filter(view_filter)
+
+ # Set up the camera
+ # Such that the image is in the top right quadrant
+ view.camera.transform = snx.Transform().translated((-0.5, -0.5))
+ view.camera.projection = projections.orthographic(1, 1, 1)
+
+ # Put it on a canvas
+ canvas = snx.Canvas(width=int(view.layout.width), height=int(view.layout.height))
+ canvas.views.append(view)
+
+ # Mouse over that image in the top right corner
+ canvas_pos = (view.layout.width, 0)
+ world_ray = canvas.to_world(canvas_pos)
+ assert world_ray is not None
+ event = MouseMoveEvent(
+ canvas_pos=canvas_pos, world_ray=world_ray, buttons=MouseButton.NONE
+ )
+
+ # And show both the view and the image saw the event
+ canvas.handle(event)
+ view_filter.assert_called_once_with(event)
+ img_filter.assert_called_once_with(event, img)
+
+ # Reset the mocks
+ img_filter.reset_mock()
+ view_filter.reset_mock()
+
+ # Mouse over empty space in the top left corner
+ canvas_pos = (0, 0)
+ world_ray = canvas.to_world(canvas_pos)
+ assert world_ray is not None
+ event = MouseMoveEvent(
+ canvas_pos=canvas_pos, world_ray=world_ray, buttons=MouseButton.NONE
+ )
+
+ # And show that the image did not see the event
+ # but that the view still saw the event
+ canvas.handle(event)
+ img_filter.assert_not_called()
+ view_filter.assert_called_once_with(event)
+
+
+def test_filter_returning_None() -> None:
+ """Some widget backends (e.g. Qt) get upset when non-booleans are returned.
+
+ This test ensures that if a faulty event filter is set that returns None,
+ the event is treated as handled (i.e. True is returned).
+ """
+ # Create a view...
+ view = snx.View()
+
+ # ...with a faulty event filter...
+ def faulty_filter(event: Event) -> bool:
+ return None # type: ignore[return-value]
+
+ view.set_event_filter(faulty_filter)
+
+ # ...put it on a canvas...
+ canvas = snx.Canvas(width=int(view.layout.width), height=int(view.layout.height))
+ canvas.views.append(view)
+
+ # ...and create a mock event...
+ canvas_pos = (0, 0)
+ world_ray = canvas.to_world(canvas_pos)
+ assert world_ray is not None
+ event = MouseMoveEvent(
+ canvas_pos=canvas_pos, world_ray=world_ray, buttons=MouseButton.NONE
+ )
+
+ # ...to test handling...
+ handled = view.filter_event(event)
+ assert isinstance(handled, bool)
+ assert handled is False
diff --git a/tests/model/_nodes/test_volume.py b/tests/model/_nodes/test_volume.py
new file mode 100644
index 00000000..5d08ac44
--- /dev/null
+++ b/tests/model/_nodes/test_volume.py
@@ -0,0 +1,38 @@
+import numpy as np
+import pytest
+
+import scenex as snx
+from scenex.app.events import Ray
+
+
+@pytest.fixture
+def volume() -> snx.Volume:
+ return snx.Volume(
+ data=np.random.randint(0, 255, (60, 100, 100), dtype=np.uint8),
+ )
+
+
+def test_bounding_box(volume: snx.Volume) -> None:
+ # Note that the volume has 60 z-slices. But depth comes last in the bounding box!
+ exp_bounding_box = np.asarray(((-0.5, -0.5, -0.5), (99.5, 99.5, 59.5)))
+ assert np.array_equal(exp_bounding_box, volume.bounding_box)
+
+
+def test_passes_through(volume: snx.Volume) -> None:
+ # Check a ray that passes through the volume hits
+ ray = Ray(origin=(50, 50, -1), direction=(0, 0, 1))
+ # Note that it intersects at -0.5 because pixel centers are at integer coordinates
+ assert volume.passes_through(ray) == 0.5
+
+ # Check a ray that grazes the left edge of the volume hits
+ ray = Ray(origin=(-0.5, 0, -1), direction=(0, 0, 1))
+ assert volume.passes_through(ray) == 0.5
+
+ # Check a ray that grazes the right edge of the volume misses (the front face)
+ ray = Ray(origin=(99.5, 0, -1), direction=(0, 0, 1))
+ # Because of symmetry, we miss the front face of the volume but hit the back face
+ assert volume.passes_through(ray) == 60.5
+
+ # Check a ray that does not pass through the volume misses
+ ray = Ray(origin=(-50, -50, -1), direction=(0, 0, 1))
+ assert volume.passes_through(ray) is None
diff --git a/tests/test_basic_scene.py b/tests/test_basic_scene.py
index b13e18ab..b43c2c85 100644
--- a/tests/test_basic_scene.py
+++ b/tests/test_basic_scene.py
@@ -1,13 +1,15 @@
from __future__ import annotations
-import importlib
-import importlib.util
-from typing import Any
+from typing import TYPE_CHECKING, Any, cast
import numpy as np
-import pytest
import scenex as snx
+from scenex.adaptors._auto import determine_backend
+
+if TYPE_CHECKING:
+ from scenex.adaptors._pygfx._scene import Scene as PygfxScene
+ from scenex.adaptors._vispy._scene import Scene as VispyScene
EXPECT_REPR = """
Scene
@@ -18,12 +20,6 @@
└── Camera
""".strip()
-BACKENDS = []
-if importlib.util.find_spec("vispy") is not None:
- BACKENDS.append("vispy")
-if importlib.util.find_spec("pygfx") is not None:
- BACKENDS.append("pygfx")
-
def _obj_name(obj: Any) -> str:
"""Return the name of a backend node object.
@@ -44,14 +40,12 @@ def _child_names(obj: Any) -> list[str]:
return [_obj_name(child) for child in obj.children]
-@pytest.mark.parametrize("backend", BACKENDS)
-def test_basic_view(basic_view: snx.View, backend: str) -> None:
- snx.show(basic_view, backend=backend)
+def test_basic_view(basic_view: snx.View) -> None:
+ snx.show(basic_view)
# make sure we've got the right backend
- adaptors = basic_view._get_adaptors(backend=backend, create=False)
+ adaptors = basic_view._get_adaptors(create=False)
assert adaptors
- assert backend in type(adaptors[0]).__module__
assert isinstance(repr(basic_view), str)
assert isinstance(basic_view.model_dump(), dict)
@@ -61,21 +55,27 @@ def test_basic_view(basic_view: snx.View, backend: str) -> None:
assert isinstance(ary, np.ndarray)
-@pytest.mark.parametrize("backend", BACKENDS)
-def test_view_tree_matches_native(basic_view: snx.View, backend: str) -> None:
+def _native_scene(scene: snx.Scene) -> Any:
+ """Gets the visualization toolkit scene object corresponding to the view."""
+ if determine_backend() == "pygfx":
+ return cast("PygfxScene", scene._get_adaptors(create=True)[0])._pygfx_node
+ if determine_backend() == "vispy":
+ return cast("VispyScene", scene._get_adaptors(create=True)[0])._vispy_node
+
+
+def test_view_tree_matches_native(basic_view: snx.View) -> None:
"""Test that the structure of the tree generated by the model matches the
structure of the tree generated by the native backend."""
- basic_view._get_adaptors(backend=backend, create=True)
+ basic_view._get_adaptors(create=True)
model_tree = snx.util.tree_dict(basic_view.scene, obj_name=_obj_name)
- native_scene = basic_view.scene._get_native(backend=backend)
+ native_scene = _native_scene(basic_view.scene)
view_tree = snx.util.tree_dict(native_scene, obj_name=_obj_name)
assert isinstance(view_tree, dict)
assert model_tree == view_tree
-@pytest.mark.parametrize("backend", BACKENDS)
-def test_changing_parent_updates_adaptor(backend: str) -> None:
+def test_changing_parent_updates_adaptor() -> None:
"""Test that changing the parent of a model object works, and emits events."""
# create a scene and a view
scene1 = snx.Scene()
@@ -83,8 +83,8 @@ def test_changing_parent_updates_adaptor(backend: str) -> None:
img1 = snx.Image(data=np.random.randint(0, 255, (10, 10), dtype=np.uint8))
img2 = snx.Image(data=np.random.randint(0, 255, (10, 10), dtype=np.uint8))
- scene1_native = scene1._get_native(backend=backend, create=True)
- scene2_native = scene2._get_native(backend=backend, create=True)
+ scene1_native = _native_scene(scene1)
+ scene2_native = _native_scene(scene2)
# nothing is in any scene yet
assert "Image" not in {_obj_name(x) for x in scene2_native.children}
diff --git a/tests/test_util.py b/tests/test_util.py
new file mode 100644
index 00000000..a2746802
--- /dev/null
+++ b/tests/test_util.py
@@ -0,0 +1,8 @@
+import scenex as snx
+
+
+def test_show_canvas_size(basic_view: snx.View) -> None:
+ """Tests that show_canvas respects the size of the canvas."""
+ canvas = snx.show(basic_view)
+ assert canvas.width == basic_view.layout.width
+ assert canvas.height == basic_view.layout.height
diff --git a/tests/utils/test_controllers.py b/tests/utils/test_controllers.py
new file mode 100644
index 00000000..07fef337
--- /dev/null
+++ b/tests/utils/test_controllers.py
@@ -0,0 +1,199 @@
+from __future__ import annotations
+
+import math
+
+import numpy as np
+import pylinalg as la
+
+import scenex as snx
+from scenex.app.events import (
+ MouseButton,
+ MouseMoveEvent,
+ MousePressEvent,
+ Ray,
+ WheelEvent,
+)
+from scenex.model._transform import Transform
+from scenex.utils.controllers import OrbitController, PanZoomController
+
+
+def _validate_ray(maybe_ray: Ray | None) -> Ray:
+ assert maybe_ray is not None
+ return maybe_ray
+
+
+def test_panzoomcontroller_pan() -> None:
+ """Tests panning behavior of the PanZoomController."""
+ controller = PanZoomController()
+ cam = snx.Camera(interactive=True, controller=controller)
+ # Simulate mouse press
+ press_event = MousePressEvent(
+ canvas_pos=(0, 0),
+ world_ray=Ray((10, 10, 0), (0, 0, -1)),
+ buttons=MouseButton.LEFT,
+ )
+ controller(press_event, cam)
+ # Simulate mouse move
+ move_event = MouseMoveEvent(
+ canvas_pos=(0, 0),
+ world_ray=Ray((15, 20, 0), (0, 0, -1)),
+ buttons=MouseButton.LEFT,
+ )
+ controller(move_event, cam)
+ # The camera should have moved by (-5, -10)
+ expected = Transform().translated((-5, -10))
+ np.testing.assert_allclose(cam.transform.root, expected.root)
+
+
+def test_panzoomcontroller_zoom() -> None:
+ """Tests zooming behavior of the PanZoomController."""
+ controller = PanZoomController()
+ cam = snx.Camera(interactive=True)
+ cam.set_event_filter(controller)
+ # Simulate wheel event
+ wheel_event = WheelEvent(
+ canvas_pos=(0, 0),
+ world_ray=Ray((0, 0, 0), (0, 0, -1)),
+ buttons=MouseButton.NONE,
+ angle_delta=(0, 120),
+ )
+ before = cam.projection
+ controller(wheel_event, cam)
+ # The projection should be scaled
+ zoom = controller._zoom_factor(wheel_event.angle_delta[1])
+ expected = before.scaled((zoom, zoom, 1))
+ np.testing.assert_allclose(cam.projection.root, expected.root)
+
+
+def test_orbitcontroller_orbit() -> None:
+ """Tests orbiting behavior of the OrbitController."""
+ # Camera is along the x axis, looking in the negative x direction at the center
+ controller = OrbitController(center=(0, 0, 0))
+ cam = snx.Camera(interactive=True, controller=controller)
+ # Add cam to the canvas
+ canvas = snx.Canvas()
+ view = snx.View(camera=cam)
+ canvas.views.append(view)
+ # Position the camera along the x-axis, looking in the negative x direction at the
+ # center
+ cam.transform = Transform().translated((10, 0, 0))
+ cam.look_at((0, 0, 0), up=(0, 0, 1))
+ ray = canvas.to_world((view.layout.width / 2, view.layout.height / 2))
+ assert ray is not None
+ np.testing.assert_allclose(ray.origin, (10, 0, 0), atol=1e-7)
+ np.testing.assert_allclose(ray.direction, (-1, 0, 0), atol=1e-7)
+
+ pos_before = cam.transform.map((0, 0, 0))[:3]
+ # Simulate mouse press
+ click_pos = (view.layout.width / 2, view.layout.height / 2)
+ press_event = MousePressEvent(
+ canvas_pos=click_pos,
+ world_ray=_validate_ray(canvas.to_world(click_pos)),
+ buttons=MouseButton.LEFT,
+ )
+ controller(press_event, cam)
+ # Simulate mouse move (orbit) of one horizontal pixel and one vertical pixel
+ move_pos = (click_pos[0] + 1, click_pos[1] + 1)
+ move_event = MouseMoveEvent(
+ canvas_pos=move_pos,
+ world_ray=_validate_ray(canvas.to_world(move_pos)),
+ buttons=MouseButton.LEFT,
+ )
+ controller(move_event, cam)
+ # Assert camera position conforms to expectation
+ # (one degree around y axis and one degree around z axis)
+ pos_after_exp = la.vec_transform_quat(
+ pos_before,
+ la.quat_mul(
+ # Increase azimuth 1 degree
+ la.quat_from_axis_angle((0, 0, -1), math.pi / 180),
+ # Increase elevation 1 degree
+ la.quat_from_axis_angle((0, -1, 0), math.pi / 180),
+ ),
+ )
+ pos_after_act = cam.transform.map((0, 0, 0))[:3]
+ np.testing.assert_allclose(pos_after_act, pos_after_exp)
+
+
+def test_orbitcontroller_zoom() -> None:
+ center = (0.0, 0.0, 0.0)
+ cam = snx.Camera(interactive=True, transform=Transform().translated((0, 0, 10)))
+ controller = OrbitController(center)
+ cam.set_event_filter(controller)
+ tform_before = cam.transform
+ # Simulate wheel event
+ wheel_event = WheelEvent(
+ canvas_pos=(0, 0),
+ world_ray=Ray((0, 0, 10), (0, 0, -1)),
+ buttons=MouseButton.NONE,
+ angle_delta=(0, 120),
+ )
+ controller(wheel_event, cam)
+ # The camera should have moved closer to center
+ zoom = controller._zoom_factor(120)
+ desired_tform = Transform().translated((0, 0, 10 * zoom))
+ np.testing.assert_allclose(cam.transform, desired_tform)
+
+ # Simulate wheel event in other direction
+ wheel_event = WheelEvent(
+ canvas_pos=(0, 0),
+ world_ray=Ray((0, 0, 10), (0, 0, -1)),
+ buttons=MouseButton.NONE,
+ angle_delta=(0, -120),
+ )
+ controller(wheel_event, cam)
+ # The camera should have moved back to the starting point
+ zoom = controller._zoom_factor(-120)
+ desired_tform = Transform().translated((0, 0, 10))
+ np.testing.assert_allclose(cam.transform, tform_before)
+
+
+def test_orbitcontroller_pan() -> None:
+ # Camera is along the x axis, looking in the negative x direction at the center
+ controller = OrbitController(center=(0, 0, 0))
+ cam = snx.Camera(interactive=True, controller=controller)
+ # Add cam to the canvas
+ canvas = snx.Canvas()
+ view = snx.View(camera=cam)
+ canvas.views.append(view)
+ # Position the camera along the x-axis, looking in the negative x direction at the
+ # center
+ cam.transform = Transform().rotated(90, (0, 1, 0)).translated((10, 0, 0))
+ ray = canvas.to_world((view.layout.width / 2, view.layout.height / 2))
+ assert ray is not None
+ np.testing.assert_allclose(ray.origin, (10, 0, 0), atol=1e-7)
+ np.testing.assert_allclose(ray.direction, (-1, 0, 0), atol=1e-7)
+ tform_before = cam.transform
+ center_before = controller.center.copy()
+
+ # Simulate right mouse press
+ click_pos = (view.layout.width / 2, view.layout.height / 2)
+ world_ray_before = canvas.to_world(click_pos)
+ assert world_ray_before is not None
+ press_event = MousePressEvent(
+ canvas_pos=click_pos,
+ world_ray=world_ray_before,
+ buttons=MouseButton.RIGHT,
+ )
+ controller(press_event, cam)
+ # Simulate right mouse move (pan)
+ click_pos = (click_pos[0], click_pos[1] + view.layout.height // 2)
+ world_ray_after = canvas.to_world(click_pos)
+ assert world_ray_after is not None
+ move_event = MouseMoveEvent(
+ canvas_pos=click_pos,
+ world_ray=world_ray_after,
+ buttons=MouseButton.RIGHT,
+ )
+ controller(move_event, cam)
+ # This should move the camera (world_ray_before - world_ray_after), so that the
+ # center stays at the same point on the camera plane.
+ distance = [
+ b - a
+ for b, a in zip(world_ray_before.origin, world_ray_after.origin, strict=True)
+ ]
+ desired_tform = tform_before.translated(distance)
+ np.testing.assert_allclose(cam.transform, desired_tform)
+ # It should move the orbit center in a similar way
+ desired_center = np.asarray(center_before) + distance
+ np.testing.assert_allclose(controller.center, desired_center)
diff --git a/tests/utils/test_projections.py b/tests/utils/test_projections.py
new file mode 100644
index 00000000..7ae6c6fb
--- /dev/null
+++ b/tests/utils/test_projections.py
@@ -0,0 +1,174 @@
+import numpy as np
+from pylinalg import vec_unproject
+
+import scenex as snx
+from scenex.model._transform import Transform
+from scenex.utils.projections import orthographic, perspective, zoom_to_fit
+
+CORNERS = np.asarray(
+ [
+ [-1, -1],
+ [-1, 1],
+ [1, -1],
+ [1, 1],
+ ]
+)
+
+
+def test_orthographic() -> None:
+ """Basic tests for the orthographic projection matrix constructor"""
+ # By default, the return should unproject NDCs to a depth-inverted unit cube
+ exp_mat = np.asarray(
+ [
+ [2, 0, 0, 0],
+ [0, 2, 0, 0],
+ [0, 0, -2, 0],
+ [0, 0, 0, 1],
+ ]
+ )
+ act_mat = orthographic()
+ assert np.array_equal(exp_mat, act_mat)
+ exp_corners = np.asarray(
+ [
+ [-0.5, -0.5, 0],
+ [-0.5, 0.5, 0],
+ [0.5, -0.5, 0],
+ [0.5, 0.5, 0],
+ ]
+ )
+ assert np.array_equal(exp_corners, vec_unproject(CORNERS, act_mat))
+
+ # Scales inversely w.r.t. width, height, depth
+ exp_mat = np.asarray(
+ [
+ [2 / 3, 0, 0, 0],
+ [0, 2, 0, 0],
+ [0, 0, -2 / 5, 0],
+ [0, 0, 0, 1],
+ ]
+ )
+ act_mat = orthographic(3, 4, 5) # Cube with width 3, height 4, depth 5 in view
+ exp_corners = np.asarray(
+ [
+ [-1.5, -2, 0],
+ [-1.5, 2, 0],
+ [1.5, -2, 0],
+ [1.5, 2, 0],
+ ]
+ )
+ assert np.array_equal(exp_corners, vec_unproject(CORNERS, act_mat))
+
+
+def test_perspective() -> None:
+ """Basic testing of the perspective matrix"""
+ fov = 90
+ depth_to_near = 300
+ depth_to_far = 1e6 # Just need something really big
+
+ mat = perspective(fov, depth_to_near, depth_to_far)
+
+ exp_mat = np.asarray(
+ [
+ [1, 0, 0, 0],
+ [0, 1, 0, 0],
+ [0, 0, -1, -300],
+ [0, 0, -1, 0],
+ ]
+ )
+
+ # Note the z-offset is like 300.09. Might be rounding errors?
+ assert np.allclose(exp_mat, mat, rtol=1e-1)
+
+ # Test a near frustum corner maps to a canvas corner
+ # Note that by convention positive z points away from the scene.
+ assert np.allclose(np.asarray((1, 1)), _project(mat, (300, 300, -300)))
+ # Test a far frustum corner maps to a canvas corner
+ assert np.allclose(np.asarray((1, 1)), _project(mat, (360, 360, -360)))
+ # Test a near frustum corner, TRANSLATED back in the scene, does not map to a corner
+ # This point models the back face of a volume
+ assert np.allclose(np.asarray((5 / 6, 5 / 6)), _project(mat, (300, 300, -360)))
+
+
+def test_zoom_to_fit_orthographic() -> None:
+ view = snx.View(
+ scene=snx.Scene(
+ children=[snx.Points(coords=np.asarray([[0, 100, 0], [100, 0, 1]]))]
+ )
+ )
+
+ zoom_to_fit(view, type="orthographic")
+ # Assert the camera is moved to the center of the scene
+ assert view.camera.transform == snx.Transform().translated((50, 50, 0.5))
+ # Projection that maps world space to canvas coordinates
+ tform = view.camera.transform.inv() @ view.camera.projection
+ # Assert the camera projects [0, 0, 0] to NDC coordinates [-1, -1]
+ assert np.array_equal((-1, -1), tform.map((0, 0, 0))[:2])
+ # ...and [100, 100, 0] to NDC coordinates [1, 1]
+ assert np.array_equal((1, 1), tform.map((100, 100, 0))[:2])
+
+ zoom_factor = 0.9
+ zoom_to_fit(view, type="orthographic", zoom_factor=zoom_factor)
+ # Assert the camera is still at the center of the scene
+ assert view.camera.transform == snx.Transform().translated((50, 50, 0.5))
+ # Projection that maps world space to canvas coordinates
+ tform = view.camera.transform.inv() @ view.camera.projection
+ # Assert the camera projects [0, 0, 0] to NDC coordinates [-0.9, -0.9]
+ assert np.allclose(
+ (-zoom_factor, -zoom_factor), tform.map((0, 0, 0))[:2], rtol=1e-10
+ )
+ # ...and [100, 100, 0] to NDC coordinates [0.9, 0.9]
+ assert np.allclose(
+ (zoom_factor, zoom_factor), tform.map((100, 100, 0))[:2], rtol=1e-10
+ )
+
+
+def test_zoom_to_fit_perspective() -> None:
+ view = snx.View(
+ scene=snx.Scene(
+ children=[snx.Points(coords=np.asarray([[0, 100, 1], [100, 0, 0]]))]
+ )
+ )
+ zoom_to_fit(view, type="perspective")
+
+ # Assert the camera is moved to the center of the scene
+ # Depth isn't particularly important to test here.
+ assert np.array_equal(
+ view.camera.transform,
+ Transform().translated((50, 50, view.camera.transform.root[3, 2])),
+ )
+ # Projection that maps world space to canvas coordinates
+ tform = view.camera.projection @ view.camera.transform.inv().T
+ # Assert the camera projects [0, 0, 0] to NDC coordinates [-1, -1]
+ assert np.array_equal((-1, -1), _project(tform, (0, 0, 1))[:2])
+ # ...and [100, 100, 0] to NDC coordinates [1, 1]
+ assert np.array_equal((1, 1), _project(tform, (100, 100, 1))[:2])
+ # And assert the entire scene is within the canvas:
+ assert np.all(_project(tform, (0, 0, 0)) > _project(tform, (0, 0, 1)))
+ assert np.all(_project(tform, (100, 100, 0)) < _project(tform, (100, 100, 1)))
+
+ zoom_factor = 0.9
+ zoom_to_fit(view, type="perspective", zoom_factor=zoom_factor)
+ # Assert the camera is still at the center of the scene
+ assert np.array_equal(
+ view.camera.transform,
+ Transform().translated((50, 50, view.camera.transform.root[3, 2])),
+ )
+ # Projection that maps world space to canvas coordinates
+ tform = view.camera.projection @ view.camera.transform.inv().T
+ # Assert the camera projects [0, 0, 0] to NDC coordinates [-0.9, -0.9]
+ assert np.allclose(
+ (-zoom_factor, -zoom_factor), _project(tform, (0, 0, 1))[:2], rtol=1e-10
+ )
+ # ...and [100, 100, 0] to NDC coordinates [0.9, 0.9]
+ assert np.allclose(
+ (zoom_factor, zoom_factor), _project(tform, (100, 100, 1))[:2], rtol=1e-10
+ )
+ # And assert the entire scene is within the canvas:
+ assert np.all(_project(tform, (0, 0, 0)) > _project(tform, (0, 0, 1)))
+ assert np.all(_project(tform, (100, 100, 0)) < _project(tform, (100, 100, 1)))
+
+
+def _project(mat: snx.Transform, world_pos: tuple[float, float, float]) -> np.ndarray:
+ # Inverting the behavior of vec_unproject
+ proj = np.dot(mat.root, np.asarray((*world_pos, 1)))
+ return proj[:2] / proj[3] # type: ignore
diff --git a/uv.lock b/uv.lock
deleted file mode 100644
index e0ae6d40..00000000
--- a/uv.lock
+++ /dev/null
@@ -1,2400 +0,0 @@
-version = 1
-revision = 2
-requires-python = ">=3.10"
-resolution-markers = [
- "python_full_version >= '3.11'",
- "python_full_version < '3.11'",
-]
-
-[[package]]
-name = "annotated-types"
-version = "0.7.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
-]
-
-[[package]]
-name = "asttokens"
-version = "3.0.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978, upload-time = "2024-11-30T04:30:14.439Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918, upload-time = "2024-11-30T04:30:10.946Z" },
-]
-
-[[package]]
-name = "babel"
-version = "2.17.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" },
-]
-
-[[package]]
-name = "backrefs"
-version = "5.9"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/eb/a7/312f673df6a79003279e1f55619abbe7daebbb87c17c976ddc0345c04c7b/backrefs-5.9.tar.gz", hash = "sha256:808548cb708d66b82ee231f962cb36faaf4f2baab032f2fbb783e9c2fdddaa59", size = 5765857, upload-time = "2025-06-22T19:34:13.97Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/19/4d/798dc1f30468134906575156c089c492cf79b5a5fd373f07fe26c4d046bf/backrefs-5.9-py310-none-any.whl", hash = "sha256:db8e8ba0e9de81fcd635f440deab5ae5f2591b54ac1ebe0550a2ca063488cd9f", size = 380267, upload-time = "2025-06-22T19:34:05.252Z" },
- { url = "https://files.pythonhosted.org/packages/55/07/f0b3375bf0d06014e9787797e6b7cc02b38ac9ff9726ccfe834d94e9991e/backrefs-5.9-py311-none-any.whl", hash = "sha256:6907635edebbe9b2dc3de3a2befff44d74f30a4562adbb8b36f21252ea19c5cf", size = 392072, upload-time = "2025-06-22T19:34:06.743Z" },
- { url = "https://files.pythonhosted.org/packages/9d/12/4f345407259dd60a0997107758ba3f221cf89a9b5a0f8ed5b961aef97253/backrefs-5.9-py312-none-any.whl", hash = "sha256:7fdf9771f63e6028d7fee7e0c497c81abda597ea45d6b8f89e8ad76994f5befa", size = 397947, upload-time = "2025-06-22T19:34:08.172Z" },
- { url = "https://files.pythonhosted.org/packages/10/bf/fa31834dc27a7f05e5290eae47c82690edc3a7b37d58f7fb35a1bdbf355b/backrefs-5.9-py313-none-any.whl", hash = "sha256:cc37b19fa219e93ff825ed1fed8879e47b4d89aa7a1884860e2db64ccd7c676b", size = 399843, upload-time = "2025-06-22T19:34:09.68Z" },
- { url = "https://files.pythonhosted.org/packages/fc/24/b29af34b2c9c41645a9f4ff117bae860291780d73880f449e0b5d948c070/backrefs-5.9-py314-none-any.whl", hash = "sha256:df5e169836cc8acb5e440ebae9aad4bf9d15e226d3bad049cf3f6a5c20cc8dc9", size = 411762, upload-time = "2025-06-22T19:34:11.037Z" },
- { url = "https://files.pythonhosted.org/packages/41/ff/392bff89415399a979be4a65357a41d92729ae8580a66073d8ec8d810f98/backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60", size = 380265, upload-time = "2025-06-22T19:34:12.405Z" },
-]
-
-[[package]]
-name = "certifi"
-version = "2025.7.9"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/de/8a/c729b6b60c66a38f590c4e774decc4b2ec7b0576be8f1aa984a53ffa812a/certifi-2025.7.9.tar.gz", hash = "sha256:c1d2ec05395148ee10cf672ffc28cd37ea0ab0d99f9cc74c43e588cbd111b079", size = 160386, upload-time = "2025-07-09T02:13:58.874Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/66/f3/80a3f974c8b535d394ff960a11ac20368e06b736da395b551a49ce950cce/certifi-2025.7.9-py3-none-any.whl", hash = "sha256:d842783a14f8fdd646895ac26f719a061408834473cfc10203f6a575beb15d39", size = 159230, upload-time = "2025-07-09T02:13:57.007Z" },
-]
-
-[[package]]
-name = "cffi"
-version = "1.17.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pycparser" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload-time = "2024-09-04T20:43:30.027Z" },
- { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload-time = "2024-09-04T20:43:32.108Z" },
- { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" },
- { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" },
- { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" },
- { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" },
- { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" },
- { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" },
- { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" },
- { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" },
- { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804, upload-time = "2024-09-04T20:43:48.186Z" },
- { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299, upload-time = "2024-09-04T20:43:49.812Z" },
- { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" },
- { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" },
- { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" },
- { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" },
- { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" },
- { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" },
- { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" },
- { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" },
- { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" },
- { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" },
- { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" },
- { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" },
- { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" },
- { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" },
- { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" },
- { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" },
- { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" },
- { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" },
- { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" },
- { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" },
- { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" },
- { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" },
- { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" },
- { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" },
- { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" },
- { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" },
- { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" },
- { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" },
- { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" },
- { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" },
- { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" },
- { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" },
- { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" },
- { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" },
-]
-
-[[package]]
-name = "cfgv"
-version = "3.4.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" },
-]
-
-[[package]]
-name = "charset-normalizer"
-version = "3.4.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" },
- { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" },
- { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" },
- { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" },
- { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" },
- { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" },
- { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" },
- { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" },
- { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" },
- { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" },
- { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" },
- { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" },
- { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" },
- { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" },
- { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" },
- { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" },
- { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" },
- { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" },
- { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" },
- { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" },
- { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" },
- { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" },
- { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" },
- { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" },
- { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" },
- { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" },
- { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" },
- { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" },
- { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" },
- { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" },
- { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" },
- { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" },
- { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" },
- { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" },
- { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" },
- { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" },
- { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" },
- { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" },
- { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" },
- { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
- { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
- { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
- { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
- { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
- { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
- { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
- { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
- { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
- { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
- { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
- { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
- { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
- { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
-]
-
-[[package]]
-name = "click"
-version = "8.2.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
-]
-
-[[package]]
-name = "cmap"
-version = "0.6.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.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/91/52/f1f5f1fa34f156ca9c94b65a8f2b3d15a1fd34f6c90e1d819d127d4265e8/cmap-0.6.2.tar.gz", hash = "sha256:a511cb0ab349d2ecb7c03f0bb050f5feff5e6fc18d1a503d930b01e3fd80459e", size = 911209, upload-time = "2025-07-05T20:32:31.189Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f2/f8/0d98cc1975f9f4505f5dfec37910f3693f268192b3a00d313f47f77bb5c3/cmap-0.6.2-py3-none-any.whl", hash = "sha256:785f4522d4aec935df93f453fa672da2850817212d1d066269f753a61595689d", size = 958864, upload-time = "2025-07-05T20:32:29.164Z" },
-]
-
-[[package]]
-name = "codespell"
-version = "2.4.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/15/e0/709453393c0ea77d007d907dd436b3ee262e28b30995ea1aa36c6ffbccaf/codespell-2.4.1.tar.gz", hash = "sha256:299fcdcb09d23e81e35a671bbe746d5ad7e8385972e65dbb833a2eaac33c01e5", size = 344740, upload-time = "2025-01-28T18:52:39.411Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/20/01/b394922252051e97aab231d416c86da3d8a6d781eeadcdca1082867de64e/codespell-2.4.1-py3-none-any.whl", hash = "sha256:3dadafa67df7e4a3dbf51e0d7315061b80d265f9552ebd699b3dd6834b47e425", size = 344501, upload-time = "2025-01-28T18:52:37.057Z" },
-]
-
-[[package]]
-name = "colorama"
-version = "0.4.6"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
-]
-
-[[package]]
-name = "coverage"
-version = "7.9.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/04/b7/c0465ca253df10a9e8dae0692a4ae6e9726d245390aaef92360e1d6d3832/coverage-7.9.2.tar.gz", hash = "sha256:997024fa51e3290264ffd7492ec97d0690293ccd2b45a6cd7d82d945a4a80c8b", size = 813556, upload-time = "2025-07-03T10:54:15.101Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a1/0d/5c2114fd776c207bd55068ae8dc1bef63ecd1b767b3389984a8e58f2b926/coverage-7.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:66283a192a14a3854b2e7f3418d7db05cdf411012ab7ff5db98ff3b181e1f912", size = 212039, upload-time = "2025-07-03T10:52:38.955Z" },
- { url = "https://files.pythonhosted.org/packages/cf/ad/dc51f40492dc2d5fcd31bb44577bc0cc8920757d6bc5d3e4293146524ef9/coverage-7.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4e01d138540ef34fcf35c1aa24d06c3de2a4cffa349e29a10056544f35cca15f", size = 212428, upload-time = "2025-07-03T10:52:41.36Z" },
- { url = "https://files.pythonhosted.org/packages/a2/a3/55cb3ff1b36f00df04439c3993d8529193cdf165a2467bf1402539070f16/coverage-7.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f22627c1fe2745ee98d3ab87679ca73a97e75ca75eb5faee48660d060875465f", size = 241534, upload-time = "2025-07-03T10:52:42.956Z" },
- { url = "https://files.pythonhosted.org/packages/eb/c9/a8410b91b6be4f6e9c2e9f0dce93749b6b40b751d7065b4410bf89cb654b/coverage-7.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b1c2d8363247b46bd51f393f86c94096e64a1cf6906803fa8d5a9d03784bdbf", size = 239408, upload-time = "2025-07-03T10:52:44.199Z" },
- { url = "https://files.pythonhosted.org/packages/ff/c4/6f3e56d467c612b9070ae71d5d3b114c0b899b5788e1ca3c93068ccb7018/coverage-7.9.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c10c882b114faf82dbd33e876d0cbd5e1d1ebc0d2a74ceef642c6152f3f4d547", size = 240552, upload-time = "2025-07-03T10:52:45.477Z" },
- { url = "https://files.pythonhosted.org/packages/fd/20/04eda789d15af1ce79bce5cc5fd64057c3a0ac08fd0576377a3096c24663/coverage-7.9.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:de3c0378bdf7066c3988d66cd5232d161e933b87103b014ab1b0b4676098fa45", size = 240464, upload-time = "2025-07-03T10:52:46.809Z" },
- { url = "https://files.pythonhosted.org/packages/a9/5a/217b32c94cc1a0b90f253514815332d08ec0812194a1ce9cca97dda1cd20/coverage-7.9.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1e2f097eae0e5991e7623958a24ced3282676c93c013dde41399ff63e230fcf2", size = 239134, upload-time = "2025-07-03T10:52:48.149Z" },
- { url = "https://files.pythonhosted.org/packages/34/73/1d019c48f413465eb5d3b6898b6279e87141c80049f7dbf73fd020138549/coverage-7.9.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28dc1f67e83a14e7079b6cea4d314bc8b24d1aed42d3582ff89c0295f09b181e", size = 239405, upload-time = "2025-07-03T10:52:49.687Z" },
- { url = "https://files.pythonhosted.org/packages/49/6c/a2beca7aa2595dad0c0d3f350382c381c92400efe5261e2631f734a0e3fe/coverage-7.9.2-cp310-cp310-win32.whl", hash = "sha256:bf7d773da6af9e10dbddacbf4e5cab13d06d0ed93561d44dae0188a42c65be7e", size = 214519, upload-time = "2025-07-03T10:52:51.036Z" },
- { url = "https://files.pythonhosted.org/packages/fc/c8/91e5e4a21f9a51e2c7cdd86e587ae01a4fcff06fc3fa8cde4d6f7cf68df4/coverage-7.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:0c0378ba787681ab1897f7c89b415bd56b0b2d9a47e5a3d8dc0ea55aac118d6c", size = 215400, upload-time = "2025-07-03T10:52:52.313Z" },
- { url = "https://files.pythonhosted.org/packages/39/40/916786453bcfafa4c788abee4ccd6f592b5b5eca0cd61a32a4e5a7ef6e02/coverage-7.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a7a56a2964a9687b6aba5b5ced6971af308ef6f79a91043c05dd4ee3ebc3e9ba", size = 212152, upload-time = "2025-07-03T10:52:53.562Z" },
- { url = "https://files.pythonhosted.org/packages/9f/66/cc13bae303284b546a030762957322bbbff1ee6b6cb8dc70a40f8a78512f/coverage-7.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:123d589f32c11d9be7fe2e66d823a236fe759b0096f5db3fb1b75b2fa414a4fa", size = 212540, upload-time = "2025-07-03T10:52:55.196Z" },
- { url = "https://files.pythonhosted.org/packages/0f/3c/d56a764b2e5a3d43257c36af4a62c379df44636817bb5f89265de4bf8bd7/coverage-7.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:333b2e0ca576a7dbd66e85ab402e35c03b0b22f525eed82681c4b866e2e2653a", size = 245097, upload-time = "2025-07-03T10:52:56.509Z" },
- { url = "https://files.pythonhosted.org/packages/b1/46/bd064ea8b3c94eb4ca5d90e34d15b806cba091ffb2b8e89a0d7066c45791/coverage-7.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:326802760da234baf9f2f85a39e4a4b5861b94f6c8d95251f699e4f73b1835dc", size = 242812, upload-time = "2025-07-03T10:52:57.842Z" },
- { url = "https://files.pythonhosted.org/packages/43/02/d91992c2b29bc7afb729463bc918ebe5f361be7f1daae93375a5759d1e28/coverage-7.9.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19e7be4cfec248df38ce40968c95d3952fbffd57b400d4b9bb580f28179556d2", size = 244617, upload-time = "2025-07-03T10:52:59.239Z" },
- { url = "https://files.pythonhosted.org/packages/b7/4f/8fadff6bf56595a16d2d6e33415841b0163ac660873ed9a4e9046194f779/coverage-7.9.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0b4a4cb73b9f2b891c1788711408ef9707666501ba23684387277ededab1097c", size = 244263, upload-time = "2025-07-03T10:53:00.601Z" },
- { url = "https://files.pythonhosted.org/packages/9b/d2/e0be7446a2bba11739edb9f9ba4eff30b30d8257370e237418eb44a14d11/coverage-7.9.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2c8937fa16c8c9fbbd9f118588756e7bcdc7e16a470766a9aef912dd3f117dbd", size = 242314, upload-time = "2025-07-03T10:53:01.932Z" },
- { url = "https://files.pythonhosted.org/packages/9d/7d/dcbac9345000121b8b57a3094c2dfcf1ccc52d8a14a40c1d4bc89f936f80/coverage-7.9.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:42da2280c4d30c57a9b578bafd1d4494fa6c056d4c419d9689e66d775539be74", size = 242904, upload-time = "2025-07-03T10:53:03.478Z" },
- { url = "https://files.pythonhosted.org/packages/41/58/11e8db0a0c0510cf31bbbdc8caf5d74a358b696302a45948d7c768dfd1cf/coverage-7.9.2-cp311-cp311-win32.whl", hash = "sha256:14fa8d3da147f5fdf9d298cacc18791818f3f1a9f542c8958b80c228320e90c6", size = 214553, upload-time = "2025-07-03T10:53:05.174Z" },
- { url = "https://files.pythonhosted.org/packages/3a/7d/751794ec8907a15e257136e48dc1021b1f671220ecccfd6c4eaf30802714/coverage-7.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:549cab4892fc82004f9739963163fd3aac7a7b0df430669b75b86d293d2df2a7", size = 215441, upload-time = "2025-07-03T10:53:06.472Z" },
- { url = "https://files.pythonhosted.org/packages/62/5b/34abcedf7b946c1c9e15b44f326cb5b0da852885312b30e916f674913428/coverage-7.9.2-cp311-cp311-win_arm64.whl", hash = "sha256:c2667a2b913e307f06aa4e5677f01a9746cd08e4b35e14ebcde6420a9ebb4c62", size = 213873, upload-time = "2025-07-03T10:53:07.699Z" },
- { url = "https://files.pythonhosted.org/packages/53/d7/7deefc6fd4f0f1d4c58051f4004e366afc9e7ab60217ac393f247a1de70a/coverage-7.9.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ae9eb07f1cfacd9cfe8eaee6f4ff4b8a289a668c39c165cd0c8548484920ffc0", size = 212344, upload-time = "2025-07-03T10:53:09.3Z" },
- { url = "https://files.pythonhosted.org/packages/95/0c/ee03c95d32be4d519e6a02e601267769ce2e9a91fc8faa1b540e3626c680/coverage-7.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ce85551f9a1119f02adc46d3014b5ee3f765deac166acf20dbb851ceb79b6f3", size = 212580, upload-time = "2025-07-03T10:53:11.52Z" },
- { url = "https://files.pythonhosted.org/packages/8b/9f/826fa4b544b27620086211b87a52ca67592622e1f3af9e0a62c87aea153a/coverage-7.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8f6389ac977c5fb322e0e38885fbbf901743f79d47f50db706e7644dcdcb6e1", size = 246383, upload-time = "2025-07-03T10:53:13.134Z" },
- { url = "https://files.pythonhosted.org/packages/7f/b3/4477aafe2a546427b58b9c540665feff874f4db651f4d3cb21b308b3a6d2/coverage-7.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff0d9eae8cdfcd58fe7893b88993723583a6ce4dfbfd9f29e001922544f95615", size = 243400, upload-time = "2025-07-03T10:53:14.614Z" },
- { url = "https://files.pythonhosted.org/packages/f8/c2/efffa43778490c226d9d434827702f2dfbc8041d79101a795f11cbb2cf1e/coverage-7.9.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae939811e14e53ed8a9818dad51d434a41ee09df9305663735f2e2d2d7d959b", size = 245591, upload-time = "2025-07-03T10:53:15.872Z" },
- { url = "https://files.pythonhosted.org/packages/c6/e7/a59888e882c9a5f0192d8627a30ae57910d5d449c80229b55e7643c078c4/coverage-7.9.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:31991156251ec202c798501e0a42bbdf2169dcb0f137b1f5c0f4267f3fc68ef9", size = 245402, upload-time = "2025-07-03T10:53:17.124Z" },
- { url = "https://files.pythonhosted.org/packages/92/a5/72fcd653ae3d214927edc100ce67440ed8a0a1e3576b8d5e6d066ed239db/coverage-7.9.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d0d67963f9cbfc7c7f96d4ac74ed60ecbebd2ea6eeb51887af0f8dce205e545f", size = 243583, upload-time = "2025-07-03T10:53:18.781Z" },
- { url = "https://files.pythonhosted.org/packages/5c/f5/84e70e4df28f4a131d580d7d510aa1ffd95037293da66fd20d446090a13b/coverage-7.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49b752a2858b10580969ec6af6f090a9a440a64a301ac1528d7ca5f7ed497f4d", size = 244815, upload-time = "2025-07-03T10:53:20.168Z" },
- { url = "https://files.pythonhosted.org/packages/39/e7/d73d7cbdbd09fdcf4642655ae843ad403d9cbda55d725721965f3580a314/coverage-7.9.2-cp312-cp312-win32.whl", hash = "sha256:88d7598b8ee130f32f8a43198ee02edd16d7f77692fa056cb779616bbea1b355", size = 214719, upload-time = "2025-07-03T10:53:21.521Z" },
- { url = "https://files.pythonhosted.org/packages/9f/d6/7486dcc3474e2e6ad26a2af2db7e7c162ccd889c4c68fa14ea8ec189c9e9/coverage-7.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:9dfb070f830739ee49d7c83e4941cc767e503e4394fdecb3b54bfdac1d7662c0", size = 215509, upload-time = "2025-07-03T10:53:22.853Z" },
- { url = "https://files.pythonhosted.org/packages/b7/34/0439f1ae2593b0346164d907cdf96a529b40b7721a45fdcf8b03c95fcd90/coverage-7.9.2-cp312-cp312-win_arm64.whl", hash = "sha256:4e2c058aef613e79df00e86b6d42a641c877211384ce5bd07585ed7ba71ab31b", size = 213910, upload-time = "2025-07-03T10:53:24.472Z" },
- { url = "https://files.pythonhosted.org/packages/94/9d/7a8edf7acbcaa5e5c489a646226bed9591ee1c5e6a84733c0140e9ce1ae1/coverage-7.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:985abe7f242e0d7bba228ab01070fde1d6c8fa12f142e43debe9ed1dde686038", size = 212367, upload-time = "2025-07-03T10:53:25.811Z" },
- { url = "https://files.pythonhosted.org/packages/e8/9e/5cd6f130150712301f7e40fb5865c1bc27b97689ec57297e568d972eec3c/coverage-7.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c3939264a76d44fde7f213924021ed31f55ef28111a19649fec90c0f109e6d", size = 212632, upload-time = "2025-07-03T10:53:27.075Z" },
- { url = "https://files.pythonhosted.org/packages/a8/de/6287a2c2036f9fd991c61cefa8c64e57390e30c894ad3aa52fac4c1e14a8/coverage-7.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae5d563e970dbe04382f736ec214ef48103d1b875967c89d83c6e3f21706d5b3", size = 245793, upload-time = "2025-07-03T10:53:28.408Z" },
- { url = "https://files.pythonhosted.org/packages/06/cc/9b5a9961d8160e3cb0b558c71f8051fe08aa2dd4b502ee937225da564ed1/coverage-7.9.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdd612e59baed2a93c8843c9a7cb902260f181370f1d772f4842987535071d14", size = 243006, upload-time = "2025-07-03T10:53:29.754Z" },
- { url = "https://files.pythonhosted.org/packages/49/d9/4616b787d9f597d6443f5588619c1c9f659e1f5fc9eebf63699eb6d34b78/coverage-7.9.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:256ea87cb2a1ed992bcdfc349d8042dcea1b80436f4ddf6e246d6bee4b5d73b6", size = 244990, upload-time = "2025-07-03T10:53:31.098Z" },
- { url = "https://files.pythonhosted.org/packages/48/83/801cdc10f137b2d02b005a761661649ffa60eb173dcdaeb77f571e4dc192/coverage-7.9.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f44ae036b63c8ea432f610534a2668b0c3aee810e7037ab9d8ff6883de480f5b", size = 245157, upload-time = "2025-07-03T10:53:32.717Z" },
- { url = "https://files.pythonhosted.org/packages/c8/a4/41911ed7e9d3ceb0ffb019e7635468df7499f5cc3edca5f7dfc078e9c5ec/coverage-7.9.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82d76ad87c932935417a19b10cfe7abb15fd3f923cfe47dbdaa74ef4e503752d", size = 243128, upload-time = "2025-07-03T10:53:34.009Z" },
- { url = "https://files.pythonhosted.org/packages/10/41/344543b71d31ac9cb00a664d5d0c9ef134a0fe87cb7d8430003b20fa0b7d/coverage-7.9.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:619317bb86de4193debc712b9e59d5cffd91dc1d178627ab2a77b9870deb2868", size = 244511, upload-time = "2025-07-03T10:53:35.434Z" },
- { url = "https://files.pythonhosted.org/packages/d5/81/3b68c77e4812105e2a060f6946ba9e6f898ddcdc0d2bfc8b4b152a9ae522/coverage-7.9.2-cp313-cp313-win32.whl", hash = "sha256:0a07757de9feb1dfafd16ab651e0f628fd7ce551604d1bf23e47e1ddca93f08a", size = 214765, upload-time = "2025-07-03T10:53:36.787Z" },
- { url = "https://files.pythonhosted.org/packages/06/a2/7fac400f6a346bb1a4004eb2a76fbff0e242cd48926a2ce37a22a6a1d917/coverage-7.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:115db3d1f4d3f35f5bb021e270edd85011934ff97c8797216b62f461dd69374b", size = 215536, upload-time = "2025-07-03T10:53:38.188Z" },
- { url = "https://files.pythonhosted.org/packages/08/47/2c6c215452b4f90d87017e61ea0fd9e0486bb734cb515e3de56e2c32075f/coverage-7.9.2-cp313-cp313-win_arm64.whl", hash = "sha256:48f82f889c80af8b2a7bb6e158d95a3fbec6a3453a1004d04e4f3b5945a02694", size = 213943, upload-time = "2025-07-03T10:53:39.492Z" },
- { url = "https://files.pythonhosted.org/packages/a3/46/e211e942b22d6af5e0f323faa8a9bc7c447a1cf1923b64c47523f36ed488/coverage-7.9.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:55a28954545f9d2f96870b40f6c3386a59ba8ed50caf2d949676dac3ecab99f5", size = 213088, upload-time = "2025-07-03T10:53:40.874Z" },
- { url = "https://files.pythonhosted.org/packages/d2/2f/762551f97e124442eccd907bf8b0de54348635b8866a73567eb4e6417acf/coverage-7.9.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cdef6504637731a63c133bb2e6f0f0214e2748495ec15fe42d1e219d1b133f0b", size = 213298, upload-time = "2025-07-03T10:53:42.218Z" },
- { url = "https://files.pythonhosted.org/packages/7a/b7/76d2d132b7baf7360ed69be0bcab968f151fa31abe6d067f0384439d9edb/coverage-7.9.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcd5ebe66c7a97273d5d2ddd4ad0ed2e706b39630ed4b53e713d360626c3dbb3", size = 256541, upload-time = "2025-07-03T10:53:43.823Z" },
- { url = "https://files.pythonhosted.org/packages/a0/17/392b219837d7ad47d8e5974ce5f8dc3deb9f99a53b3bd4d123602f960c81/coverage-7.9.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9303aed20872d7a3c9cb39c5d2b9bdbe44e3a9a1aecb52920f7e7495410dfab8", size = 252761, upload-time = "2025-07-03T10:53:45.19Z" },
- { url = "https://files.pythonhosted.org/packages/d5/77/4256d3577fe1b0daa8d3836a1ebe68eaa07dd2cbaf20cf5ab1115d6949d4/coverage-7.9.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc18ea9e417a04d1920a9a76fe9ebd2f43ca505b81994598482f938d5c315f46", size = 254917, upload-time = "2025-07-03T10:53:46.931Z" },
- { url = "https://files.pythonhosted.org/packages/53/99/fc1a008eef1805e1ddb123cf17af864743354479ea5129a8f838c433cc2c/coverage-7.9.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6406cff19880aaaadc932152242523e892faff224da29e241ce2fca329866584", size = 256147, upload-time = "2025-07-03T10:53:48.289Z" },
- { url = "https://files.pythonhosted.org/packages/92/c0/f63bf667e18b7f88c2bdb3160870e277c4874ced87e21426128d70aa741f/coverage-7.9.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d0d4f6ecdf37fcc19c88fec3e2277d5dee740fb51ffdd69b9579b8c31e4232e", size = 254261, upload-time = "2025-07-03T10:53:49.99Z" },
- { url = "https://files.pythonhosted.org/packages/8c/32/37dd1c42ce3016ff8ec9e4b607650d2e34845c0585d3518b2a93b4830c1a/coverage-7.9.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c33624f50cf8de418ab2b4d6ca9eda96dc45b2c4231336bac91454520e8d1fac", size = 255099, upload-time = "2025-07-03T10:53:51.354Z" },
- { url = "https://files.pythonhosted.org/packages/da/2e/af6b86f7c95441ce82f035b3affe1cd147f727bbd92f563be35e2d585683/coverage-7.9.2-cp313-cp313t-win32.whl", hash = "sha256:1df6b76e737c6a92210eebcb2390af59a141f9e9430210595251fbaf02d46926", size = 215440, upload-time = "2025-07-03T10:53:52.808Z" },
- { url = "https://files.pythonhosted.org/packages/4d/bb/8a785d91b308867f6b2e36e41c569b367c00b70c17f54b13ac29bcd2d8c8/coverage-7.9.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f5fd54310b92741ebe00d9c0d1d7b2b27463952c022da6d47c175d246a98d1bd", size = 216537, upload-time = "2025-07-03T10:53:54.273Z" },
- { url = "https://files.pythonhosted.org/packages/1d/a0/a6bffb5e0f41a47279fd45a8f3155bf193f77990ae1c30f9c224b61cacb0/coverage-7.9.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c48c2375287108c887ee87d13b4070a381c6537d30e8487b24ec721bf2a781cb", size = 214398, upload-time = "2025-07-03T10:53:56.715Z" },
- { url = "https://files.pythonhosted.org/packages/d7/85/f8bbefac27d286386961c25515431482a425967e23d3698b75a250872924/coverage-7.9.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:8a1166db2fb62473285bcb092f586e081e92656c7dfa8e9f62b4d39d7e6b5050", size = 204013, upload-time = "2025-07-03T10:54:12.084Z" },
- { url = "https://files.pythonhosted.org/packages/3c/38/bbe2e63902847cf79036ecc75550d0698af31c91c7575352eb25190d0fb3/coverage-7.9.2-py3-none-any.whl", hash = "sha256:e425cd5b00f6fc0ed7cdbd766c70be8baab4b7839e4d4fe5fac48581dd968ea4", size = 204005, upload-time = "2025-07-03T10:54:13.491Z" },
-]
-
-[package.optional-dependencies]
-toml = [
- { name = "tomli", marker = "python_full_version <= '3.11'" },
-]
-
-[[package]]
-name = "csscompressor"
-version = "0.9.5"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f1/2a/8c3ac3d8bc94e6de8d7ae270bb5bc437b210bb9d6d9e46630c98f4abd20c/csscompressor-0.9.5.tar.gz", hash = "sha256:afa22badbcf3120a4f392e4d22f9fff485c044a1feda4a950ecc5eba9dd31a05", size = 237808, upload-time = "2017-11-26T21:13:08.238Z" }
-
-[[package]]
-name = "decorator"
-version = "5.2.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" }
-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 = "distlib"
-version = "0.3.9"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923, upload-time = "2024-10-09T18:35:47.551Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973, upload-time = "2024-10-09T18:35:44.272Z" },
-]
-
-[[package]]
-name = "editdistpy"
-version = "0.1.6"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/0f/e3/be4d6836250feb6347799f4aa683927b7aa5db8e849906c6e54e10db2152/editdistpy-0.1.6.tar.gz", hash = "sha256:33cef3a82c6eb007edc02af65d8c99d67b75ce8e9c980105da4bd8256bcb4b25", size = 117947, upload-time = "2025-06-07T12:00:49.932Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/bb/21/b5a7db1a721bab7d5d949e70f750cbb0553d27cde65b7e18d0bf2d9e3745/editdistpy-0.1.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b113a87b09fcc8dfc9c1c55b814c03f18cf8b77ff0b9d9ea8222d1d1efd9bea5", size = 159062, upload-time = "2025-06-07T12:00:03.89Z" },
- { url = "https://files.pythonhosted.org/packages/bc/d5/5de82f7f8273464e18edfc57d579338399352fde20cdf94fa50f189e53a1/editdistpy-0.1.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bfaa5b6ac420c1b26b36c96f5b1d530bfa24ff65b52a3f59555b4816a4573bdc", size = 158964, upload-time = "2025-06-07T12:00:05.951Z" },
- { url = "https://files.pythonhosted.org/packages/5e/c5/d0adbcc4a06ba59202cb0c6402ed2b8f13101815ac63c5d06e47d00580da/editdistpy-0.1.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:512967c4c137ce8d848671bd60486359d1fa1445b95b5618d45a5287bad80a02", size = 158155, upload-time = "2025-06-07T12:00:08.214Z" },
- { url = "https://files.pythonhosted.org/packages/f1/1f/e65503ea8b35e81cd99077d027da0af4291bb570bc070eeefa29c70156a3/editdistpy-0.1.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e68985bdc2ebc2d87c0546c068628334b01335aba2443a7a1762557eff9a6364", size = 1148680, upload-time = "2025-06-07T12:00:10.025Z" },
- { url = "https://files.pythonhosted.org/packages/ac/f1/c6096dcd08c6eaa667040d1513602daf60e502396da3c5374f55d757d19a/editdistpy-0.1.6-cp310-cp310-win_amd64.whl", hash = "sha256:5ffe5ab732f2a508f5b063bd3367dbb02cdbcad8d1e566148701aea736fcf603", size = 161240, upload-time = "2025-06-07T12:00:11.946Z" },
- { url = "https://files.pythonhosted.org/packages/1f/c5/ad751f8f092df046e15cae9f2cd0ae0760156e325a6e3296b445d400587f/editdistpy-0.1.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46f579137a71dd21baf8f143684b65f0eaad3d31aec31de7daf075324037bff6", size = 159237, upload-time = "2025-06-07T12:00:13.773Z" },
- { url = "https://files.pythonhosted.org/packages/8d/96/9565023bb34364e55e2dc5d29baf6ae4e390eac6c940112f89ef1feb0e15/editdistpy-0.1.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a89cb30d28e25bb10992f914c49702d35d5592a1f850d5c6dd6e8c3dabd525", size = 159070, upload-time = "2025-06-07T12:00:15.168Z" },
- { url = "https://files.pythonhosted.org/packages/57/94/9b4b7cba720844f2534b1f42840649878f38bb98876802973ee5cf78fee5/editdistpy-0.1.6-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:916fdfa5590028dfe1e77d51101bab75a927d75db50395c933017f359fa42c0b", size = 158427, upload-time = "2025-06-07T12:00:16.502Z" },
- { url = "https://files.pythonhosted.org/packages/b7/26/09517c5851825a9330487244c7b22a75c8a9dda70d496364cce01f266e75/editdistpy-0.1.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:461677cf04c4a439c82c3e58a47471b9b37a1e9d75b12d9a2fc5d953b61ae79a", size = 1148964, upload-time = "2025-06-07T12:00:18.492Z" },
- { url = "https://files.pythonhosted.org/packages/3e/d6/9cea8d3a8c9b5e15ef0236ee7099fff0eaa46660b5e95562427f2e34a14d/editdistpy-0.1.6-cp311-cp311-win_amd64.whl", hash = "sha256:34fd6f11ad16d6733dac9309cf155210a655f23cb15092c259187b1cea05e9a8", size = 161696, upload-time = "2025-06-07T12:00:20.412Z" },
- { url = "https://files.pythonhosted.org/packages/14/c7/e3b2b47e74c4afffb422fc7c44ca602d620982f84335f879cdb24f47ea2f/editdistpy-0.1.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa0bae603d1dd8e74618ac6f81e420fd19a8a7e44afdb8dedb1c871debda3e88", size = 159960, upload-time = "2025-06-07T12:00:22.18Z" },
- { url = "https://files.pythonhosted.org/packages/34/5a/c77ac24f215400d1cf2ec6e413d76e6560abad608ca0f868bb167808f6d6/editdistpy-0.1.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3104f9807d5c59f70c1d4b144eb31775525d005ae160ccbe5593f956fe09bde", size = 159789, upload-time = "2025-06-07T12:00:23.584Z" },
- { url = "https://files.pythonhosted.org/packages/bd/77/3f5bb75ba8ff713b60572f9bab947914ccbc57ab782035fd3eeb82c2f03c/editdistpy-0.1.6-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e9105e1a770989cee59aa049b972d63cdc3b04822247f548bf7af136f83fd20", size = 159589, upload-time = "2025-06-07T12:00:25.001Z" },
- { url = "https://files.pythonhosted.org/packages/9f/7f/3c0fb0aae0d9962ec85bf07114f4d01ac680dcfe76245dd9e793c4fa8a49/editdistpy-0.1.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:63bc15e924120ecbc45697314bd9c416c67e23aa1dae218ef74d32d859353a89", size = 1150312, upload-time = "2025-06-07T12:00:27.002Z" },
- { url = "https://files.pythonhosted.org/packages/43/21/83ea650770a5709299807a34df58d93fba598780e6c928aa77da3d574608/editdistpy-0.1.6-cp312-cp312-win_amd64.whl", hash = "sha256:cf2153f8ab6b9bff027253d6e216b731a62e88286779a5ab68ebc7cfd8ee3d01", size = 162138, upload-time = "2025-06-07T12:00:29.074Z" },
- { url = "https://files.pythonhosted.org/packages/81/f6/8f204358a583698347e575f34797061a0d6827f3b870904a6ef4d9463bf2/editdistpy-0.1.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:28baa481c7e283bb485d717ab2983d1e02820cb7809bd809c22d2c900dfa8a35", size = 158903, upload-time = "2025-06-07T12:00:30.54Z" },
- { url = "https://files.pythonhosted.org/packages/ae/97/86647e1d6a02923dfb9f933f7ffaf0eeff5a764fcf795ec0068ef8cc8993/editdistpy-0.1.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9f15444910036df38d654a36509c767b31ffb80bf7100709c456c62dc5fda853", size = 158731, upload-time = "2025-06-07T12:00:31.99Z" },
- { url = "https://files.pythonhosted.org/packages/f5/e0/2253b73b598b338cc71b3f3d97b1156cc5d11c43216c1834ce33bec93a3f/editdistpy-0.1.6-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd8395ae5c9ddb5d31ce4f9be4cf80d662a73dcbd79fe5e1b1d0bdb6cba363ea", size = 158782, upload-time = "2025-06-07T12:00:33.826Z" },
- { url = "https://files.pythonhosted.org/packages/db/f6/a4ab9df1c044fb1f04019a031b642c40bc6c4f4ebdf8853e95ef8cad52b0/editdistpy-0.1.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3b2865c571a069429a6374189d570c26c2ccb3a73fe5871f36d8a28882669fc", size = 1149388, upload-time = "2025-06-07T12:00:35.855Z" },
- { url = "https://files.pythonhosted.org/packages/c7/fc/27b9d5175a46f7ebabeccd6536cef126adbd4628338bac00d62985fc217f/editdistpy-0.1.6-cp313-cp313-win_amd64.whl", hash = "sha256:cdbca54505e29b32b25b8956ac6bef739cdca32351e9548db38f2413ccf802c1", size = 160607, upload-time = "2025-06-07T12:00:39.816Z" },
-]
-
-[[package]]
-name = "exceptiongroup"
-version = "1.3.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "typing-extensions", marker = "python_full_version < '3.11'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" },
-]
-
-[[package]]
-name = "executing"
-version = "2.2.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693, upload-time = "2025-01-22T15:41:29.403Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702, upload-time = "2025-01-22T15:41:25.929Z" },
-]
-
-[[package]]
-name = "fancycompleter"
-version = "0.11.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pyrepl", marker = "python_full_version < '3.13'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/4e/4c/d11187dee93eff89d082afda79b63c79320ae1347e49485a38f05ad359d0/fancycompleter-0.11.1.tar.gz", hash = "sha256:5b4ad65d76b32b1259251516d0f1cb2d82832b1ff8506697a707284780757f69", size = 341776, upload-time = "2025-05-26T12:59:11.045Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/30/c3/6f0e3896f193528bbd2b4d2122d4be8108a37efab0b8475855556a8c4afa/fancycompleter-0.11.1-py3-none-any.whl", hash = "sha256:44243d7fab37087208ca5acacf8f74c0aa4d733d04d593857873af7513cdf8a6", size = 11207, upload-time = "2025-05-26T12:59:09.857Z" },
-]
-
-[[package]]
-name = "filelock"
-version = "3.18.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" },
-]
-
-[[package]]
-name = "freetype-py"
-version = "2.5.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d0/9c/61ba17f846b922c2d6d101cc886b0e8fb597c109cedfcb39b8c5d2304b54/freetype-py-2.5.1.zip", hash = "sha256:cfe2686a174d0dd3d71a9d8ee9bf6a2c23f5872385cf8ce9f24af83d076e2fbd", size = 851738, upload-time = "2024-08-29T18:32:26.37Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/38/a8/258dd138ebe60c79cd8cfaa6d021599208a33f0175a5e29b01f60c9ab2c7/freetype_py-2.5.1-py3-none-macosx_10_9_universal2.whl", hash = "sha256:d01ded2557694f06aa0413f3400c0c0b2b5ebcaabeef7aaf3d756be44f51e90b", size = 1747885, upload-time = "2024-08-29T18:32:17.604Z" },
- { url = "https://files.pythonhosted.org/packages/a2/93/280ad06dc944e40789b0a641492321a2792db82edda485369cbc59d14366/freetype_py-2.5.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d2f6b3d68496797da23204b3b9c4e77e67559c80390fc0dc8b3f454ae1cd819", size = 1051055, upload-time = "2024-08-29T18:32:19.153Z" },
- { url = "https://files.pythonhosted.org/packages/b6/36/853cad240ec63e21a37a512ee19c896b655ce1772d803a3dd80fccfe63fe/freetype_py-2.5.1-py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:289b443547e03a4f85302e3ac91376838e0d11636050166662a4f75e3087ed0b", size = 1043856, upload-time = "2024-08-29T18:32:20.565Z" },
- { url = "https://files.pythonhosted.org/packages/93/6f/fcc1789e42b8c6617c3112196d68e87bfe7d957d80812d3c24d639782dcb/freetype_py-2.5.1-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:cd3bfdbb7e1a84818cfbc8025fca3096f4f2afcd5d4641184bf0a3a2e6f97bbf", size = 1108180, upload-time = "2024-08-29T18:32:21.871Z" },
- { url = "https://files.pythonhosted.org/packages/2a/1b/161d3a6244b8a820aef188e4397a750d4a8196316809576d015f26594296/freetype_py-2.5.1-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:3c1aefc4f0d5b7425f014daccc5fdc7c6f914fb7d6a695cc684f1c09cd8c1660", size = 1106792, upload-time = "2024-08-29T18:32:23.134Z" },
- { url = "https://files.pythonhosted.org/packages/93/6e/bd7fbfacca077bc6f34f1a1109800a2c41ab50f4704d3a0507ba41009915/freetype_py-2.5.1-py3-none-win_amd64.whl", hash = "sha256:0b7f8e0342779f65ca13ef8bc103938366fecade23e6bb37cb671c2b8ad7f124", size = 814608, upload-time = "2024-08-29T18:32:24.648Z" },
-]
-
-[[package]]
-name = "ghp-import"
-version = "2.1.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "python-dateutil" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" },
-]
-
-[[package]]
-name = "glfw"
-version = "2.9.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/38/97/a2d667c98b8474f6b8294042488c1bd488681fb3cb4c3b9cdac1a9114287/glfw-2.9.0.tar.gz", hash = "sha256:077111a150ff09bc302c5e4ae265a5eb6aeaff0c8b01f727f7fb34e3764bb8e2", size = 31453, upload-time = "2025-04-15T15:39:54.142Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/21/71/13dd8a8d547809543d21de9438a3a76a8728fc7966d01ad9fb54599aebf5/glfw-2.9.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-macosx_10_6_intel.whl", hash = "sha256:183da99152f63469e9263146db2eb1b6cc4ee0c4082b280743e57bd1b0a3bd70", size = 105297, upload-time = "2025-04-15T15:39:39.677Z" },
- { url = "https://files.pythonhosted.org/packages/f8/a2/45e6dceec1e0a0ffa8dd3c0ecf1e11d74639a55186243129160c6434d456/glfw-2.9.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-macosx_11_0_arm64.whl", hash = "sha256:aef5b555673b9555216e4cd7bc0bdbbb9983f66c620a85ba7310cfcfda5cd38c", size = 102146, upload-time = "2025-04-15T15:39:42.354Z" },
- { url = "https://files.pythonhosted.org/packages/d2/72/b6261ed918e3747c6070fe80636c63a3c8f1c42ce122670315eeeada156f/glfw-2.9.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-manylinux2014_aarch64.whl", hash = "sha256:fcc430cb21984afba74945b7df38a5e1a02b36c0b4a2a2bab42b4a26d7cc51d6", size = 230002, upload-time = "2025-04-15T15:39:43.933Z" },
- { url = "https://files.pythonhosted.org/packages/45/d6/7f95786332e8b798569b8e60db2ee081874cec2a62572b8ec55c309d85b7/glfw-2.9.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-manylinux2014_x86_64.whl", hash = "sha256:7f85b58546880466ac445fc564c5c831ca93c8a99795ab8eaf0a2d521af293d7", size = 241949, upload-time = "2025-04-15T15:39:45.28Z" },
- { url = "https://files.pythonhosted.org/packages/a1/e6/093ab7874a74bba351e754f6e7748c031bd7276702135da6cbcd00e1f3e2/glfw-2.9.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-manylinux_2_28_aarch64.whl", hash = "sha256:2123716c8086b80b797e849a534fc6f21aebca300519e57c80618a65ca8135dc", size = 231016, upload-time = "2025-04-15T15:39:46.669Z" },
- { url = "https://files.pythonhosted.org/packages/7f/ba/de3630757c7d7fc2086aaf3994926d6b869d31586e4d0c14f1666af31b93/glfw-2.9.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-manylinux_2_28_x86_64.whl", hash = "sha256:4e11271e49eb9bc53431ade022e284d5a59abeace81fe3b178db1bf3ccc0c449", size = 243489, upload-time = "2025-04-15T15:39:48.321Z" },
- { url = "https://files.pythonhosted.org/packages/32/36/c3bada8503681806231d1705ea1802bac8febf69e4186b9f0f0b9e2e4f7e/glfw-2.9.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-win32.whl", hash = "sha256:8e4fbff88e4e953bb969b6813195d5de4641f886530cc8083897e56b00bf2c8e", size = 552655, upload-time = "2025-04-15T15:39:50.029Z" },
- { url = "https://files.pythonhosted.org/packages/cb/70/7f2f052ca20c3b69892818f2ee1fea53b037ea9145ff75b944ed1dc4ff82/glfw-2.9.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-win_amd64.whl", hash = "sha256:9aa3ae51601601c53838315bd2a03efb1e6bebecd072b2f64ddbd0b2556d511a", size = 559441, upload-time = "2025-04-15T15:39:52.531Z" },
-]
-
-[[package]]
-name = "griffe"
-version = "1.7.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "colorama" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/a9/3e/5aa9a61f7c3c47b0b52a1d930302992229d191bf4bc76447b324b731510a/griffe-1.7.3.tar.gz", hash = "sha256:52ee893c6a3a968b639ace8015bec9d36594961e156e23315c8e8e51401fa50b", size = 395137, upload-time = "2025-04-23T11:29:09.147Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/58/c6/5c20af38c2a57c15d87f7f38bee77d63c1d2a3689f74fefaf35915dd12b2/griffe-1.7.3-py3-none-any.whl", hash = "sha256:c6b3ee30c2f0f17f30bcdef5068d6ab7a2a4f1b8bf1a3e74b56fffd21e1c5f75", size = 129303, upload-time = "2025-04-23T11:29:07.145Z" },
-]
-
-[[package]]
-name = "hsluv"
-version = "5.0.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ac/81/af16607fa045724e515579d312577261b436f36f419e7c677e7e88fcc943/hsluv-5.0.4.tar.gz", hash = "sha256:2281f946427a882010042844a38c7bbe9e0d0aaf9d46babe46366ed6f169b72e", size = 543090, upload-time = "2023-09-11T21:46:52.022Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/96/36/5bddefea3d7adf22a64f9aa9701492f8a9fe6948223f5cf2602c22ec9be7/hsluv-5.0.4-py2.py3-none-any.whl", hash = "sha256:0138bd10038e2ee1b13eecae9a7d49d4ec8c320b1d7eb4f860832c792e3e4567", size = 5252, upload-time = "2023-09-11T21:46:50.407Z" },
-]
-
-[[package]]
-name = "htmlmin2"
-version = "0.1.13"
-source = { registry = "https://pypi.org/simple" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/be/31/a76f4bfa885f93b8167cb4c85cf32b54d1f64384d0b897d45bc6d19b7b45/htmlmin2-0.1.13-py3-none-any.whl", hash = "sha256:75609f2a42e64f7ce57dbff28a39890363bde9e7e5885db633317efbdf8c79a2", size = 34486, upload-time = "2023-03-14T21:28:30.388Z" },
-]
-
-[[package]]
-name = "identify"
-version = "2.6.12"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a2/88/d193a27416618628a5eea64e3223acd800b40749a96ffb322a9b55a49ed1/identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6", size = 99254, upload-time = "2025-05-23T20:37:53.3Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2", size = 99145, upload-time = "2025-05-23T20:37:51.495Z" },
-]
-
-[[package]]
-name = "idna"
-version = "3.10"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
-]
-
-[[package]]
-name = "imageio"
-version = "2.37.0"
-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.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
- { name = "pillow" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/0c/47/57e897fb7094afb2d26e8b2e4af9a45c7cf1a405acdeeca001fdf2c98501/imageio-2.37.0.tar.gz", hash = "sha256:71b57b3669666272c818497aebba2b4c5f20d5b37c81720e5e1a56d59c492996", size = 389963, upload-time = "2025-01-20T02:42:37.089Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/cb/bd/b394387b598ed84d8d0fa90611a90bee0adc2021820ad5729f7ced74a8e2/imageio-2.37.0-py3-none-any.whl", hash = "sha256:11efa15b87bc7871b61590326b2d635439acc321cf7f8ce996f812543ce10eed", size = 315796, upload-time = "2025-01-20T02:42:34.931Z" },
-]
-
-[package.optional-dependencies]
-tifffile = [
- { name = "tifffile", version = "2025.5.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "tifffile", version = "2025.6.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
-]
-
-[[package]]
-name = "imgui-bundle"
-version = "1.6.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "glfw" },
- { name = "munch" },
- { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "numpy", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
- { name = "pillow" },
- { name = "pydantic" },
- { name = "pyopengl" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/34/99/b448b548cd7162516215bb74d5ca84ca85c800859c2d4e1df0acb4adff2c/imgui_bundle-1.6.2.tar.gz", hash = "sha256:17b2019a0d4ebb66bc29b234c086ea5a3cccc7c0c540517fe569d84372316970", size = 35446860, upload-time = "2025-01-04T18:28:04.552Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/6e/05/a96b72ce8fab391b94cf876e20c1c3c01f73d622e20f48a2ed344622566a/imgui_bundle-1.6.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:dbf2a82baf6d8ab4787ed531baa339c2c3f8f76f2c70f3c764f8f3c3e6ed2097", size = 13771697, upload-time = "2025-01-04T18:00:59.453Z" },
- { url = "https://files.pythonhosted.org/packages/02/76/5115d550c09ac350096440d55e69826e4dc08e42c13c14bcbc2537ef935d/imgui_bundle-1.6.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:519194a5b4fd3c1748bcf5b9afb6575b883e68c87947ee1332f4fa27fbcfcd14", size = 14588838, upload-time = "2025-01-04T18:26:54.578Z" },
- { url = "https://files.pythonhosted.org/packages/ca/64/290c08415d4948c87a7d183b72e82b8de167151a3bb40253583ff8cc5548/imgui_bundle-1.6.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80e96d31e1ce56df6de1120c0f0764ab54e6af1efd1752499133cba7e879b1e9", size = 22071859, upload-time = "2025-01-04T18:27:06.461Z" },
- { url = "https://files.pythonhosted.org/packages/19/78/3155e48d35e892734ed3cc1df17ef5fd95077bf75a01c6256cd5df069495/imgui_bundle-1.6.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3baefbb7374909541900dec15d4630f06e4f1413c3e6560ec619e210a0f5d101", size = 24188110, upload-time = "2025-01-04T18:27:13.472Z" },
- { url = "https://files.pythonhosted.org/packages/65/d3/985cd35d5786f312e7c246e0b48c33cfc400e3b57fccfe1c6e7cc2f68edb/imgui_bundle-1.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:e6ba109dbc788a217d283cb7159391f40f6efacd79d8d1a4fa61962220573fbc", size = 35901291, upload-time = "2025-01-04T18:27:18.056Z" },
- { url = "https://files.pythonhosted.org/packages/a9/93/67186150aefc129cdd1f5dd92d437afa96e284e709377e711995e2c952b7/imgui_bundle-1.6.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:486e8408d4b0e3c2c8ef1814ddfde0aa87d25e6f8a650b7ccda672a2e1b47062", size = 13776771, upload-time = "2025-01-04T18:01:22.312Z" },
- { url = "https://files.pythonhosted.org/packages/c1/9c/e434ffb35da32c3b235475eda2aad0fc27d6ad4dfbaf45f7edf241c155a5/imgui_bundle-1.6.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:1ec8db8526eb6ad7dfe77c38ea4a19d5beaef2f79a0b080515929df77ac0c5fc", size = 14606484, upload-time = "2025-01-04T18:27:23.3Z" },
- { url = "https://files.pythonhosted.org/packages/e7/6d/12c951f7d8eafebbacc3f4f69fe1659cbef5f438d4e9f4d327fe7a2ebdf4/imgui_bundle-1.6.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5152504650622110559d5039fa60b09da91a5b7f8bcd4065fdf7f053b1967ad7", size = 22078554, upload-time = "2025-01-04T18:27:27.897Z" },
- { url = "https://files.pythonhosted.org/packages/f7/85/c2e4fbbadd7a4ec8fe491761a15bcd4f0d9c34baed7f154cfda213e4e237/imgui_bundle-1.6.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:40a4f4d1a89b1e5b421077cbdde3f217b0b24468875c930625c9f5749421617f", size = 24191812, upload-time = "2025-01-04T18:27:31.814Z" },
- { url = "https://files.pythonhosted.org/packages/ec/90/404a3df0fe6e833f65cdb2d321f96e56cfda360b845298df7bfbca9d6f8c/imgui_bundle-1.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:add71c6733b6785e48bb059472e10e49d7ef56108fac7eb2bf05692cc6040b73", size = 35884517, upload-time = "2025-01-04T18:27:37.831Z" },
- { url = "https://files.pythonhosted.org/packages/1b/99/a5e31449fabb296de05a4031b0d99ac73a8ca518e2f092920a0143c4db0c/imgui_bundle-1.6.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f2e4494339209a8417d745658b4d36efe03949536a2b02492a03b2eb8f187850", size = 13776720, upload-time = "2025-01-04T18:01:42.755Z" },
- { url = "https://files.pythonhosted.org/packages/53/d7/87bf35d8856c382b49b641312995f0e0a3134853d2b58954a89cd916dea8/imgui_bundle-1.6.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:2a26dd3475271caf0ea5ca800bde95c9bc4cc889739869d9c6c0335c11d579e4", size = 14606842, upload-time = "2025-01-04T18:27:42.965Z" },
- { url = "https://files.pythonhosted.org/packages/5a/24/3c4c39fca1660d311a1e5cdc12294bb6eb660eb821ddfed8f70beee71f0f/imgui_bundle-1.6.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3316e7227d25a1618205db478a7c8258a6f667f9e6a532e6a1075e26c33ded1e", size = 22078357, upload-time = "2025-01-04T18:27:47.824Z" },
- { url = "https://files.pythonhosted.org/packages/09/ac/ce71d687fd00342bcc7f1c0960e42f31e17b772ff68afec1cf1a9944f376/imgui_bundle-1.6.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c76d14d2a270ff8b5de55aa8fafc255402570ebe6f3cbe0a6fd030c26f48b217", size = 24191812, upload-time = "2025-01-04T18:27:52.528Z" },
- { url = "https://files.pythonhosted.org/packages/73/62/7b413c5c24f34c27c0e4f033e088c1a977d0020ebe0005dec92fa1633eee/imgui_bundle-1.6.2-cp313-cp313-win_amd64.whl", hash = "sha256:4bf9c0afb85dfdb9ca49e63d388a2a6e5b8935a5d9552fe7c51b3969caa5d4f8", size = 35884807, upload-time = "2025-01-04T18:27:57.428Z" },
-]
-
-[[package]]
-name = "importlib-metadata"
-version = "8.7.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "zipp" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" },
-]
-
-[[package]]
-name = "importlib-resources"
-version = "6.5.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" },
-]
-
-[[package]]
-name = "iniconfig"
-version = "2.1.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
-]
-
-[[package]]
-name = "ipython"
-version = "8.37.0"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version < '3.11'",
-]
-dependencies = [
- { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" },
- { name = "decorator", marker = "python_full_version < '3.11'" },
- { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
- { name = "jedi", marker = "python_full_version < '3.11'" },
- { name = "matplotlib-inline", marker = "python_full_version < '3.11'" },
- { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
- { name = "prompt-toolkit", marker = "python_full_version < '3.11'" },
- { name = "pygments", marker = "python_full_version < '3.11'" },
- { name = "stack-data", marker = "python_full_version < '3.11'" },
- { name = "traitlets", marker = "python_full_version < '3.11'" },
- { name = "typing-extensions", marker = "python_full_version < '3.11'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/85/31/10ac88f3357fc276dc8a64e8880c82e80e7459326ae1d0a211b40abf6665/ipython-8.37.0.tar.gz", hash = "sha256:ca815841e1a41a1e6b73a0b08f3038af9b2252564d01fc405356d34033012216", size = 5606088, upload-time = "2025-05-31T16:39:09.613Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/91/d0/274fbf7b0b12643cbbc001ce13e6a5b1607ac4929d1b11c72460152c9fc3/ipython-8.37.0-py3-none-any.whl", hash = "sha256:ed87326596b878932dbcb171e3e698845434d8c61b8d8cd474bf663041a9dcf2", size = 831864, upload-time = "2025-05-31T16:39:06.38Z" },
-]
-
-[[package]]
-name = "ipython"
-version = "9.4.0"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.11'",
-]
-dependencies = [
- { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" },
- { name = "decorator", marker = "python_full_version >= '3.11'" },
- { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" },
- { name = "jedi", marker = "python_full_version >= '3.11'" },
- { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" },
- { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
- { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" },
- { name = "pygments", marker = "python_full_version >= '3.11'" },
- { name = "stack-data", marker = "python_full_version >= '3.11'" },
- { name = "traitlets", marker = "python_full_version >= '3.11'" },
- { name = "typing-extensions", marker = "python_full_version == '3.11.*'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/54/80/406f9e3bde1c1fd9bf5a0be9d090f8ae623e401b7670d8f6fdf2ab679891/ipython-9.4.0.tar.gz", hash = "sha256:c033c6d4e7914c3d9768aabe76bbe87ba1dc66a92a05db6bfa1125d81f2ee270", size = 4385338, upload-time = "2025-07-01T11:11:30.606Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/63/f8/0031ee2b906a15a33d6bfc12dd09c3dfa966b3cb5b284ecfb7549e6ac3c4/ipython-9.4.0-py3-none-any.whl", hash = "sha256:25850f025a446d9b359e8d296ba175a36aedd32e83ca9b5060430fe16801f066", size = 611021, upload-time = "2025-07-01T11:11:27.85Z" },
-]
-
-[[package]]
-name = "ipython-pygments-lexers"
-version = "1.1.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pygments", marker = "python_full_version >= '3.11'" },
-]
-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 = [
- { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" },
-]
-
-[[package]]
-name = "jedi"
-version = "0.19.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "parso" },
-]
-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 = [
- { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" },
-]
-
-[[package]]
-name = "jinja2"
-version = "3.1.6"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "markupsafe" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
-]
-
-[[package]]
-name = "jsmin"
-version = "3.0.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/5e/73/e01e4c5e11ad0494f4407a3f623ad4d87714909f50b17a06ed121034ff6e/jsmin-3.0.1.tar.gz", hash = "sha256:c0959a121ef94542e807a674142606f7e90214a2b3d1eb17300244bbb5cc2bfc", size = 13925, upload-time = "2022-01-16T20:35:59.13Z" }
-
-[[package]]
-name = "kiwisolver"
-version = "1.4.8"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538, upload-time = "2024-12-24T18:30:51.519Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/47/5f/4d8e9e852d98ecd26cdf8eaf7ed8bc33174033bba5e07001b289f07308fd/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db", size = 124623, upload-time = "2024-12-24T18:28:17.687Z" },
- { url = "https://files.pythonhosted.org/packages/1d/70/7f5af2a18a76fe92ea14675f8bd88ce53ee79e37900fa5f1a1d8e0b42998/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b", size = 66720, upload-time = "2024-12-24T18:28:19.158Z" },
- { url = "https://files.pythonhosted.org/packages/c6/13/e15f804a142353aefd089fadc8f1d985561a15358c97aca27b0979cb0785/kiwisolver-1.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d", size = 65413, upload-time = "2024-12-24T18:28:20.064Z" },
- { url = "https://files.pythonhosted.org/packages/ce/6d/67d36c4d2054e83fb875c6b59d0809d5c530de8148846b1370475eeeece9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d", size = 1650826, upload-time = "2024-12-24T18:28:21.203Z" },
- { url = "https://files.pythonhosted.org/packages/de/c6/7b9bb8044e150d4d1558423a1568e4f227193662a02231064e3824f37e0a/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c", size = 1628231, upload-time = "2024-12-24T18:28:23.851Z" },
- { url = "https://files.pythonhosted.org/packages/b6/38/ad10d437563063eaaedbe2c3540a71101fc7fb07a7e71f855e93ea4de605/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3", size = 1408938, upload-time = "2024-12-24T18:28:26.687Z" },
- { url = "https://files.pythonhosted.org/packages/52/ce/c0106b3bd7f9e665c5f5bc1e07cc95b5dabd4e08e3dad42dbe2faad467e7/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed", size = 1422799, upload-time = "2024-12-24T18:28:30.538Z" },
- { url = "https://files.pythonhosted.org/packages/d0/87/efb704b1d75dc9758087ba374c0f23d3254505edaedd09cf9d247f7878b9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f", size = 1354362, upload-time = "2024-12-24T18:28:32.943Z" },
- { url = "https://files.pythonhosted.org/packages/eb/b3/fd760dc214ec9a8f208b99e42e8f0130ff4b384eca8b29dd0efc62052176/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff", size = 2222695, upload-time = "2024-12-24T18:28:35.641Z" },
- { url = "https://files.pythonhosted.org/packages/a2/09/a27fb36cca3fc01700687cc45dae7a6a5f8eeb5f657b9f710f788748e10d/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d", size = 2370802, upload-time = "2024-12-24T18:28:38.357Z" },
- { url = "https://files.pythonhosted.org/packages/3d/c3/ba0a0346db35fe4dc1f2f2cf8b99362fbb922d7562e5f911f7ce7a7b60fa/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c", size = 2334646, upload-time = "2024-12-24T18:28:40.941Z" },
- { url = "https://files.pythonhosted.org/packages/41/52/942cf69e562f5ed253ac67d5c92a693745f0bed3c81f49fc0cbebe4d6b00/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605", size = 2467260, upload-time = "2024-12-24T18:28:42.273Z" },
- { url = "https://files.pythonhosted.org/packages/32/26/2d9668f30d8a494b0411d4d7d4ea1345ba12deb6a75274d58dd6ea01e951/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e", size = 2288633, upload-time = "2024-12-24T18:28:44.87Z" },
- { url = "https://files.pythonhosted.org/packages/98/99/0dd05071654aa44fe5d5e350729961e7bb535372935a45ac89a8924316e6/kiwisolver-1.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751", size = 71885, upload-time = "2024-12-24T18:28:47.346Z" },
- { url = "https://files.pythonhosted.org/packages/6c/fc/822e532262a97442989335394d441cd1d0448c2e46d26d3e04efca84df22/kiwisolver-1.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271", size = 65175, upload-time = "2024-12-24T18:28:49.651Z" },
- { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635, upload-time = "2024-12-24T18:28:51.826Z" },
- { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717, upload-time = "2024-12-24T18:28:54.256Z" },
- { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413, upload-time = "2024-12-24T18:28:55.184Z" },
- { url = "https://files.pythonhosted.org/packages/a9/98/1df4089b1ed23d83d410adfdc5947245c753bddfbe06541c4aae330e9e70/kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", size = 1343994, upload-time = "2024-12-24T18:28:57.493Z" },
- { url = "https://files.pythonhosted.org/packages/8d/bf/b4b169b050c8421a7c53ea1ea74e4ef9c335ee9013216c558a047f162d20/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", size = 1434804, upload-time = "2024-12-24T18:29:00.077Z" },
- { url = "https://files.pythonhosted.org/packages/66/5a/e13bd341fbcf73325ea60fdc8af752addf75c5079867af2e04cc41f34434/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", size = 1450690, upload-time = "2024-12-24T18:29:01.401Z" },
- { url = "https://files.pythonhosted.org/packages/9b/4f/5955dcb376ba4a830384cc6fab7d7547bd6759fe75a09564910e9e3bb8ea/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", size = 1376839, upload-time = "2024-12-24T18:29:02.685Z" },
- { url = "https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", size = 1435109, upload-time = "2024-12-24T18:29:04.113Z" },
- { url = "https://files.pythonhosted.org/packages/13/fc/e756382cb64e556af6c1809a1bbb22c141bbc2445049f2da06b420fe52bf/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", size = 2245269, upload-time = "2024-12-24T18:29:05.488Z" },
- { url = "https://files.pythonhosted.org/packages/76/15/e59e45829d7f41c776d138245cabae6515cb4eb44b418f6d4109c478b481/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", size = 2393468, upload-time = "2024-12-24T18:29:06.79Z" },
- { url = "https://files.pythonhosted.org/packages/e9/39/483558c2a913ab8384d6e4b66a932406f87c95a6080112433da5ed668559/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", size = 2355394, upload-time = "2024-12-24T18:29:08.24Z" },
- { url = "https://files.pythonhosted.org/packages/01/aa/efad1fbca6570a161d29224f14b082960c7e08268a133fe5dc0f6906820e/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", size = 2490901, upload-time = "2024-12-24T18:29:09.653Z" },
- { url = "https://files.pythonhosted.org/packages/c9/4f/15988966ba46bcd5ab9d0c8296914436720dd67fca689ae1a75b4ec1c72f/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", size = 2312306, upload-time = "2024-12-24T18:29:12.644Z" },
- { url = "https://files.pythonhosted.org/packages/2d/27/bdf1c769c83f74d98cbc34483a972f221440703054894a37d174fba8aa68/kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", size = 71966, upload-time = "2024-12-24T18:29:14.089Z" },
- { url = "https://files.pythonhosted.org/packages/4a/c9/9642ea855604aeb2968a8e145fc662edf61db7632ad2e4fb92424be6b6c0/kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", size = 65311, upload-time = "2024-12-24T18:29:15.892Z" },
- { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152, upload-time = "2024-12-24T18:29:16.85Z" },
- { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555, upload-time = "2024-12-24T18:29:19.146Z" },
- { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067, upload-time = "2024-12-24T18:29:20.096Z" },
- { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443, upload-time = "2024-12-24T18:29:22.843Z" },
- { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728, upload-time = "2024-12-24T18:29:24.463Z" },
- { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388, upload-time = "2024-12-24T18:29:25.776Z" },
- { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849, upload-time = "2024-12-24T18:29:27.202Z" },
- { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533, upload-time = "2024-12-24T18:29:28.638Z" },
- { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898, upload-time = "2024-12-24T18:29:30.368Z" },
- { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605, upload-time = "2024-12-24T18:29:33.151Z" },
- { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801, upload-time = "2024-12-24T18:29:34.584Z" },
- { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077, upload-time = "2024-12-24T18:29:36.138Z" },
- { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410, upload-time = "2024-12-24T18:29:39.991Z" },
- { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853, upload-time = "2024-12-24T18:29:42.006Z" },
- { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424, upload-time = "2024-12-24T18:29:44.38Z" },
- { url = "https://files.pythonhosted.org/packages/79/b3/e62464a652f4f8cd9006e13d07abad844a47df1e6537f73ddfbf1bc997ec/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", size = 124156, upload-time = "2024-12-24T18:29:45.368Z" },
- { url = "https://files.pythonhosted.org/packages/8d/2d/f13d06998b546a2ad4f48607a146e045bbe48030774de29f90bdc573df15/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", size = 66555, upload-time = "2024-12-24T18:29:46.37Z" },
- { url = "https://files.pythonhosted.org/packages/59/e3/b8bd14b0a54998a9fd1e8da591c60998dc003618cb19a3f94cb233ec1511/kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", size = 65071, upload-time = "2024-12-24T18:29:47.333Z" },
- { url = "https://files.pythonhosted.org/packages/f0/1c/6c86f6d85ffe4d0ce04228d976f00674f1df5dc893bf2dd4f1928748f187/kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", size = 1378053, upload-time = "2024-12-24T18:29:49.636Z" },
- { url = "https://files.pythonhosted.org/packages/4e/b9/1c6e9f6dcb103ac5cf87cb695845f5fa71379021500153566d8a8a9fc291/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", size = 1472278, upload-time = "2024-12-24T18:29:51.164Z" },
- { url = "https://files.pythonhosted.org/packages/ee/81/aca1eb176de671f8bda479b11acdc42c132b61a2ac861c883907dde6debb/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", size = 1478139, upload-time = "2024-12-24T18:29:52.594Z" },
- { url = "https://files.pythonhosted.org/packages/49/f4/e081522473671c97b2687d380e9e4c26f748a86363ce5af48b4a28e48d06/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", size = 1413517, upload-time = "2024-12-24T18:29:53.941Z" },
- { url = "https://files.pythonhosted.org/packages/8f/e9/6a7d025d8da8c4931522922cd706105aa32b3291d1add8c5427cdcd66e63/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", size = 1474952, upload-time = "2024-12-24T18:29:56.523Z" },
- { url = "https://files.pythonhosted.org/packages/82/13/13fa685ae167bee5d94b415991c4fc7bb0a1b6ebea6e753a87044b209678/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794", size = 2269132, upload-time = "2024-12-24T18:29:57.989Z" },
- { url = "https://files.pythonhosted.org/packages/ef/92/bb7c9395489b99a6cb41d502d3686bac692586db2045adc19e45ee64ed23/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", size = 2425997, upload-time = "2024-12-24T18:29:59.393Z" },
- { url = "https://files.pythonhosted.org/packages/ed/12/87f0e9271e2b63d35d0d8524954145837dd1a6c15b62a2d8c1ebe0f182b4/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", size = 2376060, upload-time = "2024-12-24T18:30:01.338Z" },
- { url = "https://files.pythonhosted.org/packages/02/6e/c8af39288edbce8bf0fa35dee427b082758a4b71e9c91ef18fa667782138/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", size = 2520471, upload-time = "2024-12-24T18:30:04.574Z" },
- { url = "https://files.pythonhosted.org/packages/13/78/df381bc7b26e535c91469f77f16adcd073beb3e2dd25042efd064af82323/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", size = 2338793, upload-time = "2024-12-24T18:30:06.25Z" },
- { url = "https://files.pythonhosted.org/packages/d0/dc/c1abe38c37c071d0fc71c9a474fd0b9ede05d42f5a458d584619cfd2371a/kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", size = 71855, upload-time = "2024-12-24T18:30:07.535Z" },
- { url = "https://files.pythonhosted.org/packages/a0/b6/21529d595b126ac298fdd90b705d87d4c5693de60023e0efcb4f387ed99e/kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", size = 65430, upload-time = "2024-12-24T18:30:08.504Z" },
- { url = "https://files.pythonhosted.org/packages/34/bd/b89380b7298e3af9b39f49334e3e2a4af0e04819789f04b43d560516c0c8/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", size = 126294, upload-time = "2024-12-24T18:30:09.508Z" },
- { url = "https://files.pythonhosted.org/packages/83/41/5857dc72e5e4148eaac5aa76e0703e594e4465f8ab7ec0fc60e3a9bb8fea/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", size = 67736, upload-time = "2024-12-24T18:30:11.039Z" },
- { url = "https://files.pythonhosted.org/packages/e1/d1/be059b8db56ac270489fb0b3297fd1e53d195ba76e9bbb30e5401fa6b759/kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", size = 66194, upload-time = "2024-12-24T18:30:14.886Z" },
- { url = "https://files.pythonhosted.org/packages/e1/83/4b73975f149819eb7dcf9299ed467eba068ecb16439a98990dcb12e63fdd/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", size = 1465942, upload-time = "2024-12-24T18:30:18.927Z" },
- { url = "https://files.pythonhosted.org/packages/c7/2c/30a5cdde5102958e602c07466bce058b9d7cb48734aa7a4327261ac8e002/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", size = 1595341, upload-time = "2024-12-24T18:30:22.102Z" },
- { url = "https://files.pythonhosted.org/packages/ff/9b/1e71db1c000385aa069704f5990574b8244cce854ecd83119c19e83c9586/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", size = 1598455, upload-time = "2024-12-24T18:30:24.947Z" },
- { url = "https://files.pythonhosted.org/packages/85/92/c8fec52ddf06231b31cbb779af77e99b8253cd96bd135250b9498144c78b/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", size = 1522138, upload-time = "2024-12-24T18:30:26.286Z" },
- { url = "https://files.pythonhosted.org/packages/0b/51/9eb7e2cd07a15d8bdd976f6190c0164f92ce1904e5c0c79198c4972926b7/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", size = 1582857, upload-time = "2024-12-24T18:30:28.86Z" },
- { url = "https://files.pythonhosted.org/packages/0f/95/c5a00387a5405e68ba32cc64af65ce881a39b98d73cc394b24143bebc5b8/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", size = 2293129, upload-time = "2024-12-24T18:30:30.34Z" },
- { url = "https://files.pythonhosted.org/packages/44/83/eeb7af7d706b8347548313fa3a3a15931f404533cc54fe01f39e830dd231/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", size = 2421538, upload-time = "2024-12-24T18:30:33.334Z" },
- { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661, upload-time = "2024-12-24T18:30:34.939Z" },
- { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710, upload-time = "2024-12-24T18:30:37.281Z" },
- { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213, upload-time = "2024-12-24T18:30:40.019Z" },
- { url = "https://files.pythonhosted.org/packages/1f/f9/ae81c47a43e33b93b0a9819cac6723257f5da2a5a60daf46aa5c7226ea85/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a", size = 60403, upload-time = "2024-12-24T18:30:41.372Z" },
- { url = "https://files.pythonhosted.org/packages/58/ca/f92b5cb6f4ce0c1ebfcfe3e2e42b96917e16f7090e45b21102941924f18f/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8", size = 58657, upload-time = "2024-12-24T18:30:42.392Z" },
- { url = "https://files.pythonhosted.org/packages/80/28/ae0240f732f0484d3a4dc885d055653c47144bdf59b670aae0ec3c65a7c8/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0", size = 84948, upload-time = "2024-12-24T18:30:44.703Z" },
- { url = "https://files.pythonhosted.org/packages/5d/eb/78d50346c51db22c7203c1611f9b513075f35c4e0e4877c5dde378d66043/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c", size = 81186, upload-time = "2024-12-24T18:30:45.654Z" },
- { url = "https://files.pythonhosted.org/packages/43/f8/7259f18c77adca88d5f64f9a522792e178b2691f3748817a8750c2d216ef/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b", size = 80279, upload-time = "2024-12-24T18:30:47.951Z" },
- { url = "https://files.pythonhosted.org/packages/3a/1d/50ad811d1c5dae091e4cf046beba925bcae0a610e79ae4c538f996f63ed5/kiwisolver-1.4.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b", size = 71762, upload-time = "2024-12-24T18:30:48.903Z" },
-]
-
-[[package]]
-name = "markdown"
-version = "3.8.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" },
-]
-
-[[package]]
-name = "markdown-it-py"
-version = "3.0.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "mdurl" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
-]
-
-[[package]]
-name = "markupsafe"
-version = "3.0.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" },
- { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" },
- { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" },
- { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" },
- { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" },
- { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" },
- { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" },
- { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" },
- { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" },
- { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" },
- { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" },
- { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" },
- { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" },
- { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" },
- { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" },
- { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" },
- { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" },
- { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" },
- { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" },
- { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" },
- { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" },
- { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" },
- { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" },
- { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" },
- { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" },
- { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" },
- { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" },
- { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" },
- { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" },
- { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" },
- { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" },
- { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" },
- { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" },
- { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" },
- { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" },
- { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" },
- { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" },
- { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" },
- { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" },
- { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" },
- { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" },
- { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" },
- { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" },
- { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" },
- { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" },
- { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" },
- { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" },
- { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" },
- { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
- { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
-]
-
-[[package]]
-name = "matplotlib-inline"
-version = "0.1.7"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "traitlets" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159, upload-time = "2024-04-15T13:44:44.803Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899, upload-time = "2024-04-15T13:44:43.265Z" },
-]
-
-[[package]]
-name = "mdurl"
-version = "0.1.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
-]
-
-[[package]]
-name = "mergedeep"
-version = "1.3.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" },
-]
-
-[[package]]
-name = "mike"
-version = "2.1.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "importlib-metadata" },
- { name = "importlib-resources" },
- { name = "jinja2" },
- { name = "mkdocs" },
- { name = "pyparsing" },
- { name = "pyyaml" },
- { name = "pyyaml-env-tag" },
- { name = "verspec" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/ab/f7/2933f1a1fb0e0f077d5d6a92c6c7f8a54e6128241f116dff4df8b6050bbf/mike-2.1.3.tar.gz", hash = "sha256:abd79b8ea483fb0275b7972825d3082e5ae67a41820f8d8a0dc7a3f49944e810", size = 38119, upload-time = "2024-08-13T05:02:14.167Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/fd/1a/31b7cd6e4e7a02df4e076162e9783620777592bea9e4bb036389389af99d/mike-2.1.3-py3-none-any.whl", hash = "sha256:d90c64077e84f06272437b464735130d380703a76a5738b152932884c60c062a", size = 33754, upload-time = "2024-08-13T05:02:12.515Z" },
-]
-
-[[package]]
-name = "mkdocs"
-version = "1.6.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "click" },
- { name = "colorama", marker = "sys_platform == 'win32'" },
- { name = "ghp-import" },
- { name = "jinja2" },
- { name = "markdown" },
- { name = "markupsafe" },
- { name = "mergedeep" },
- { name = "mkdocs-get-deps" },
- { name = "packaging" },
- { name = "pathspec" },
- { name = "pyyaml" },
- { name = "pyyaml-env-tag" },
- { name = "watchdog" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" },
-]
-
-[[package]]
-name = "mkdocs-api-autonav"
-version = "0.3.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "mkdocs" },
- { name = "mkdocstrings-python" },
- { name = "pyyaml" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/41/39/4f24167d977a70eb72afcea7632fd0ecca3dd0e63081d8060c0ea050aeef/mkdocs_api_autonav-0.3.0.tar.gz", hash = "sha256:1c0f10c69db38bd35d9c343814c50c033224b790e68b45876ca7e3cdfd25005c", size = 74239, upload-time = "2025-06-13T14:58:38.015Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/18/5c/19f8b99d248f3fc99283374d4eadbc1863439b0a6f31eb396a650a9ad315/mkdocs_api_autonav-0.3.0-py3-none-any.whl", hash = "sha256:3e5fce7a43e1a131b31e23b2391cde8b189a0a0aa772b74782c7141c3617e618", size = 12169, upload-time = "2025-06-13T14:58:36.972Z" },
-]
-
-[[package]]
-name = "mkdocs-autorefs"
-version = "1.4.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "markdown" },
- { name = "markupsafe" },
- { name = "mkdocs" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/47/0c/c9826f35b99c67fa3a7cddfa094c1a6c43fafde558c309c6e4403e5b37dc/mkdocs_autorefs-1.4.2.tar.gz", hash = "sha256:e2ebe1abd2b67d597ed19378c0fff84d73d1dbce411fce7a7cc6f161888b6749", size = 54961, upload-time = "2025-05-20T13:09:09.886Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/87/dc/fc063b78f4b769d1956319351704e23ebeba1e9e1d6a41b4b602325fd7e4/mkdocs_autorefs-1.4.2-py3-none-any.whl", hash = "sha256:83d6d777b66ec3c372a1aad4ae0cf77c243ba5bcda5bf0c6b8a2c5e7a3d89f13", size = 24969, upload-time = "2025-05-20T13:09:08.237Z" },
-]
-
-[[package]]
-name = "mkdocs-get-deps"
-version = "0.2.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "mergedeep" },
- { name = "platformdirs" },
- { name = "pyyaml" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" },
-]
-
-[[package]]
-name = "mkdocs-material"
-version = "9.6.15"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "babel" },
- { name = "backrefs" },
- { name = "colorama" },
- { name = "jinja2" },
- { name = "markdown" },
- { name = "mkdocs" },
- { name = "mkdocs-material-extensions" },
- { name = "paginate" },
- { name = "pygments" },
- { name = "pymdown-extensions" },
- { name = "requests" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/95/c1/f804ba2db2ddc2183e900befe7dad64339a34fa935034e1ab405289d0a97/mkdocs_material-9.6.15.tar.gz", hash = "sha256:64adf8fa8dba1a17905b6aee1894a5aafd966d4aeb44a11088519b0f5ca4f1b5", size = 3951836, upload-time = "2025-07-01T10:14:15.671Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/1d/30/dda19f0495a9096b64b6b3c07c4bfcff1c76ee0fc521086d53593f18b4c0/mkdocs_material-9.6.15-py3-none-any.whl", hash = "sha256:ac969c94d4fe5eb7c924b6d2f43d7db41159ea91553d18a9afc4780c34f2717a", size = 8716840, upload-time = "2025-07-01T10:14:13.18Z" },
-]
-
-[[package]]
-name = "mkdocs-material-extensions"
-version = "1.3.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" },
-]
-
-[[package]]
-name = "mkdocs-minify-plugin"
-version = "0.8.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "csscompressor" },
- { name = "htmlmin2" },
- { name = "jsmin" },
- { name = "mkdocs" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/52/67/fe4b77e7a8ae7628392e28b14122588beaf6078b53eb91c7ed000fd158ac/mkdocs-minify-plugin-0.8.0.tar.gz", hash = "sha256:bc11b78b8120d79e817308e2b11539d790d21445eb63df831e393f76e52e753d", size = 8366, upload-time = "2024-01-29T16:11:32.982Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/1b/cd/2e8d0d92421916e2ea4ff97f10a544a9bd5588eb747556701c983581df13/mkdocs_minify_plugin-0.8.0-py3-none-any.whl", hash = "sha256:5fba1a3f7bd9a2142c9954a6559a57e946587b21f133165ece30ea145c66aee6", size = 6723, upload-time = "2024-01-29T16:11:31.851Z" },
-]
-
-[[package]]
-name = "mkdocs-spellcheck"
-version = "1.1.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "mkdocs" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/77/b4/d3b4b3dfcec8c545effa8841cdb3045a9ec0c34996c18bd00537881c2a4d/mkdocs_spellcheck-1.1.1.tar.gz", hash = "sha256:07cec07c9469f71de44b2e10999d0b3cc6f1803a454b78ca27c3f10b741fe8bd", size = 33369, upload-time = "2025-03-26T13:46:29.53Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c8/52/fff58845feed7d8fde450882429427e2d3e2d523552da9d5cb6781e179cc/mkdocs_spellcheck-1.1.1-py3-none-any.whl", hash = "sha256:cb616205ca6a027c3b31d7092b1f3ea01684f6093a705ff1c08191ebb8cd9937", size = 13276, upload-time = "2025-03-26T13:46:28.125Z" },
-]
-
-[package.optional-dependencies]
-all = [
- { name = "codespell" },
- { name = "symspellpy" },
-]
-
-[[package]]
-name = "mkdocstrings"
-version = "0.29.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "jinja2" },
- { name = "markdown" },
- { name = "markupsafe" },
- { name = "mkdocs" },
- { name = "mkdocs-autorefs" },
- { name = "pymdown-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/41/e8/d22922664a627a0d3d7ff4a6ca95800f5dde54f411982591b4621a76225d/mkdocstrings-0.29.1.tar.gz", hash = "sha256:8722f8f8c5cd75da56671e0a0c1bbed1df9946c0cef74794d6141b34011abd42", size = 1212686, upload-time = "2025-03-31T08:33:11.997Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/98/14/22533a578bf8b187e05d67e2c1721ce10e3f526610eebaf7a149d557ea7a/mkdocstrings-0.29.1-py3-none-any.whl", hash = "sha256:37a9736134934eea89cbd055a513d40a020d87dfcae9e3052c2a6b8cd4af09b6", size = 1631075, upload-time = "2025-03-31T08:33:09.661Z" },
-]
-
-[[package]]
-name = "mkdocstrings-python"
-version = "1.16.12"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "griffe" },
- { name = "mkdocs-autorefs" },
- { name = "mkdocstrings" },
- { name = "typing-extensions", marker = "python_full_version < '3.11'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/bf/ed/b886f8c714fd7cccc39b79646b627dbea84cd95c46be43459ef46852caf0/mkdocstrings_python-1.16.12.tar.gz", hash = "sha256:9b9eaa066e0024342d433e332a41095c4e429937024945fea511afe58f63175d", size = 206065, upload-time = "2025-06-03T12:52:49.276Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3b/dd/a24ee3de56954bfafb6ede7cd63c2413bb842cc48eb45e41c43a05a33074/mkdocstrings_python-1.16.12-py3-none-any.whl", hash = "sha256:22ded3a63b3d823d57457a70ff9860d5a4de9e8b1e482876fc9baabaf6f5f374", size = 124287, upload-time = "2025-06-03T12:52:47.819Z" },
-]
-
-[[package]]
-name = "munch"
-version = "4.0.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e7/2b/45098135b5f9f13221820d90f9e0516e11a2a0f55012c13b081d202b782a/munch-4.0.0.tar.gz", hash = "sha256:542cb151461263216a4e37c3fd9afc425feeaf38aaa3025cd2a981fadb422235", size = 19089, upload-time = "2023-07-01T09:49:35.98Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/56/b3/7c69b37f03260a061883bec0e7b05be7117c1b1c85f5212c72c8c2bc3c8c/munch-4.0.0-py2.py3-none-any.whl", hash = "sha256:71033c45db9fb677a0b7eb517a4ce70ae09258490e419b0e7f00d1e386ecb1b4", size = 9950, upload-time = "2023-07-01T09:49:34.472Z" },
-]
-
-[[package]]
-name = "mypy"
-version = "1.16.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "mypy-extensions" },
- { name = "pathspec" },
- { name = "tomli", marker = "python_full_version < '3.11'" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/81/69/92c7fa98112e4d9eb075a239caa4ef4649ad7d441545ccffbd5e34607cbb/mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab", size = 3324747, upload-time = "2025-06-16T16:51:35.145Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/8e/12/2bf23a80fcef5edb75de9a1e295d778e0f46ea89eb8b115818b663eff42b/mypy-1.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4f0fed1022a63c6fec38f28b7fc77fca47fd490445c69d0a66266c59dd0b88a", size = 10958644, upload-time = "2025-06-16T16:51:11.649Z" },
- { url = "https://files.pythonhosted.org/packages/08/50/bfe47b3b278eacf348291742fd5e6613bbc4b3434b72ce9361896417cfe5/mypy-1.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86042bbf9f5a05ea000d3203cf87aa9d0ccf9a01f73f71c58979eb9249f46d72", size = 10087033, upload-time = "2025-06-16T16:35:30.089Z" },
- { url = "https://files.pythonhosted.org/packages/21/de/40307c12fe25675a0776aaa2cdd2879cf30d99eec91b898de00228dc3ab5/mypy-1.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea7469ee5902c95542bea7ee545f7006508c65c8c54b06dc2c92676ce526f3ea", size = 11875645, upload-time = "2025-06-16T16:35:48.49Z" },
- { url = "https://files.pythonhosted.org/packages/a6/d8/85bdb59e4a98b7a31495bd8f1a4445d8ffc86cde4ab1f8c11d247c11aedc/mypy-1.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:352025753ef6a83cb9e7f2427319bb7875d1fdda8439d1e23de12ab164179574", size = 12616986, upload-time = "2025-06-16T16:48:39.526Z" },
- { url = "https://files.pythonhosted.org/packages/0e/d0/bb25731158fa8f8ee9e068d3e94fcceb4971fedf1424248496292512afe9/mypy-1.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff9fa5b16e4c1364eb89a4d16bcda9987f05d39604e1e6c35378a2987c1aac2d", size = 12878632, upload-time = "2025-06-16T16:36:08.195Z" },
- { url = "https://files.pythonhosted.org/packages/2d/11/822a9beb7a2b825c0cb06132ca0a5183f8327a5e23ef89717c9474ba0bc6/mypy-1.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:1256688e284632382f8f3b9e2123df7d279f603c561f099758e66dd6ed4e8bd6", size = 9484391, upload-time = "2025-06-16T16:37:56.151Z" },
- { url = "https://files.pythonhosted.org/packages/9a/61/ec1245aa1c325cb7a6c0f8570a2eee3bfc40fa90d19b1267f8e50b5c8645/mypy-1.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:472e4e4c100062488ec643f6162dd0d5208e33e2f34544e1fc931372e806c0cc", size = 10890557, upload-time = "2025-06-16T16:37:21.421Z" },
- { url = "https://files.pythonhosted.org/packages/6b/bb/6eccc0ba0aa0c7a87df24e73f0ad34170514abd8162eb0c75fd7128171fb/mypy-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea16e2a7d2714277e349e24d19a782a663a34ed60864006e8585db08f8ad1782", size = 10012921, upload-time = "2025-06-16T16:51:28.659Z" },
- { url = "https://files.pythonhosted.org/packages/5f/80/b337a12e2006715f99f529e732c5f6a8c143bb58c92bb142d5ab380963a5/mypy-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08e850ea22adc4d8a4014651575567b0318ede51e8e9fe7a68f25391af699507", size = 11802887, upload-time = "2025-06-16T16:50:53.627Z" },
- { url = "https://files.pythonhosted.org/packages/d9/59/f7af072d09793d581a745a25737c7c0a945760036b16aeb620f658a017af/mypy-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22d76a63a42619bfb90122889b903519149879ddbf2ba4251834727944c8baca", size = 12531658, upload-time = "2025-06-16T16:33:55.002Z" },
- { url = "https://files.pythonhosted.org/packages/82/c4/607672f2d6c0254b94a646cfc45ad589dd71b04aa1f3d642b840f7cce06c/mypy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c7ce0662b6b9dc8f4ed86eb7a5d505ee3298c04b40ec13b30e572c0e5ae17c4", size = 12732486, upload-time = "2025-06-16T16:37:03.301Z" },
- { url = "https://files.pythonhosted.org/packages/b6/5e/136555ec1d80df877a707cebf9081bd3a9f397dedc1ab9750518d87489ec/mypy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:211287e98e05352a2e1d4e8759c5490925a7c784ddc84207f4714822f8cf99b6", size = 9479482, upload-time = "2025-06-16T16:47:37.48Z" },
- { url = "https://files.pythonhosted.org/packages/b4/d6/39482e5fcc724c15bf6280ff5806548c7185e0c090712a3736ed4d07e8b7/mypy-1.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:af4792433f09575d9eeca5c63d7d90ca4aeceda9d8355e136f80f8967639183d", size = 11066493, upload-time = "2025-06-16T16:47:01.683Z" },
- { url = "https://files.pythonhosted.org/packages/e6/e5/26c347890efc6b757f4d5bb83f4a0cf5958b8cf49c938ac99b8b72b420a6/mypy-1.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66df38405fd8466ce3517eda1f6640611a0b8e70895e2a9462d1d4323c5eb4b9", size = 10081687, upload-time = "2025-06-16T16:48:19.367Z" },
- { url = "https://files.pythonhosted.org/packages/44/c7/b5cb264c97b86914487d6a24bd8688c0172e37ec0f43e93b9691cae9468b/mypy-1.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44e7acddb3c48bd2713994d098729494117803616e116032af192871aed80b79", size = 11839723, upload-time = "2025-06-16T16:49:20.912Z" },
- { url = "https://files.pythonhosted.org/packages/15/f8/491997a9b8a554204f834ed4816bda813aefda31cf873bb099deee3c9a99/mypy-1.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab5eca37b50188163fa7c1b73c685ac66c4e9bdee4a85c9adac0e91d8895e15", size = 12722980, upload-time = "2025-06-16T16:37:40.929Z" },
- { url = "https://files.pythonhosted.org/packages/df/f0/2bd41e174b5fd93bc9de9a28e4fb673113633b8a7f3a607fa4a73595e468/mypy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb6229b2c9086247e21a83c309754b9058b438704ad2f6807f0d8227f6ebdd", size = 12903328, upload-time = "2025-06-16T16:34:35.099Z" },
- { url = "https://files.pythonhosted.org/packages/61/81/5572108a7bec2c46b8aff7e9b524f371fe6ab5efb534d38d6b37b5490da8/mypy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:1f0435cf920e287ff68af3d10a118a73f212deb2ce087619eb4e648116d1fe9b", size = 9562321, upload-time = "2025-06-16T16:48:58.823Z" },
- { url = "https://files.pythonhosted.org/packages/28/e3/96964af4a75a949e67df4b95318fe2b7427ac8189bbc3ef28f92a1c5bc56/mypy-1.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ddc91eb318c8751c69ddb200a5937f1232ee8efb4e64e9f4bc475a33719de438", size = 11063480, upload-time = "2025-06-16T16:47:56.205Z" },
- { url = "https://files.pythonhosted.org/packages/f5/4d/cd1a42b8e5be278fab7010fb289d9307a63e07153f0ae1510a3d7b703193/mypy-1.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:87ff2c13d58bdc4bbe7dc0dedfe622c0f04e2cb2a492269f3b418df2de05c536", size = 10090538, upload-time = "2025-06-16T16:46:43.92Z" },
- { url = "https://files.pythonhosted.org/packages/c9/4f/c3c6b4b66374b5f68bab07c8cabd63a049ff69796b844bc759a0ca99bb2a/mypy-1.16.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a7cfb0fe29fe5a9841b7c8ee6dffb52382c45acdf68f032145b75620acfbd6f", size = 11836839, upload-time = "2025-06-16T16:36:28.039Z" },
- { url = "https://files.pythonhosted.org/packages/b4/7e/81ca3b074021ad9775e5cb97ebe0089c0f13684b066a750b7dc208438403/mypy-1.16.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:051e1677689c9d9578b9c7f4d206d763f9bbd95723cd1416fad50db49d52f359", size = 12715634, upload-time = "2025-06-16T16:50:34.441Z" },
- { url = "https://files.pythonhosted.org/packages/e9/95/bdd40c8be346fa4c70edb4081d727a54d0a05382d84966869738cfa8a497/mypy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d5d2309511cc56c021b4b4e462907c2b12f669b2dbeb68300110ec27723971be", size = 12895584, upload-time = "2025-06-16T16:34:54.857Z" },
- { url = "https://files.pythonhosted.org/packages/5a/fd/d486a0827a1c597b3b48b1bdef47228a6e9ee8102ab8c28f944cb83b65dc/mypy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:4f58ac32771341e38a853c5d0ec0dfe27e18e27da9cdb8bbc882d2249c71a3ee", size = 9573886, upload-time = "2025-06-16T16:36:43.589Z" },
- { url = "https://files.pythonhosted.org/packages/cf/d3/53e684e78e07c1a2bf7105715e5edd09ce951fc3f47cf9ed095ec1b7a037/mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37", size = 2265923, upload-time = "2025-06-16T16:48:02.366Z" },
-]
-
-[[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 = "nodeenv"
-version = "1.9.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
-]
-
-[[package]]
-name = "numpy"
-version = "2.2.6"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version < '3.11'",
-]
-sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" },
- { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" },
- { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" },
- { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" },
- { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" },
- { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" },
- { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" },
- { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" },
- { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" },
- { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" },
- { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" },
- { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" },
- { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" },
- { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" },
- { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" },
- { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" },
- { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" },
- { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" },
- { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" },
- { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" },
- { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" },
- { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" },
- { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" },
- { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" },
- { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" },
- { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" },
- { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" },
- { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" },
- { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" },
- { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" },
- { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" },
- { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" },
- { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" },
- { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" },
- { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" },
- { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" },
- { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" },
- { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" },
- { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" },
- { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" },
- { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" },
- { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" },
- { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" },
- { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" },
- { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" },
- { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" },
- { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" },
- { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" },
- { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" },
- { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" },
- { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" },
- { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" },
- { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" },
- { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" },
-]
-
-[[package]]
-name = "numpy"
-version = "2.3.1"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.11'",
-]
-sdist = { url = "https://files.pythonhosted.org/packages/2e/19/d7c972dfe90a353dbd3efbbe1d14a5951de80c99c9dc1b93cd998d51dc0f/numpy-2.3.1.tar.gz", hash = "sha256:1ec9ae20a4226da374362cca3c62cd753faf2f951440b0e3b98e93c235441d2b", size = 20390372, upload-time = "2025-06-21T12:28:33.469Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b0/c7/87c64d7ab426156530676000c94784ef55676df2f13b2796f97722464124/numpy-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ea9e48336a402551f52cd8f593343699003d2353daa4b72ce8d34f66b722070", size = 21199346, upload-time = "2025-06-21T11:47:47.57Z" },
- { url = "https://files.pythonhosted.org/packages/58/0e/0966c2f44beeac12af8d836e5b5f826a407cf34c45cb73ddcdfce9f5960b/numpy-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ccb7336eaf0e77c1635b232c141846493a588ec9ea777a7c24d7166bb8533ae", size = 14361143, upload-time = "2025-06-21T11:48:10.766Z" },
- { url = "https://files.pythonhosted.org/packages/7d/31/6e35a247acb1bfc19226791dfc7d4c30002cd4e620e11e58b0ddf836fe52/numpy-2.3.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0bb3a4a61e1d327e035275d2a993c96fa786e4913aa089843e6a2d9dd205c66a", size = 5378989, upload-time = "2025-06-21T11:48:19.998Z" },
- { url = "https://files.pythonhosted.org/packages/b0/25/93b621219bb6f5a2d4e713a824522c69ab1f06a57cd571cda70e2e31af44/numpy-2.3.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:e344eb79dab01f1e838ebb67aab09965fb271d6da6b00adda26328ac27d4a66e", size = 6912890, upload-time = "2025-06-21T11:48:31.376Z" },
- { url = "https://files.pythonhosted.org/packages/ef/60/6b06ed98d11fb32e27fb59468b42383f3877146d3ee639f733776b6ac596/numpy-2.3.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:467db865b392168ceb1ef1ffa6f5a86e62468c43e0cfb4ab6da667ede10e58db", size = 14569032, upload-time = "2025-06-21T11:48:52.563Z" },
- { url = "https://files.pythonhosted.org/packages/75/c9/9bec03675192077467a9c7c2bdd1f2e922bd01d3a69b15c3a0fdcd8548f6/numpy-2.3.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:afed2ce4a84f6b0fc6c1ce734ff368cbf5a5e24e8954a338f3bdffa0718adffb", size = 16930354, upload-time = "2025-06-21T11:49:17.473Z" },
- { url = "https://files.pythonhosted.org/packages/6a/e2/5756a00cabcf50a3f527a0c968b2b4881c62b1379223931853114fa04cda/numpy-2.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0025048b3c1557a20bc80d06fdeb8cc7fc193721484cca82b2cfa072fec71a93", size = 15879605, upload-time = "2025-06-21T11:49:41.161Z" },
- { url = "https://files.pythonhosted.org/packages/ff/86/a471f65f0a86f1ca62dcc90b9fa46174dd48f50214e5446bc16a775646c5/numpy-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5ee121b60aa509679b682819c602579e1df14a5b07fe95671c8849aad8f2115", size = 18666994, upload-time = "2025-06-21T11:50:08.516Z" },
- { url = "https://files.pythonhosted.org/packages/43/a6/482a53e469b32be6500aaf61cfafd1de7a0b0d484babf679209c3298852e/numpy-2.3.1-cp311-cp311-win32.whl", hash = "sha256:a8b740f5579ae4585831b3cf0e3b0425c667274f82a484866d2adf9570539369", size = 6603672, upload-time = "2025-06-21T11:50:19.584Z" },
- { url = "https://files.pythonhosted.org/packages/6b/fb/bb613f4122c310a13ec67585c70e14b03bfc7ebabd24f4d5138b97371d7c/numpy-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:d4580adadc53311b163444f877e0789f1c8861e2698f6b2a4ca852fda154f3ff", size = 13024015, upload-time = "2025-06-21T11:50:39.139Z" },
- { url = "https://files.pythonhosted.org/packages/51/58/2d842825af9a0c041aca246dc92eb725e1bc5e1c9ac89712625db0c4e11c/numpy-2.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:ec0bdafa906f95adc9a0c6f26a4871fa753f25caaa0e032578a30457bff0af6a", size = 10456989, upload-time = "2025-06-21T11:50:55.616Z" },
- { url = "https://files.pythonhosted.org/packages/c6/56/71ad5022e2f63cfe0ca93559403d0edef14aea70a841d640bd13cdba578e/numpy-2.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2959d8f268f3d8ee402b04a9ec4bb7604555aeacf78b360dc4ec27f1d508177d", size = 20896664, upload-time = "2025-06-21T12:15:30.845Z" },
- { url = "https://files.pythonhosted.org/packages/25/65/2db52ba049813670f7f987cc5db6dac9be7cd95e923cc6832b3d32d87cef/numpy-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:762e0c0c6b56bdedfef9a8e1d4538556438288c4276901ea008ae44091954e29", size = 14131078, upload-time = "2025-06-21T12:15:52.23Z" },
- { url = "https://files.pythonhosted.org/packages/57/dd/28fa3c17b0e751047ac928c1e1b6990238faad76e9b147e585b573d9d1bd/numpy-2.3.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:867ef172a0976aaa1f1d1b63cf2090de8b636a7674607d514505fb7276ab08fc", size = 5112554, upload-time = "2025-06-21T12:16:01.434Z" },
- { url = "https://files.pythonhosted.org/packages/c9/fc/84ea0cba8e760c4644b708b6819d91784c290288c27aca916115e3311d17/numpy-2.3.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:4e602e1b8682c2b833af89ba641ad4176053aaa50f5cacda1a27004352dde943", size = 6646560, upload-time = "2025-06-21T12:16:11.895Z" },
- { url = "https://files.pythonhosted.org/packages/61/b2/512b0c2ddec985ad1e496b0bd853eeb572315c0f07cd6997473ced8f15e2/numpy-2.3.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8e333040d069eba1652fb08962ec5b76af7f2c7bce1df7e1418c8055cf776f25", size = 14260638, upload-time = "2025-06-21T12:16:32.611Z" },
- { url = "https://files.pythonhosted.org/packages/6e/45/c51cb248e679a6c6ab14b7a8e3ead3f4a3fe7425fc7a6f98b3f147bec532/numpy-2.3.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:e7cbf5a5eafd8d230a3ce356d892512185230e4781a361229bd902ff403bc660", size = 16632729, upload-time = "2025-06-21T12:16:57.439Z" },
- { url = "https://files.pythonhosted.org/packages/e4/ff/feb4be2e5c09a3da161b412019caf47183099cbea1132fd98061808c2df2/numpy-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f1b8f26d1086835f442286c1d9b64bb3974b0b1e41bb105358fd07d20872952", size = 15565330, upload-time = "2025-06-21T12:17:20.638Z" },
- { url = "https://files.pythonhosted.org/packages/bc/6d/ceafe87587101e9ab0d370e4f6e5f3f3a85b9a697f2318738e5e7e176ce3/numpy-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ee8340cb48c9b7a5899d1149eece41ca535513a9698098edbade2a8e7a84da77", size = 18361734, upload-time = "2025-06-21T12:17:47.938Z" },
- { url = "https://files.pythonhosted.org/packages/2b/19/0fb49a3ea088be691f040c9bf1817e4669a339d6e98579f91859b902c636/numpy-2.3.1-cp312-cp312-win32.whl", hash = "sha256:e772dda20a6002ef7061713dc1e2585bc1b534e7909b2030b5a46dae8ff077ab", size = 6320411, upload-time = "2025-06-21T12:17:58.475Z" },
- { url = "https://files.pythonhosted.org/packages/b1/3e/e28f4c1dd9e042eb57a3eb652f200225e311b608632bc727ae378623d4f8/numpy-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cfecc7822543abdea6de08758091da655ea2210b8ffa1faf116b940693d3df76", size = 12734973, upload-time = "2025-06-21T12:18:17.601Z" },
- { url = "https://files.pythonhosted.org/packages/04/a8/8a5e9079dc722acf53522b8f8842e79541ea81835e9b5483388701421073/numpy-2.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:7be91b2239af2658653c5bb6f1b8bccafaf08226a258caf78ce44710a0160d30", size = 10191491, upload-time = "2025-06-21T12:18:33.585Z" },
- { url = "https://files.pythonhosted.org/packages/d4/bd/35ad97006d8abff8631293f8ea6adf07b0108ce6fec68da3c3fcca1197f2/numpy-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25a1992b0a3fdcdaec9f552ef10d8103186f5397ab45e2d25f8ac51b1a6b97e8", size = 20889381, upload-time = "2025-06-21T12:19:04.103Z" },
- { url = "https://files.pythonhosted.org/packages/f1/4f/df5923874d8095b6062495b39729178eef4a922119cee32a12ee1bd4664c/numpy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7dea630156d39b02a63c18f508f85010230409db5b2927ba59c8ba4ab3e8272e", size = 14152726, upload-time = "2025-06-21T12:19:25.599Z" },
- { url = "https://files.pythonhosted.org/packages/8c/0f/a1f269b125806212a876f7efb049b06c6f8772cf0121139f97774cd95626/numpy-2.3.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bada6058dd886061f10ea15f230ccf7dfff40572e99fef440a4a857c8728c9c0", size = 5105145, upload-time = "2025-06-21T12:19:34.782Z" },
- { url = "https://files.pythonhosted.org/packages/6d/63/a7f7fd5f375b0361682f6ffbf686787e82b7bbd561268e4f30afad2bb3c0/numpy-2.3.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:a894f3816eb17b29e4783e5873f92faf55b710c2519e5c351767c51f79d8526d", size = 6639409, upload-time = "2025-06-21T12:19:45.228Z" },
- { url = "https://files.pythonhosted.org/packages/bf/0d/1854a4121af895aab383f4aa233748f1df4671ef331d898e32426756a8a6/numpy-2.3.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:18703df6c4a4fee55fd3d6e5a253d01c5d33a295409b03fda0c86b3ca2ff41a1", size = 14257630, upload-time = "2025-06-21T12:20:06.544Z" },
- { url = "https://files.pythonhosted.org/packages/50/30/af1b277b443f2fb08acf1c55ce9d68ee540043f158630d62cef012750f9f/numpy-2.3.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5902660491bd7a48b2ec16c23ccb9124b8abfd9583c5fdfa123fe6b421e03de1", size = 16627546, upload-time = "2025-06-21T12:20:31.002Z" },
- { url = "https://files.pythonhosted.org/packages/6e/ec/3b68220c277e463095342d254c61be8144c31208db18d3fd8ef02712bcd6/numpy-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:36890eb9e9d2081137bd78d29050ba63b8dab95dff7912eadf1185e80074b2a0", size = 15562538, upload-time = "2025-06-21T12:20:54.322Z" },
- { url = "https://files.pythonhosted.org/packages/77/2b/4014f2bcc4404484021c74d4c5ee8eb3de7e3f7ac75f06672f8dcf85140a/numpy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a780033466159c2270531e2b8ac063704592a0bc62ec4a1b991c7c40705eb0e8", size = 18360327, upload-time = "2025-06-21T12:21:21.053Z" },
- { url = "https://files.pythonhosted.org/packages/40/8d/2ddd6c9b30fcf920837b8672f6c65590c7d92e43084c25fc65edc22e93ca/numpy-2.3.1-cp313-cp313-win32.whl", hash = "sha256:39bff12c076812595c3a306f22bfe49919c5513aa1e0e70fac756a0be7c2a2b8", size = 6312330, upload-time = "2025-06-21T12:25:07.447Z" },
- { url = "https://files.pythonhosted.org/packages/dd/c8/beaba449925988d415efccb45bf977ff8327a02f655090627318f6398c7b/numpy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d5ee6eec45f08ce507a6570e06f2f879b374a552087a4179ea7838edbcbfa42", size = 12731565, upload-time = "2025-06-21T12:25:26.444Z" },
- { url = "https://files.pythonhosted.org/packages/0b/c3/5c0c575d7ec78c1126998071f58facfc124006635da75b090805e642c62e/numpy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c4d9e0a8368db90f93bd192bfa771ace63137c3488d198ee21dfb8e7771916e", size = 10190262, upload-time = "2025-06-21T12:25:42.196Z" },
- { url = "https://files.pythonhosted.org/packages/ea/19/a029cd335cf72f79d2644dcfc22d90f09caa86265cbbde3b5702ccef6890/numpy-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b0b5397374f32ec0649dd98c652a1798192042e715df918c20672c62fb52d4b8", size = 20987593, upload-time = "2025-06-21T12:21:51.664Z" },
- { url = "https://files.pythonhosted.org/packages/25/91/8ea8894406209107d9ce19b66314194675d31761fe2cb3c84fe2eeae2f37/numpy-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c5bdf2015ccfcee8253fb8be695516ac4457c743473a43290fd36eba6a1777eb", size = 14300523, upload-time = "2025-06-21T12:22:13.583Z" },
- { url = "https://files.pythonhosted.org/packages/a6/7f/06187b0066eefc9e7ce77d5f2ddb4e314a55220ad62dd0bfc9f2c44bac14/numpy-2.3.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d70f20df7f08b90a2062c1f07737dd340adccf2068d0f1b9b3d56e2038979fee", size = 5227993, upload-time = "2025-06-21T12:22:22.53Z" },
- { url = "https://files.pythonhosted.org/packages/e8/ec/a926c293c605fa75e9cfb09f1e4840098ed46d2edaa6e2152ee35dc01ed3/numpy-2.3.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:2fb86b7e58f9ac50e1e9dd1290154107e47d1eef23a0ae9145ded06ea606f992", size = 6736652, upload-time = "2025-06-21T12:22:33.629Z" },
- { url = "https://files.pythonhosted.org/packages/e3/62/d68e52fb6fde5586650d4c0ce0b05ff3a48ad4df4ffd1b8866479d1d671d/numpy-2.3.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:23ab05b2d241f76cb883ce8b9a93a680752fbfcbd51c50eff0b88b979e471d8c", size = 14331561, upload-time = "2025-06-21T12:22:55.056Z" },
- { url = "https://files.pythonhosted.org/packages/fc/ec/b74d3f2430960044bdad6900d9f5edc2dc0fb8bf5a0be0f65287bf2cbe27/numpy-2.3.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ce2ce9e5de4703a673e705183f64fd5da5bf36e7beddcb63a25ee2286e71ca48", size = 16693349, upload-time = "2025-06-21T12:23:20.53Z" },
- { url = "https://files.pythonhosted.org/packages/0d/15/def96774b9d7eb198ddadfcbd20281b20ebb510580419197e225f5c55c3e/numpy-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c4913079974eeb5c16ccfd2b1f09354b8fed7e0d6f2cab933104a09a6419b1ee", size = 15642053, upload-time = "2025-06-21T12:23:43.697Z" },
- { url = "https://files.pythonhosted.org/packages/2b/57/c3203974762a759540c6ae71d0ea2341c1fa41d84e4971a8e76d7141678a/numpy-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:010ce9b4f00d5c036053ca684c77441f2f2c934fd23bee058b4d6f196efd8280", size = 18434184, upload-time = "2025-06-21T12:24:10.708Z" },
- { url = "https://files.pythonhosted.org/packages/22/8a/ccdf201457ed8ac6245187850aff4ca56a79edbea4829f4e9f14d46fa9a5/numpy-2.3.1-cp313-cp313t-win32.whl", hash = "sha256:6269b9edfe32912584ec496d91b00b6d34282ca1d07eb10e82dfc780907d6c2e", size = 6440678, upload-time = "2025-06-21T12:24:21.596Z" },
- { url = "https://files.pythonhosted.org/packages/f1/7e/7f431d8bd8eb7e03d79294aed238b1b0b174b3148570d03a8a8a8f6a0da9/numpy-2.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:2a809637460e88a113e186e87f228d74ae2852a2e0c44de275263376f17b5bdc", size = 12870697, upload-time = "2025-06-21T12:24:40.644Z" },
- { url = "https://files.pythonhosted.org/packages/d4/ca/af82bf0fad4c3e573c6930ed743b5308492ff19917c7caaf2f9b6f9e2e98/numpy-2.3.1-cp313-cp313t-win_arm64.whl", hash = "sha256:eccb9a159db9aed60800187bc47a6d3451553f0e1b08b068d8b277ddfbb9b244", size = 10260376, upload-time = "2025-06-21T12:24:56.884Z" },
- { url = "https://files.pythonhosted.org/packages/e8/34/facc13b9b42ddca30498fc51f7f73c3d0f2be179943a4b4da8686e259740/numpy-2.3.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ad506d4b09e684394c42c966ec1527f6ebc25da7f4da4b1b056606ffe446b8a3", size = 21070637, upload-time = "2025-06-21T12:26:12.518Z" },
- { url = "https://files.pythonhosted.org/packages/65/b6/41b705d9dbae04649b529fc9bd3387664c3281c7cd78b404a4efe73dcc45/numpy-2.3.1-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:ebb8603d45bc86bbd5edb0d63e52c5fd9e7945d3a503b77e486bd88dde67a19b", size = 5304087, upload-time = "2025-06-21T12:26:22.294Z" },
- { url = "https://files.pythonhosted.org/packages/7a/b4/fe3ac1902bff7a4934a22d49e1c9d71a623204d654d4cc43c6e8fe337fcb/numpy-2.3.1-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:15aa4c392ac396e2ad3d0a2680c0f0dee420f9fed14eef09bdb9450ee6dcb7b7", size = 6817588, upload-time = "2025-06-21T12:26:32.939Z" },
- { url = "https://files.pythonhosted.org/packages/ae/ee/89bedf69c36ace1ac8f59e97811c1f5031e179a37e4821c3a230bf750142/numpy-2.3.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c6e0bf9d1a2f50d2b65a7cf56db37c095af17b59f6c132396f7c6d5dd76484df", size = 14399010, upload-time = "2025-06-21T12:26:54.086Z" },
- { url = "https://files.pythonhosted.org/packages/15/08/e00e7070ede29b2b176165eba18d6f9784d5349be3c0c1218338e79c27fd/numpy-2.3.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:eabd7e8740d494ce2b4ea0ff05afa1b7b291e978c0ae075487c51e8bd93c0c68", size = 16752042, upload-time = "2025-06-21T12:27:19.018Z" },
- { url = "https://files.pythonhosted.org/packages/48/6b/1c6b515a83d5564b1698a61efa245727c8feecf308f4091f565988519d20/numpy-2.3.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e610832418a2bc09d974cc9fecebfa51e9532d6190223bc5ef6a7402ebf3b5cb", size = 12927246, upload-time = "2025-06-21T12:27:38.618Z" },
-]
-
-[[package]]
-name = "packaging"
-version = "25.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
-]
-
-[[package]]
-name = "paginate"
-version = "0.5.7"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" },
-]
-
-[[package]]
-name = "parso"
-version = "0.8.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609, upload-time = "2024-04-05T09:43:55.897Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650, upload-time = "2024-04-05T09:43:53.299Z" },
-]
-
-[[package]]
-name = "pathspec"
-version = "0.12.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
-]
-
-[[package]]
-name = "pdbpp"
-version = "0.11.6"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "fancycompleter" },
- { name = "pygments" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/27/ef/e6ae1a1e1d2cbe0fbf04f22b21d7d58e8e13d24dfcc3b6a4dea18a3ed1e0/pdbpp-0.11.6.tar.gz", hash = "sha256:36a73c5bcf0c3c35034be4cf99e6106e3ee0c8f5e0faafc2cf9be5f1481eb4b7", size = 78178, upload-time = "2025-04-16T10:20:07.008Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c5/e1/77fa5c45fcd95a80b0d793b604c19b71c9fa8d5b27d403eced79f3842828/pdbpp-0.11.6-py3-none-any.whl", hash = "sha256:8e024d36bd2f35a3b19d8732524c696b8b4aef633250d28547198e746cd81ccb", size = 33334, upload-time = "2025-04-16T10:20:05.529Z" },
-]
-
-[[package]]
-name = "pexpect"
-version = "4.9.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "ptyprocess" },
-]
-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 = [
- { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" },
-]
-
-[[package]]
-name = "pillow"
-version = "11.3.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554, upload-time = "2025-07-01T09:13:39.342Z" },
- { url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548, upload-time = "2025-07-01T09:13:41.835Z" },
- { url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742, upload-time = "2025-07-03T13:09:47.439Z" },
- { url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087, upload-time = "2025-07-03T13:09:51.796Z" },
- { url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350, upload-time = "2025-07-01T09:13:43.865Z" },
- { url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840, upload-time = "2025-07-01T09:13:46.161Z" },
- { url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005, upload-time = "2025-07-01T09:13:47.829Z" },
- { url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372, upload-time = "2025-07-01T09:13:52.145Z" },
- { url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090, upload-time = "2025-07-01T09:13:53.915Z" },
- { url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988, upload-time = "2025-07-01T09:13:55.699Z" },
- { url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899, upload-time = "2025-07-01T09:13:57.497Z" },
- { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" },
- { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" },
- { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" },
- { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" },
- { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" },
- { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" },
- { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" },
- { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" },
- { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" },
- { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" },
- { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" },
- { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" },
- { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" },
- { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" },
- { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" },
- { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" },
- { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" },
- { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" },
- { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" },
- { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" },
- { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" },
- { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" },
- { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" },
- { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" },
- { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" },
- { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" },
- { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" },
- { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" },
- { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" },
- { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" },
- { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" },
- { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" },
- { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" },
- { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" },
- { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" },
- { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" },
- { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" },
- { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" },
- { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" },
- { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" },
- { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" },
- { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" },
- { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" },
- { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" },
- { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" },
- { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" },
- { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" },
- { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" },
- { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" },
- { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" },
- { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" },
- { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" },
- { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" },
- { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" },
- { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" },
- { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" },
- { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" },
- { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" },
- { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" },
- { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" },
- { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" },
- { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" },
- { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" },
- { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" },
- { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" },
- { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" },
- { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" },
- { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" },
- { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" },
- { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556, upload-time = "2025-07-01T09:16:09.961Z" },
- { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625, upload-time = "2025-07-01T09:16:11.913Z" },
- { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207, upload-time = "2025-07-03T13:11:10.201Z" },
- { url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939, upload-time = "2025-07-03T13:11:15.68Z" },
- { url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166, upload-time = "2025-07-01T09:16:13.74Z" },
- { url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482, upload-time = "2025-07-01T09:16:16.107Z" },
- { url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596, upload-time = "2025-07-01T09:16:18.07Z" },
- { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" },
- { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" },
- { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" },
- { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" },
- { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" },
- { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" },
- { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" },
-]
-
-[[package]]
-name = "platformdirs"
-version = "4.3.8"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" },
-]
-
-[[package]]
-name = "pluggy"
-version = "1.6.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
-]
-
-[[package]]
-name = "pre-commit"
-version = "4.2.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "cfgv" },
- { name = "identify" },
- { name = "nodeenv" },
- { name = "pyyaml" },
- { name = "virtualenv" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" },
-]
-
-[[package]]
-name = "pre-commit-uv"
-version = "4.1.4"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pre-commit" },
- { name = "uv" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/b1/6c/c3c1d01698c8abb0b546defc0304971fa7fb2ba84ad35587b9dad095d73f/pre_commit_uv-4.1.4.tar.gz", hash = "sha256:3db606a79b226127b27dbbd8381b78c0e30de3ac775a8492c576a68e9250535c", size = 6493, upload-time = "2024-10-29T23:07:28.918Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f1/70/1b65f9118ef64f6ffe5d57a67170bbff25d4f4a3d1cb78e8ed3392e16114/pre_commit_uv-4.1.4-py3-none-any.whl", hash = "sha256:7f01fb494fa1caa5097d20a38f71df7cea0209197b2564699cef9b3f3aa9d135", size = 5578, upload-time = "2024-10-29T23:07:27.128Z" },
-]
-
-[[package]]
-name = "prompt-toolkit"
-version = "3.0.51"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "wcwidth" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" },
-]
-
-[[package]]
-name = "psygnal"
-version = "0.14.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/2b/c7/ee9108e587dbbc87fddcc9b971975b3371930a0059d3696d5733469e8989/psygnal-0.14.0.tar.gz", hash = "sha256:bdd219217d240611af31621a6701505256e245abb6e0dc86d7e4443c3f7d6d41", size = 121999, upload-time = "2025-07-01T19:57:39.479Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/92/fe/9137e7362ba9a10d546af9210017eae36065b91e64b2f1ae801a557b492d/psygnal-0.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:43b9afb0977142f4194756377056ded2a5a3019dda9fcbeb235c9919f8e3aaf4", size = 511625, upload-time = "2025-07-01T19:57:02.117Z" },
- { url = "https://files.pythonhosted.org/packages/bd/7d/014590d2e228e79072206a487f7ba41170881b464d9abb610469857900f8/psygnal-0.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a7eb817a55c87bbb120ad9c9245f18545d256dc91a94a8ea0746ef343b715d4", size = 480801, upload-time = "2025-07-01T19:57:03.722Z" },
- { url = "https://files.pythonhosted.org/packages/7f/ae/e54be26a82078004bc9ea5e0a318645abdd5b3e41a7d09870120f2f3062c/psygnal-0.14.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36189e829e16163c5f8f9d14e39084539c05ad52389a48c08e266fb68dee12b8", size = 847991, upload-time = "2025-07-01T19:57:05.188Z" },
- { url = "https://files.pythonhosted.org/packages/31/3c/fdcc59a2c9382808bbfde61e22585e9855d47345a8f7e99ea9b44963f0b7/psygnal-0.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:72dc4fd171d8d783b1bc915d876abb340d5f870fa6a81fb3f5310c719341d3b2", size = 841530, upload-time = "2025-07-01T19:57:06.871Z" },
- { url = "https://files.pythonhosted.org/packages/9b/19/54c2aca65b2702a5774fa2004448384e8738b866c01a5593cbf38c5432cb/psygnal-0.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:c569365f6df45463de1851e6219ac08a5bf91ecf4fdf7ba57a6303535b945d5c", size = 407212, upload-time = "2025-07-01T19:57:08.098Z" },
- { url = "https://files.pythonhosted.org/packages/58/4f/7878f6f877f3f20248665e4f932815b1785d6be7f95106298ff8af906018/psygnal-0.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f2bed92247fb7db3113b9aaeb5335fe95b479a08fd8837218d2f3ae699e8d7a3", size = 505298, upload-time = "2025-07-01T19:57:09.73Z" },
- { url = "https://files.pythonhosted.org/packages/e1/88/16de03be01459c73f10712122aaa21d1b192a8cee56d8cf36b565298b244/psygnal-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fd017d55c805a979d122a8ff89e8b83793f835e15dd12ee6c52f700cb5c83210", size = 473629, upload-time = "2025-07-01T19:57:11.4Z" },
- { url = "https://files.pythonhosted.org/packages/78/a1/04a64e6e1cacd2a7e46ab668bce6bc4f47534a1b6bdfcdc1ae02fb62c58c/psygnal-0.14.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2eb0991ec544de6f2ab24f2c84eb979e730c1897c4fa90b86d7f2e195ae41f6", size = 837407, upload-time = "2025-07-01T19:57:12.924Z" },
- { url = "https://files.pythonhosted.org/packages/07/27/d80e6e9001c426d2b494250432b254b100ece828cf5c3e9114446866a3da/psygnal-0.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f486e8c383266930011cd53068343ae57222fef1062b3c598203027eea6e2e3d", size = 827844, upload-time = "2025-07-01T19:57:14.785Z" },
- { url = "https://files.pythonhosted.org/packages/61/0e/2e4a5c430b4f3741be770c2dca5788f64a89fe164ab90d6dda58e55513b9/psygnal-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:dada160b89c666592c3b91715904f0a609cd5ad6bde0360317747e674173e4f3", size = 412080, upload-time = "2025-07-01T19:57:16.075Z" },
- { url = "https://files.pythonhosted.org/packages/13/95/b31f7c4493bdde9dc5caca48a0b2bb0ba1c1784459cef2f38d570561ebee/psygnal-0.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:08f3867a1f1b0952403ee492482823600ae26d79fa2b57be273f31559d70dfb9", size = 514648, upload-time = "2025-07-01T19:57:17.357Z" },
- { url = "https://files.pythonhosted.org/packages/6e/38/8428f77237fb3fe67312ff22658de4c74000391d1b58fd8c041da2d77854/psygnal-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5055400f031ea9c54cb5f0ec272d8172386a0868242f86540cb675cfb4c82298", size = 471455, upload-time = "2025-07-01T19:57:18.547Z" },
- { url = "https://files.pythonhosted.org/packages/6a/aa/b22c1ee65e5efb7bd5fad296440654330f6e8e04243200ce30eecf566031/psygnal-0.14.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:59a47acd9b0c516885a4211339f5bf8a9f43916f1c01799513c16b7b2fee704f", size = 869495, upload-time = "2025-07-01T19:57:20.213Z" },
- { url = "https://files.pythonhosted.org/packages/a1/0e/0d1b4d6d3affd0ee1407f51d1272e6bb33d34f99ac4dedf2c4460b33c5cb/psygnal-0.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49f6441d157260f1e8a5c843875b79bc9cd5008c2a5ac77a979f25f7cba769e3", size = 862696, upload-time = "2025-07-01T19:57:21.571Z" },
- { url = "https://files.pythonhosted.org/packages/0c/2b/0a301152a86d3ff2bc444523c652ffe0bb0e6746d3c0e6ffc0fd8269b817/psygnal-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:d7f7ff9f64d6c514cd7eba863314329a953d9a2ae16f13226df812199f3c5a74", size = 415722, upload-time = "2025-07-01T19:57:22.8Z" },
- { url = "https://files.pythonhosted.org/packages/46/9c/80c79f7d0fc0887857f05cac11ae62097b0be83fe0f608adf9edbceed5bc/psygnal-0.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d580a96d2326a72582d217756477d0302e02418ed319142a4f9a94f408ade40", size = 513698, upload-time = "2025-07-01T19:57:24.07Z" },
- { url = "https://files.pythonhosted.org/packages/c0/03/63f8d51206fd6762f4ea3ca466d08e32836b39995c07949808921884a693/psygnal-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ffa20c31461d6651861a2aa3df8b28ad4a750e98595119d08efa5e212d02c8a", size = 470780, upload-time = "2025-07-01T19:57:25.759Z" },
- { url = "https://files.pythonhosted.org/packages/55/4b/4b7c469a16edd5ee22ab8b7559613e392600bc543e13931b05b9bfc03da7/psygnal-0.14.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb84461e14c73ee08b7aadf6ee6c9744155c88f9b13848b1e6d4ca0eb80ebc44", size = 866202, upload-time = "2025-07-01T19:57:27.209Z" },
- { url = "https://files.pythonhosted.org/packages/e0/83/e9d7a970bee3080bfb9ffac061ab6de8d17f11d946ca1aade94c6f3a571b/psygnal-0.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b225bbc546a25a7bc427c8e8b1792647c8457475754048134d5a8a0e982d225e", size = 858963, upload-time = "2025-07-01T19:57:28.811Z" },
- { url = "https://files.pythonhosted.org/packages/68/bf/e29ad79e905368a926e36f7fc5a88ba54d9129fadf66266029e8d3d02ee1/psygnal-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9a102e4e2a531a7646f3aecee747b6181355272700340827fc86ee4f5028a29", size = 416364, upload-time = "2025-07-01T19:57:30.449Z" },
- { url = "https://files.pythonhosted.org/packages/32/54/73887f18105faf343b8d3c51952e5d4bc478806d848f0171aac942b6394e/psygnal-0.14.0-py3-none-any.whl", hash = "sha256:2f9011348b923283f13c8b2bca5ffcd6f6c6b5d06954d7e6953a8bbcf62ff4b6", size = 88997, upload-time = "2025-07-01T19:57:38.464Z" },
-]
-
-[[package]]
-name = "ptyprocess"
-version = "0.7.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" },
-]
-
-[[package]]
-name = "pure-eval"
-version = "0.2.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" },
-]
-
-[[package]]
-name = "pycparser"
-version = "2.22"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
-]
-
-[[package]]
-name = "pydantic"
-version = "2.11.7"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "annotated-types" },
- { name = "pydantic-core" },
- { name = "typing-extensions" },
- { name = "typing-inspection" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" },
-]
-
-[[package]]
-name = "pydantic-core"
-version = "2.33.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817, upload-time = "2025-04-23T18:30:43.919Z" },
- { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357, upload-time = "2025-04-23T18:30:46.372Z" },
- { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011, upload-time = "2025-04-23T18:30:47.591Z" },
- { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730, upload-time = "2025-04-23T18:30:49.328Z" },
- { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178, upload-time = "2025-04-23T18:30:50.907Z" },
- { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462, upload-time = "2025-04-23T18:30:52.083Z" },
- { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652, upload-time = "2025-04-23T18:30:53.389Z" },
- { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306, upload-time = "2025-04-23T18:30:54.661Z" },
- { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720, upload-time = "2025-04-23T18:30:56.11Z" },
- { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915, upload-time = "2025-04-23T18:30:57.501Z" },
- { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884, upload-time = "2025-04-23T18:30:58.867Z" },
- { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496, upload-time = "2025-04-23T18:31:00.078Z" },
- { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019, upload-time = "2025-04-23T18:31:01.335Z" },
- { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" },
- { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" },
- { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" },
- { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" },
- { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" },
- { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" },
- { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" },
- { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" },
- { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" },
- { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" },
- { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" },
- { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" },
- { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" },
- { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" },
- { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" },
- { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" },
- { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" },
- { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" },
- { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" },
- { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" },
- { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" },
- { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" },
- { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" },
- { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" },
- { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" },
- { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" },
- { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" },
- { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" },
- { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" },
- { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" },
- { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" },
- { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" },
- { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" },
- { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" },
- { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" },
- { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" },
- { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" },
- { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" },
- { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" },
- { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" },
- { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" },
- { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" },
- { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" },
- { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" },
- { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
- { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" },
- { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" },
- { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" },
- { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527, upload-time = "2025-04-23T18:32:59.771Z" },
- { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225, upload-time = "2025-04-23T18:33:04.51Z" },
- { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490, upload-time = "2025-04-23T18:33:06.391Z" },
- { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525, upload-time = "2025-04-23T18:33:08.44Z" },
- { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446, upload-time = "2025-04-23T18:33:10.313Z" },
- { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678, upload-time = "2025-04-23T18:33:12.224Z" },
- { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" },
- { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" },
- { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" },
- { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" },
- { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" },
- { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" },
- { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" },
- { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" },
- { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" },
-]
-
-[[package]]
-name = "pygfx"
-version = "0.12.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "freetype-py" },
- { name = "hsluv" },
- { name = "jinja2" },
- { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "numpy", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
- { name = "pylinalg" },
- { name = "rendercanvas" },
- { name = "uharfbuzz" },
- { name = "wgpu" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/ec/6a/119d03f840ac2ad98b28bf86d3027341bff26a4a337017c086694741ea21/pygfx-0.12.0.tar.gz", hash = "sha256:d07e5a4846862e576a898dfd27aadce25a8d894f9df2e4804624a54dc846ebd8", size = 1106508, upload-time = "2025-06-12T21:06:15.928Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/80/4b/a5f018c2a42b865278685b5a97e0ad391ba40f18f0760cf46fb77b34925e/pygfx-0.12.0-py3-none-any.whl", hash = "sha256:ec6a916168ac2e338130fb7473a8ce8ea9d1a7a0f1559aee0ebdf3a9dc5cf437", size = 1185931, upload-time = "2025-06-12T21:06:14.368Z" },
-]
-
-[[package]]
-name = "pygments"
-version = "2.19.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
-]
-
-[[package]]
-name = "pylinalg"
-version = "0.6.7"
-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.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/0f/79/b3e346cc0fc4db0df6bd1e429e8f1edd115c6a839aaf651a1021a795a930/pylinalg-0.6.7.tar.gz", hash = "sha256:c3f0d01d936129ec4b9cfcdd2cc4f68382a7a3ede0a5a9448091279e1187f204", size = 15990, upload-time = "2025-02-24T20:45:48.358Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ba/55/470aa69f6fde6ca08449469dc9e58e08994acede0977dda05de8268a581f/pylinalg-0.6.7-py3-none-any.whl", hash = "sha256:7d71f98b20ea7d8805f03101198761c2e7952648f60eaba58c407cc1fe7406f1", size = 18408, upload-time = "2025-02-24T20:45:46.792Z" },
-]
-
-[[package]]
-name = "pymdown-extensions"
-version = "10.16"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "markdown" },
- { name = "pyyaml" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/1a/0a/c06b542ac108bfc73200677309cd9188a3a01b127a63f20cadc18d873d88/pymdown_extensions-10.16.tar.gz", hash = "sha256:71dac4fca63fabeffd3eb9038b756161a33ec6e8d230853d3cecf562155ab3de", size = 853197, upload-time = "2025-06-21T17:56:36.974Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/98/d4/10bb14004d3c792811e05e21b5e5dcae805aacb739bd12a0540967b99592/pymdown_extensions-10.16-py3-none-any.whl", hash = "sha256:f5dd064a4db588cb2d95229fc4ee63a1b16cc8b4d0e6145c0899ed8723da1df2", size = 266143, upload-time = "2025-06-21T17:56:35.356Z" },
-]
-
-[[package]]
-name = "pyopengl"
-version = "3.1.9"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/c0/42/71080db298df3ddb7e3090bfea8fd7c300894d8b10954c22f8719bd434eb/pyopengl-3.1.9.tar.gz", hash = "sha256:28ebd82c5f4491a418aeca9672dffb3adbe7d33b39eada4548a5b4e8c03f60c8", size = 1913642, upload-time = "2025-01-20T02:17:53.263Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/92/44/8634af40b0db528b5b37e901c0dc67321354880d251bf8965901d57693a5/PyOpenGL-3.1.9-py3-none-any.whl", hash = "sha256:15995fd3b0deb991376805da36137a4ae5aba6ddbb5e29ac1f35462d130a3f77", size = 3190341, upload-time = "2025-01-20T02:17:50.913Z" },
-]
-
-[[package]]
-name = "pyparsing"
-version = "3.2.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload-time = "2025-03-25T05:01:28.114Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" },
-]
-
-[[package]]
-name = "pyqt6"
-version = "6.9.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pyqt6-qt6" },
- { name = "pyqt6-sip" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/32/1b/567f46eb43ca961efd38d7a0b73efb70d7342854f075fd919179fdb2a571/pyqt6-6.9.1.tar.gz", hash = "sha256:50642be03fb40f1c2111a09a1f5a0f79813e039c15e78267e6faaf8a96c1c3a6", size = 1067230, upload-time = "2025-06-06T08:49:30.307Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/18/c4/fc2a69cf3df09b213185ef5a677c3940cd20e7855d29e40061a685b9c6ee/pyqt6-6.9.1-cp39-abi3-macosx_10_14_universal2.whl", hash = "sha256:33c23d28f6608747ecc8bfd04c8795f61631af9db4fb1e6c2a7523ec4cc916d9", size = 59770566, upload-time = "2025-06-06T08:48:20.331Z" },
- { url = "https://files.pythonhosted.org/packages/d5/78/92f3c46440a83ebe22ae614bd6792e7b052bcb58ff128f677f5662015184/pyqt6-6.9.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:37884df27f774e2e1c0c96fa41e817a222329b80ffc6241725b0dc8c110acb35", size = 37804959, upload-time = "2025-06-06T08:48:39.587Z" },
- { url = "https://files.pythonhosted.org/packages/5a/5e/e77fa2761d809cd08d724f44af01a4b6ceb0ff9648e43173187b0e4fac4e/pyqt6-6.9.1-cp39-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:055870b703c1a49ca621f8a89e2ec4d848e6c739d39367eb9687af3b056d9aa3", size = 40414608, upload-time = "2025-06-06T08:49:00.26Z" },
- { url = "https://files.pythonhosted.org/packages/c4/09/69cf80456b6a985e06dd24ed0c2d3451e43567bf2807a5f3a86ef7a74a2e/pyqt6-6.9.1-cp39-abi3-win_amd64.whl", hash = "sha256:15b95bd273bb6288b070ed7a9503d5ff377aa4882dd6d175f07cad28cdb21da0", size = 25717996, upload-time = "2025-06-06T08:49:13.208Z" },
- { url = "https://files.pythonhosted.org/packages/52/b3/0839d8fd18b86362a4de384740f2f6b6885b5d06fda7720f8a335425e316/pyqt6-6.9.1-cp39-abi3-win_arm64.whl", hash = "sha256:08792c72d130a02e3248a120f0b9bbb4bf4319095f92865bc5b365b00518f53d", size = 25212132, upload-time = "2025-06-06T08:49:27.41Z" },
-]
-
-[[package]]
-name = "pyqt6-qt6"
-version = "6.9.1"
-source = { registry = "https://pypi.org/simple" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/51/40/04f652e714f85ba6b0c24f4ead860f2c5769f9e64737f415524d792d5914/pyqt6_qt6-6.9.1-py3-none-macosx_10_14_x86_64.whl", hash = "sha256:3854c7f83ee4e8c2d91e23ab88b77f90e2ca7ace34fe72f634a446959f2b4d4a", size = 66236777, upload-time = "2025-06-03T14:53:17.684Z" },
- { url = "https://files.pythonhosted.org/packages/57/31/e4fa40568a59953ce5cf9a5adfbd1be4a806dafd94e39072d3cc0bed5468/pyqt6_qt6-6.9.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:123e4aeb037c099bb4696a3ea8edcb1d9d62cedd0b2b950556b26024c97f3293", size = 60551574, upload-time = "2025-06-03T14:53:48.42Z" },
- { url = "https://files.pythonhosted.org/packages/aa/8d/7c8073cbbefe9c103ec8add70f29ffee1db95a3755b429b9f47cd6afa41b/pyqt6_qt6-6.9.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:cc5bd193ebd2d1a3ec66e1eee65bf532d762c239459bce1ecebf56177243e89b", size = 82000130, upload-time = "2025-06-03T14:54:26.585Z" },
- { url = "https://files.pythonhosted.org/packages/1e/60/a4ab932028b0c15c0501cb52eb1e7f24f4ce2e4c78d46c7cce58a375a88c/pyqt6_qt6-6.9.1-py3-none-manylinux_2_39_aarch64.whl", hash = "sha256:b065af7243d1d450a49470a8185301196a18b1d41085d3ef476eb55bbb225083", size = 80463127, upload-time = "2025-06-03T14:55:03.272Z" },
- { url = "https://files.pythonhosted.org/packages/e7/85/552710819019a96d39d924071324a474aec54b31c410d7de8ebb398adcc1/pyqt6_qt6-6.9.1-py3-none-win_amd64.whl", hash = "sha256:f9e54c424bc921ecb76792a75d123e4ecfc26b00b0c57dae526f41f1d57951d3", size = 73778423, upload-time = "2025-06-03T14:55:39.756Z" },
- { url = "https://files.pythonhosted.org/packages/16/b4/70f6b18a4913f2326dcf7acb15c12cc0b91cb3932c2ba3b5728811f22acd/pyqt6_qt6-6.9.1-py3-none-win_arm64.whl", hash = "sha256:432caaedf5570bc8a9b7c75bc6af6a26bf88589536472eca73417ac019f59d41", size = 49617924, upload-time = "2025-06-03T14:57:13.038Z" },
-]
-
-[[package]]
-name = "pyqt6-sip"
-version = "13.10.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/2f/4a/96daf6c2e4f689faae9bd8cebb52754e76522c58a6af9b5ec86a2e8ec8b4/pyqt6_sip-13.10.2.tar.gz", hash = "sha256:464ad156bf526500ce6bd05cac7a82280af6309974d816739b4a9a627156fafe", size = 92548, upload-time = "2025-05-23T12:26:49.901Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/92/a8/9eb019525f26801cf91ba38c8493ef641ee943d3b77885e78ac9fab11870/pyqt6_sip-13.10.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8132ec1cbbecc69d23dcff23916ec07218f1a9bbbc243bf6f1df967117ce303e", size = 110689, upload-time = "2025-05-23T12:26:21.436Z" },
- { url = "https://files.pythonhosted.org/packages/0b/29/79a2dba1cc6ec02c927dd0ffd596ca15ba0a2968123143bc00fc35f0173b/pyqt6_sip-13.10.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07f77e89d93747dda71b60c3490b00d754451729fbcbcec840e42084bf061655", size = 305804, upload-time = "2025-05-23T12:26:23.297Z" },
- { url = "https://files.pythonhosted.org/packages/bb/4f/fa8468f055679905d0e38d471ae16b5968896ee1d951477e162d9d0a712d/pyqt6_sip-13.10.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4ffa71ddff6ef031d52cd4f88b8bba08b3516313c023c7e5825cf4a0ba598712", size = 284059, upload-time = "2025-05-23T12:26:24.507Z" },
- { url = "https://files.pythonhosted.org/packages/e1/4e/abc995daaafe5ac55e00df0f42c4a5ee81473425a3250a20dc4301399842/pyqt6_sip-13.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:e907394795e61f1174134465c889177f584336a98d7a10beade2437bf5942244", size = 53410, upload-time = "2025-05-23T12:26:25.62Z" },
- { url = "https://files.pythonhosted.org/packages/75/9c/ea9ba7786f471ce025dff71653eec4a6c067d24d36d28cced457dd31314c/pyqt6_sip-13.10.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1a6c2f168773af9e6c7ef5e52907f16297d4efd346e4c958eda54ea9135be18e", size = 110707, upload-time = "2025-05-23T12:26:26.666Z" },
- { url = "https://files.pythonhosted.org/packages/d6/00/984a94f14ba378c802a8e304803bb6dc6961cd9f24befa1bf3987731f0c3/pyqt6_sip-13.10.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1d3cc9015a1bd8c8d3e86a009591e897d4d46b0c514aede7d2970a2208749cd", size = 317301, upload-time = "2025-05-23T12:26:28.182Z" },
- { url = "https://files.pythonhosted.org/packages/0d/b1/c3b433ebcee2503571d71be025de5dab4489d7153007fd5ae79c543eeedb/pyqt6_sip-13.10.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ddd578a8d975bfb5fef83751829bf09a97a1355fa1de098e4fb4d1b74ee872fc", size = 294277, upload-time = "2025-05-23T12:26:29.406Z" },
- { url = "https://files.pythonhosted.org/packages/24/96/4e909f0a4f7a9ad0076a0e200c10f96a5a09492efb683f3d66c885f9aba4/pyqt6_sip-13.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:061d4a2eb60a603d8be7db6c7f27eb29d9cea97a09aa4533edc1662091ce4f03", size = 53418, upload-time = "2025-05-23T12:26:30.536Z" },
- { url = "https://files.pythonhosted.org/packages/37/96/153c418d8c167fc56f2e62372b8862d577f3ece41b24c5205a05b0c2b0cd/pyqt6_sip-13.10.2-cp311-cp311-win_arm64.whl", hash = "sha256:45ac06f0380b7aa4fcffd89f9e8c00d1b575dc700c603446a9774fda2dcfc0de", size = 44969, upload-time = "2025-05-23T12:26:31.498Z" },
- { url = "https://files.pythonhosted.org/packages/22/5b/1240017e0d59575289ba52b58fd7f95e7ddf0ed2ede95f3f7e2dc845d337/pyqt6_sip-13.10.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:83e6a56d3e715f748557460600ec342cbd77af89ec89c4f2a68b185fa14ea46c", size = 112199, upload-time = "2025-05-23T12:26:32.503Z" },
- { url = "https://files.pythonhosted.org/packages/51/11/1fc3bae02a12a3ac8354aa579b56206286e8b5ca9586677b1058c81c2f74/pyqt6_sip-13.10.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ccf197f8fa410e076936bee28ad9abadb450931d5be5625446fd20e0d8b27a6", size = 322757, upload-time = "2025-05-23T12:26:33.752Z" },
- { url = "https://files.pythonhosted.org/packages/21/40/de9491213f480a27199690616959a17a0f234962b86aa1dd4ca2584e922d/pyqt6_sip-13.10.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:37af463dcce39285e686d49523d376994d8a2508b9acccb7616c4b117c9c4ed7", size = 304251, upload-time = "2025-05-23T12:26:35.66Z" },
- { url = "https://files.pythonhosted.org/packages/02/21/cc80e03f1052408c62c341e9fe9b81454c94184f4bd8a95d29d2ec86df92/pyqt6_sip-13.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:c7b34a495b92790c70eae690d9e816b53d3b625b45eeed6ae2c0fe24075a237e", size = 53519, upload-time = "2025-05-23T12:26:36.797Z" },
- { url = "https://files.pythonhosted.org/packages/77/cf/53bd0863252b260a502659cb3124d9c9fe38047df9360e529b437b4ac890/pyqt6_sip-13.10.2-cp312-cp312-win_arm64.whl", hash = "sha256:c80cc059d772c632f5319632f183e7578cd0976b9498682833035b18a3483e92", size = 45349, upload-time = "2025-05-23T12:26:37.729Z" },
- { url = "https://files.pythonhosted.org/packages/a1/1e/979ea64c98ca26979d8ce11e9a36579e17d22a71f51d7366d6eec3c82c13/pyqt6_sip-13.10.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8b5d06a0eac36038fa8734657d99b5fe92263ae7a0cd0a67be6acfe220a063e1", size = 112227, upload-time = "2025-05-23T12:26:38.758Z" },
- { url = "https://files.pythonhosted.org/packages/d9/21/84c230048e3bfef4a9209d16e56dcd2ae10590d03a31556ae8b5f1dcc724/pyqt6_sip-13.10.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad376a6078da37b049fdf9d6637d71b52727e65c4496a80b753ddc8d27526aca", size = 322920, upload-time = "2025-05-23T12:26:39.856Z" },
- { url = "https://files.pythonhosted.org/packages/b0/1e/c6a28a142f14e735088534cc92951c3f48cccd77cdd4f3b10d7996be420f/pyqt6_sip-13.10.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3dde8024d055f496eba7d44061c5a1ba4eb72fc95e5a9d7a0dbc908317e0888b", size = 303833, upload-time = "2025-05-23T12:26:41.075Z" },
- { url = "https://files.pythonhosted.org/packages/89/63/e5adf350c1c3123d4865c013f164c5265512fa79f09ad464fb2fdf9f9e61/pyqt6_sip-13.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:0b097eb58b4df936c4a2a88a2f367c8bb5c20ff049a45a7917ad75d698e3b277", size = 53527, upload-time = "2025-05-23T12:26:42.625Z" },
- { url = "https://files.pythonhosted.org/packages/58/74/2df4195306d050fbf4963fb5636108a66e5afa6dc05fd9e81e51ec96c384/pyqt6_sip-13.10.2-cp313-cp313-win_arm64.whl", hash = "sha256:cc6a1dfdf324efaac6e7b890a608385205e652845c62130de919fd73a6326244", size = 45373, upload-time = "2025-05-23T12:26:43.536Z" },
-]
-
-[[package]]
-name = "pyrepl"
-version = "0.11.3.post1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/92/78/65b9a4a4b15f7f5aae84c3edb9d3adc1acb2acc38eb9d5404cd5cf980927/pyrepl-0.11.3.post1.tar.gz", hash = "sha256:0ca7568c8be919b69f99644d29d31738a5b1a87750d06dd36564bcfad278d402", size = 50954, upload-time = "2025-04-13T12:21:51.403Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2d/45/02a5e2da58a32ff407b14d38690c1d2f8705bd6909320817b444070dcbb0/pyrepl-0.11.3.post1-py3-none-any.whl", hash = "sha256:59fcd67588892731dc6e7aff106c380d303d54324ff028827804a2b056223d92", size = 55613, upload-time = "2025-04-13T12:21:49.464Z" },
-]
-
-[[package]]
-name = "pyright"
-version = "1.1.403"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "nodeenv" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/fe/f6/35f885264ff08c960b23d1542038d8da86971c5d8c955cfab195a4f672d7/pyright-1.1.403.tar.gz", hash = "sha256:3ab69b9f41c67fb5bbb4d7a36243256f0d549ed3608678d381d5f51863921104", size = 3913526, upload-time = "2025-07-09T07:15:52.882Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/49/b6/b04e5c2f41a5ccad74a1a4759da41adb20b4bc9d59a5e08d29ba60084d07/pyright-1.1.403-py3-none-any.whl", hash = "sha256:c0eeca5aa76cbef3fcc271259bbd785753c7ad7bcac99a9162b4c4c7daed23b3", size = 5684504, upload-time = "2025-07-09T07:15:50.958Z" },
-]
-
-[[package]]
-name = "pytest"
-version = "8.4.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
- { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
- { name = "iniconfig" },
- { name = "packaging" },
- { name = "pluggy" },
- { name = "pygments" },
- { name = "tomli", marker = "python_full_version < '3.11'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" },
-]
-
-[[package]]
-name = "pytest-cov"
-version = "6.2.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "coverage", extra = ["toml"] },
- { name = "pluggy" },
- { name = "pytest" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" },
-]
-
-[[package]]
-name = "python-dateutil"
-version = "2.9.0.post0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "six" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
-]
-
-[[package]]
-name = "pyyaml"
-version = "6.0.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" },
- { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" },
- { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" },
- { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" },
- { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" },
- { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" },
- { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" },
- { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" },
- { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" },
- { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" },
- { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" },
- { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" },
- { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" },
- { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" },
- { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" },
- { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" },
- { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" },
- { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" },
- { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" },
- { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" },
- { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" },
- { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" },
- { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" },
- { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" },
- { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" },
- { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" },
- { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" },
- { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
- { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
- { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
- { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
- { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
- { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
- { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
- { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
- { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
-]
-
-[[package]]
-name = "pyyaml-env-tag"
-version = "1.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pyyaml" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" },
-]
-
-[[package]]
-name = "rendercanvas"
-version = "2.2.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "sniffio" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/1f/10/6fe20f4e7d74e580e15f3a440069bac3d01460738502392c300494d97459/rendercanvas-2.2.0.tar.gz", hash = "sha256:d2a0f620eb29db852cd9e1d4abf259f1385db890a2dc2146506205e681e6bc79", size = 61541, upload-time = "2025-06-30T13:45:46.404Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2c/d5/4386e000f0179813c14b23aa6b300fbbb3bc353f5aa269bfc515082cd8cd/rendercanvas-2.2.0-py3-none-any.whl", hash = "sha256:798d38f8700239bc62d0a2a1c661fe8c241545dc9c28fb2679507548b93ff40b", size = 73979, upload-time = "2025-06-30T13:45:45.562Z" },
-]
-
-[[package]]
-name = "requests"
-version = "2.32.4"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "certifi" },
- { name = "charset-normalizer" },
- { name = "idna" },
- { name = "urllib3" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
-]
-
-[[package]]
-name = "rich"
-version = "14.0.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "markdown-it-py" },
- { name = "pygments" },
- { name = "typing-extensions", marker = "python_full_version < '3.11'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" },
-]
-
-[[package]]
-name = "rubicon-objc"
-version = "0.5.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e0/83/e57741dcf862a2581d53eccf8b11749c97f73d9754bbc538fb6c7b527da3/rubicon_objc-0.5.1.tar.gz", hash = "sha256:90bee9fc1de4515e17615e15648989b88bb8d4d2ffc8c7c52748272cd7f30a66", size = 174639, upload-time = "2025-06-03T06:33:50.822Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/58/0a/e451c3dbda38dd6abab1fd16c3b35623fc0635dffcbbf97f1acc55a58508/rubicon_objc-0.5.1-py3-none-any.whl", hash = "sha256:17092756241b8370231cfaad45ad6e8ce99534987f2acbc944d65df5bdf8f6cd", size = 63323, upload-time = "2025-06-03T06:33:48.863Z" },
-]
-
-[[package]]
-name = "ruff"
-version = "0.12.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/6c/3d/d9a195676f25d00dbfcf3cf95fdd4c685c497fcfa7e862a44ac5e4e96480/ruff-0.12.2.tar.gz", hash = "sha256:d7b4f55cd6f325cb7621244f19c873c565a08aff5a4ba9c69aa7355f3f7afd3e", size = 4432239, upload-time = "2025-07-03T16:40:19.566Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/74/b6/2098d0126d2d3318fd5bec3ad40d06c25d377d95749f7a0c5af17129b3b1/ruff-0.12.2-py3-none-linux_armv6l.whl", hash = "sha256:093ea2b221df1d2b8e7ad92fc6ffdca40a2cb10d8564477a987b44fd4008a7be", size = 10369761, upload-time = "2025-07-03T16:39:38.847Z" },
- { url = "https://files.pythonhosted.org/packages/b1/4b/5da0142033dbe155dc598cfb99262d8ee2449d76920ea92c4eeb9547c208/ruff-0.12.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:09e4cf27cc10f96b1708100fa851e0daf21767e9709e1649175355280e0d950e", size = 11155659, upload-time = "2025-07-03T16:39:42.294Z" },
- { url = "https://files.pythonhosted.org/packages/3e/21/967b82550a503d7c5c5c127d11c935344b35e8c521f52915fc858fb3e473/ruff-0.12.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8ae64755b22f4ff85e9c52d1f82644abd0b6b6b6deedceb74bd71f35c24044cc", size = 10537769, upload-time = "2025-07-03T16:39:44.75Z" },
- { url = "https://files.pythonhosted.org/packages/33/91/00cff7102e2ec71a4890fb7ba1803f2cdb122d82787c7d7cf8041fe8cbc1/ruff-0.12.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eb3a6b2db4d6e2c77e682f0b988d4d61aff06860158fdb413118ca133d57922", size = 10717602, upload-time = "2025-07-03T16:39:47.652Z" },
- { url = "https://files.pythonhosted.org/packages/9b/eb/928814daec4e1ba9115858adcda44a637fb9010618721937491e4e2283b8/ruff-0.12.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73448de992d05517170fc37169cbca857dfeaeaa8c2b9be494d7bcb0d36c8f4b", size = 10198772, upload-time = "2025-07-03T16:39:49.641Z" },
- { url = "https://files.pythonhosted.org/packages/50/fa/f15089bc20c40f4f72334f9145dde55ab2b680e51afb3b55422effbf2fb6/ruff-0.12.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b8b94317cbc2ae4a2771af641739f933934b03555e51515e6e021c64441532d", size = 11845173, upload-time = "2025-07-03T16:39:52.069Z" },
- { url = "https://files.pythonhosted.org/packages/43/9f/1f6f98f39f2b9302acc161a4a2187b1e3a97634fe918a8e731e591841cf4/ruff-0.12.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:45fc42c3bf1d30d2008023a0a9a0cfb06bf9835b147f11fe0679f21ae86d34b1", size = 12553002, upload-time = "2025-07-03T16:39:54.551Z" },
- { url = "https://files.pythonhosted.org/packages/d8/70/08991ac46e38ddd231c8f4fd05ef189b1b94be8883e8c0c146a025c20a19/ruff-0.12.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce48f675c394c37e958bf229fb5c1e843e20945a6d962cf3ea20b7a107dcd9f4", size = 12171330, upload-time = "2025-07-03T16:39:57.55Z" },
- { url = "https://files.pythonhosted.org/packages/88/a9/5a55266fec474acfd0a1c73285f19dd22461d95a538f29bba02edd07a5d9/ruff-0.12.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793d8859445ea47591272021a81391350205a4af65a9392401f418a95dfb75c9", size = 11774717, upload-time = "2025-07-03T16:39:59.78Z" },
- { url = "https://files.pythonhosted.org/packages/87/e5/0c270e458fc73c46c0d0f7cf970bb14786e5fdb88c87b5e423a4bd65232b/ruff-0.12.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6932323db80484dda89153da3d8e58164d01d6da86857c79f1961934354992da", size = 11646659, upload-time = "2025-07-03T16:40:01.934Z" },
- { url = "https://files.pythonhosted.org/packages/b7/b6/45ab96070c9752af37f0be364d849ed70e9ccede07675b0ec4e3ef76b63b/ruff-0.12.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6aa7e623a3a11538108f61e859ebf016c4f14a7e6e4eba1980190cacb57714ce", size = 10604012, upload-time = "2025-07-03T16:40:04.363Z" },
- { url = "https://files.pythonhosted.org/packages/86/91/26a6e6a424eb147cc7627eebae095cfa0b4b337a7c1c413c447c9ebb72fd/ruff-0.12.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2a4a20aeed74671b2def096bdf2eac610c7d8ffcbf4fb0e627c06947a1d7078d", size = 10176799, upload-time = "2025-07-03T16:40:06.514Z" },
- { url = "https://files.pythonhosted.org/packages/f5/0c/9f344583465a61c8918a7cda604226e77b2c548daf8ef7c2bfccf2b37200/ruff-0.12.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:71a4c550195612f486c9d1f2b045a600aeba851b298c667807ae933478fcef04", size = 11241507, upload-time = "2025-07-03T16:40:08.708Z" },
- { url = "https://files.pythonhosted.org/packages/1c/b7/99c34ded8fb5f86c0280278fa89a0066c3760edc326e935ce0b1550d315d/ruff-0.12.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4987b8f4ceadf597c927beee65a5eaf994c6e2b631df963f86d8ad1bdea99342", size = 11717609, upload-time = "2025-07-03T16:40:10.836Z" },
- { url = "https://files.pythonhosted.org/packages/51/de/8589fa724590faa057e5a6d171e7f2f6cffe3287406ef40e49c682c07d89/ruff-0.12.2-py3-none-win32.whl", hash = "sha256:369ffb69b70cd55b6c3fc453b9492d98aed98062db9fec828cdfd069555f5f1a", size = 10523823, upload-time = "2025-07-03T16:40:13.203Z" },
- { url = "https://files.pythonhosted.org/packages/94/47/8abf129102ae4c90cba0c2199a1a9b0fa896f6f806238d6f8c14448cc748/ruff-0.12.2-py3-none-win_amd64.whl", hash = "sha256:dca8a3b6d6dc9810ed8f328d406516bf4d660c00caeaef36eb831cf4871b0639", size = 11629831, upload-time = "2025-07-03T16:40:15.478Z" },
- { url = "https://files.pythonhosted.org/packages/e2/1f/72d2946e3cc7456bb837e88000eb3437e55f80db339c840c04015a11115d/ruff-0.12.2-py3-none-win_arm64.whl", hash = "sha256:48d6c6bfb4761df68bc05ae630e24f506755e702d4fb08f08460be778c7ccb12", size = 10735334, upload-time = "2025-07-03T16:40:17.677Z" },
-]
-
-[[package]]
-name = "scenex"
-source = { editable = "." }
-dependencies = [
- { name = "cmap" },
- { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "numpy", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
- { name = "psygnal" },
- { name = "pydantic" },
-]
-
-[package.optional-dependencies]
-imgui = [
- { name = "imgui-bundle" },
-]
-pygfx = [
- { name = "pygfx" },
-]
-vispy = [
- { name = "pyopengl" },
- { name = "vispy" },
-]
-
-[package.dev-dependencies]
-dev = [
- { name = "imageio", extra = ["tifffile"] },
- { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "ipython", version = "9.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
- { name = "mike" },
- { name = "mkdocs" },
- { name = "mkdocs-api-autonav" },
- { name = "mkdocs-material" },
- { name = "mkdocs-minify-plugin" },
- { name = "mkdocs-spellcheck", extra = ["all"] },
- { name = "mkdocstrings-python" },
- { name = "mypy" },
- { name = "pdbpp", marker = "sys_platform != 'win32'" },
- { name = "pre-commit-uv" },
- { name = "pyqt6" },
- { name = "pyright" },
- { name = "pytest" },
- { name = "pytest-cov" },
- { name = "rich" },
- { name = "ruff" },
- { name = "scenex", extra = ["imgui", "pygfx", "vispy"] },
-]
-docs = [
- { name = "mike" },
- { name = "mkdocs" },
- { name = "mkdocs-api-autonav" },
- { name = "mkdocs-material" },
- { name = "mkdocs-minify-plugin" },
- { name = "mkdocs-spellcheck", extra = ["all"] },
- { name = "mkdocstrings-python" },
-]
-test = [
- { name = "pyqt6" },
- { name = "pytest" },
- { name = "pytest-cov" },
-]
-
-[package.metadata]
-requires-dist = [
- { name = "cmap", specifier = ">=0.5" },
- { name = "imgui-bundle", marker = "extra == 'imgui'", specifier = ">=1.6,!=1.6.3,<1.92.0" },
- { name = "numpy", specifier = ">=1.24" },
- { name = "psygnal", specifier = ">=0.11.1" },
- { name = "pydantic", specifier = ">=2.10" },
- { name = "pygfx", marker = "extra == 'pygfx'", specifier = ">=0.9.0" },
- { name = "pyopengl", marker = "extra == 'vispy'" },
- { name = "vispy", marker = "extra == 'vispy'", specifier = ">=0.15.0" },
-]
-provides-extras = ["imgui", "pygfx", "vispy"]
-
-[package.metadata.requires-dev]
-dev = [
- { name = "imageio", extras = ["tifffile"], specifier = ">=2.20" },
- { name = "ipython" },
- { name = "mike", specifier = ">=2.1.3" },
- { name = "mkdocs", specifier = ">=1.6.1" },
- { name = "mkdocs-api-autonav", specifier = ">=0.2.1" },
- { name = "mkdocs-material", specifier = ">=9.6.12" },
- { name = "mkdocs-minify-plugin", specifier = ">=0.8.0" },
- { name = "mkdocs-spellcheck", extras = ["all"], specifier = ">=1.1.1" },
- { name = "mkdocstrings-python", specifier = ">=1.16.10" },
- { name = "mypy" },
- { name = "pdbpp", marker = "sys_platform != 'win32'" },
- { name = "pre-commit-uv" },
- { name = "pyqt6" },
- { name = "pyright", specifier = ">=1.1.392" },
- { name = "pytest", specifier = ">=8" },
- { name = "pytest-cov", specifier = ">=6" },
- { name = "rich" },
- { name = "ruff" },
- { name = "scenex", extras = ["pygfx", "vispy", "imgui"] },
-]
-docs = [
- { name = "mike", specifier = ">=2.1.3" },
- { name = "mkdocs", specifier = ">=1.6.1" },
- { name = "mkdocs-api-autonav", specifier = ">=0.2.1" },
- { name = "mkdocs-material", specifier = ">=9.6.12" },
- { name = "mkdocs-minify-plugin", specifier = ">=0.8.0" },
- { name = "mkdocs-spellcheck", extras = ["all"], specifier = ">=1.1.1" },
- { name = "mkdocstrings-python", specifier = ">=1.16.10" },
-]
-test = [
- { name = "pyqt6" },
- { name = "pytest", specifier = ">=8" },
- { name = "pytest-cov", specifier = ">=6" },
-]
-
-[[package]]
-name = "six"
-version = "1.17.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
-]
-
-[[package]]
-name = "sniffio"
-version = "1.3.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
-]
-
-[[package]]
-name = "stack-data"
-version = "0.6.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "asttokens" },
- { name = "executing" },
- { name = "pure-eval" },
-]
-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 = [
- { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" },
-]
-
-[[package]]
-name = "symspellpy"
-version = "6.9.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "editdistpy" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/ce/42/f445e683376bb5eee35db34e062372e0c4cfa6671632cb0b0dafa747e32e/symspellpy-6.9.0.tar.gz", hash = "sha256:5ce8cb8a13e531db03f664407abc9e42e272f16bab7c0639500a8bdd07eee482", size = 2615560, upload-time = "2025-03-09T09:42:33.92Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/54/07/c903a3a42222a4bb6aedf620a63554f577f3020b4f56f339889718126c1c/symspellpy-6.9.0-py3-none-any.whl", hash = "sha256:b6dba96275fdbedc240491854160bbe596dda930f2d58567ad55faf82a4e4b43", size = 2612234, upload-time = "2025-03-09T09:42:32.311Z" },
-]
-
-[[package]]
-name = "tifffile"
-version = "2025.5.10"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version < '3.11'",
-]
-dependencies = [
- { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/44/d0/18fed0fc0916578a4463f775b0fbd9c5fed2392152d039df2fb533bfdd5d/tifffile-2025.5.10.tar.gz", hash = "sha256:018335d34283aa3fd8c263bae5c3c2b661ebc45548fde31504016fcae7bf1103", size = 365290, upload-time = "2025-05-10T19:22:34.386Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/5d/06/bd0a6097da704a7a7c34a94cfd771c3ea3c2f405dd214e790d22c93f6be1/tifffile-2025.5.10-py3-none-any.whl", hash = "sha256:e37147123c0542d67bc37ba5cdd67e12ea6fbe6e86c52bee037a9eb6a064e5ad", size = 226533, upload-time = "2025-05-10T19:22:27.279Z" },
-]
-
-[[package]]
-name = "tifffile"
-version = "2025.6.11"
-source = { registry = "https://pypi.org/simple" }
-resolution-markers = [
- "python_full_version >= '3.11'",
-]
-dependencies = [
- { name = "numpy", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/11/9e/636e3e433c24da41dd639e0520db60750dbf5e938d023b83af8097382ea3/tifffile-2025.6.11.tar.gz", hash = "sha256:0ece4c2e7a10656957d568a093b07513c0728d30c1bd8cc12725901fffdb7143", size = 370125, upload-time = "2025-06-12T04:49:38.839Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3a/d8/1ba8f32bfc9cb69e37edeca93738e883f478fbe84ae401f72c0d8d507841/tifffile-2025.6.11-py3-none-any.whl", hash = "sha256:32effb78b10b3a283eb92d4ebf844ae7e93e151458b0412f38518b4e6d2d7542", size = 230800, upload-time = "2025-06-12T04:49:37.458Z" },
-]
-
-[[package]]
-name = "tomli"
-version = "2.2.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" },
- { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" },
- { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" },
- { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" },
- { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" },
- { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" },
- { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" },
- { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" },
- { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" },
- { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" },
- { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" },
- { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" },
- { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" },
- { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" },
- { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" },
- { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" },
- { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" },
- { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" },
- { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" },
- { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" },
- { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" },
- { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" },
- { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" },
- { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" },
- { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" },
- { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" },
- { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" },
- { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" },
- { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" },
- { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" },
- { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" },
-]
-
-[[package]]
-name = "traitlets"
-version = "5.14.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" },
-]
-
-[[package]]
-name = "typing-extensions"
-version = "4.14.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
-]
-
-[[package]]
-name = "typing-inspection"
-version = "0.4.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
-]
-
-[[package]]
-name = "uharfbuzz"
-version = "0.50.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/42/43/2c63967f7cefe2729660a5637fd7c82964889604eeef633de9267c4cf392/uharfbuzz-0.50.2.tar.gz", hash = "sha256:fd9c1e600248fe2464916e5ff93d80e4cfae113da3c2273355636a913a55c874", size = 1568221, upload-time = "2025-05-10T20:44:15.084Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ff/01/8183516e76625bb3a5704e14bb378ac36b486eb73dd533267d94f6fdd970/uharfbuzz-0.50.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:98f22ea69b5df2983e77148cc48a034a2e15fdb42fafceac8202b88a73a26b1c", size = 2924894, upload-time = "2025-05-10T20:42:10.778Z" },
- { url = "https://files.pythonhosted.org/packages/ca/c0/abffb23e70cc16b3d73929d53b1cfd8722d80ab279dc7dd1b2edaab4c6a4/uharfbuzz-0.50.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1a7ef89483dbf274231e60001ca529ccdc903b5cb8ecf72502223dbb5b5980ae", size = 1536066, upload-time = "2025-05-10T20:42:13.307Z" },
- { url = "https://files.pythonhosted.org/packages/fe/fb/b44464f864fffbddf9e42153ce15fe649b5096b5616c588bcc61dd012bbe/uharfbuzz-0.50.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76518e22206b15dd2fc6684864b09efde90f66a0d3bdb4c5680c4c77fcb92fb4", size = 1398198, upload-time = "2025-05-10T20:42:14.817Z" },
- { url = "https://files.pythonhosted.org/packages/d3/e6/b082a846d894ca5bbd9819da60bc5b676974dc6f683fd41f06235db10804/uharfbuzz-0.50.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dca5064094206963c77b47eb1764c35f2fd2c6035ddcfc205c27890968c644e8", size = 15080268, upload-time = "2025-05-10T20:42:16.511Z" },
- { url = "https://files.pythonhosted.org/packages/57/58/351f476fd2ece3d357815264172640fb59e916146fe00f0f662e9990fcf9/uharfbuzz-0.50.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01f76731a97177b58b7016a7bfa9c91b4de8ebbc78185b728a3d8597762ae5d5", size = 15222637, upload-time = "2025-05-10T20:42:18.486Z" },
- { url = "https://files.pythonhosted.org/packages/f2/8d/595c0ece8981b69dd9da73d64927aed78f49b0696043337347df331a89e8/uharfbuzz-0.50.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fcc501c79e1dfadd972ee5d0e8f1223c214d875521715748b96398dd85b10791", size = 15668242, upload-time = "2025-05-10T20:42:21.26Z" },
- { url = "https://files.pythonhosted.org/packages/bc/dd/f35097116af7138f151b5eeca5174c58260afb0da0e632085bf547e1732a/uharfbuzz-0.50.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9ad1a3148a55fd04f8980ee3c7ea4351e48f968306dd18c1c59b9104bbdba63a", size = 16054946, upload-time = "2025-05-10T20:42:23.715Z" },
- { url = "https://files.pythonhosted.org/packages/07/5e/7470ecca4486e4acc9086c47a913e4f6162c0c88e5884e0561707425957b/uharfbuzz-0.50.2-cp310-cp310-win32.whl", hash = "sha256:a6345c62279dc544b2c55955ab7189a23b756e34e5d625afae4478c0093f1f0f", size = 980449, upload-time = "2025-05-10T20:42:26.436Z" },
- { url = "https://files.pythonhosted.org/packages/ce/76/bbb55d73c93f59453c70519fb4ac5c73203efef25061aaedb3145def3b81/uharfbuzz-0.50.2-cp310-cp310-win_amd64.whl", hash = "sha256:f0563ff4aae0028dc2bec69940454795f126f3b42dd2a789a855f77fba190562", size = 1225070, upload-time = "2025-05-10T20:42:27.746Z" },
- { url = "https://files.pythonhosted.org/packages/7f/5c/6570db333415c32b241d1f704ad2c774f2b32d017f59e82647f65c650bf4/uharfbuzz-0.50.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d792c6ddadc28f75a9a32873971618b723b2689e6a362b4af54fbaef76c6cda4", size = 2940883, upload-time = "2025-05-10T20:42:28.91Z" },
- { url = "https://files.pythonhosted.org/packages/29/9b/7007099d7e62d868b143f96bab8d8f52e0baeded69deb8f1404c5ad99adc/uharfbuzz-0.50.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef3abd02eeed18fca0a85be69a08392e841a415c43f2a12bc487203058180144", size = 1544255, upload-time = "2025-05-10T20:42:30.223Z" },
- { url = "https://files.pythonhosted.org/packages/4c/ac/cd46aac4ee3ed36e098e2d7fc1edc145f7b518caf01732e8c961c009f52b/uharfbuzz-0.50.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f50ea173f7419f0e8bfed47ac99f600ba55bfe08c2622e81f8cfccf966ed5727", size = 1405026, upload-time = "2025-05-10T20:42:31.37Z" },
- { url = "https://files.pythonhosted.org/packages/3c/b3/618ddf1fa0a67fb704d7fa89be76e8c6c91b1e0c7947164ecfcbb2ca88f2/uharfbuzz-0.50.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15a3ac92d5fca62f3f2b2770df815c19662fd42234443554ff27f96ec4900fe7", size = 15179057, upload-time = "2025-05-10T20:42:32.84Z" },
- { url = "https://files.pythonhosted.org/packages/d0/54/42a2785cffc0ff4ef9fb43c51e850b8fe6fdcb72a2f09c2da9cd394a5d7a/uharfbuzz-0.50.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3771162eb34fbd657743979d62376715eb993f3d428523ee7abd3b5bc8b7fed", size = 15323199, upload-time = "2025-05-10T20:42:35.278Z" },
- { url = "https://files.pythonhosted.org/packages/b4/9f/e8594123d075bc0686e2e40f0d390efba2c13d43df4a4ca53bc8c5c1a777/uharfbuzz-0.50.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ba74d1d7c464bab56439ee0511d842190568f89d5f309c0331341a450be56b18", size = 15774103, upload-time = "2025-05-10T20:42:37.201Z" },
- { url = "https://files.pythonhosted.org/packages/94/a7/df5158abef058588fb245b6802db0190e0a669e82b5ae12dc4d0cac7aaa6/uharfbuzz-0.50.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eee5cecf7007db8f34dfcbc881b729bd6a55b3c6cf4289d498366374550138e3", size = 16150293, upload-time = "2025-05-10T20:42:43.825Z" },
- { url = "https://files.pythonhosted.org/packages/73/20/d9f2d8db728179ed919d845df78972b28eec0087f3fdda4afca01167f5ce/uharfbuzz-0.50.2-cp311-cp311-win32.whl", hash = "sha256:afd50dc9ce3020b656c4f14595d28908ccb9e2383420b6ae3a3819203a28a367", size = 979754, upload-time = "2025-05-10T20:42:46.223Z" },
- { url = "https://files.pythonhosted.org/packages/ac/24/68333abda1908282f11d76c4ba2e3ff25df6f4d1622644d2fb67c59aca71/uharfbuzz-0.50.2-cp311-cp311-win_amd64.whl", hash = "sha256:530f62171cf39859fc47e5ff591e6bf34d4a401581333e052e67521e02db1b60", size = 1224859, upload-time = "2025-05-10T20:42:47.412Z" },
- { url = "https://files.pythonhosted.org/packages/87/a4/4e06c8d116daff0472e6d592b6d13ee380efa57b8f3f383d92548dab22e5/uharfbuzz-0.50.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e601618bb7e11eadc65c2092bd89d35daba50cf78fed51e6c0f046e7ddcaec66", size = 2931027, upload-time = "2025-05-10T20:42:48.63Z" },
- { url = "https://files.pythonhosted.org/packages/40/28/648d113b9882d5ee9540e4d533506c4600e468c65c0fa3eccdd783a22453/uharfbuzz-0.50.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d37c2559e1a77c46b9516ea33e41471e7ecb68132f9d43dec95662f342c0ef0c", size = 1542874, upload-time = "2025-05-10T20:42:49.822Z" },
- { url = "https://files.pythonhosted.org/packages/1d/80/19e0e857733a7fad2091da97d5e9a75af9dd744e5cd1cc8cb63647ffdbd2/uharfbuzz-0.50.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e8c40baa11da4c6274343c96695d87b54e6d43537c630c74b3e9d6b7ef9a5067", size = 1397217, upload-time = "2025-05-10T20:42:50.984Z" },
- { url = "https://files.pythonhosted.org/packages/ae/d1/b0e2712d341c84a0e968d9ffadf4a5e0846921211d303786d01c03021d12/uharfbuzz-0.50.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8676d6ed2fe3ca6dd8e9b6541b57a2e341a17add7e5dee125b13b140fd97c846", size = 15129918, upload-time = "2025-05-10T20:42:52.316Z" },
- { url = "https://files.pythonhosted.org/packages/50/98/d1a44f630118f2d92ca1bf06225d4e17a98c4fa0a6e309b2c6caa5137b68/uharfbuzz-0.50.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:303a6fa44fe040bf9b2d21174d9d858e18dcc96f1a03193334e4c3bb9b2fdf1e", size = 15283144, upload-time = "2025-05-10T20:42:54.808Z" },
- { url = "https://files.pythonhosted.org/packages/8d/14/14e5c5fd9dbe4fc5114a79f8b2130307897d85ac813a5ea69158e8cf996b/uharfbuzz-0.50.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ca6669a69f3ee58fb96454a56542f6b7f145b0ffcdeb1e923452acb7b799893d", size = 15763775, upload-time = "2025-05-10T20:42:57.226Z" },
- { url = "https://files.pythonhosted.org/packages/13/67/e2c510f2d73d52130b2ce44f39e49611d199bb1e31373e5453bb251e09b6/uharfbuzz-0.50.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4810d14924821d1d503facf1f2677dc3504d681d1ffd421dbce59ca4991ea913", size = 16173691, upload-time = "2025-05-10T20:42:59.647Z" },
- { url = "https://files.pythonhosted.org/packages/b6/47/6543c706e2e0672fda071e4b8e9a06bf290cccc25b4191545d4172bf83d4/uharfbuzz-0.50.2-cp312-cp312-win32.whl", hash = "sha256:4eef6b615b0f7412890cdadf69f5067bedfebd9f09fb323bad749f733c7af17b", size = 978602, upload-time = "2025-05-10T20:43:01.617Z" },
- { url = "https://files.pythonhosted.org/packages/67/5c/d725e37f4b0aed18d0cfe5488ab049641f020ccb07bd0b647715b4eeeef9/uharfbuzz-0.50.2-cp312-cp312-win_amd64.whl", hash = "sha256:2dcf5bc74099e9a7fbc2804cdc51d8475d6641af75e33adcd7c0171af6587cd7", size = 1226795, upload-time = "2025-05-10T20:43:03.123Z" },
- { url = "https://files.pythonhosted.org/packages/71/58/36cbf3db229bd01e10831aa7458dfcb94e945f86389ac5b85d6f2bc7b787/uharfbuzz-0.50.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:193c0e67cad91310c347958ea96aaf8bd8f35172775bf503d529f70c08502d3e", size = 2926998, upload-time = "2025-05-10T20:43:04.418Z" },
- { url = "https://files.pythonhosted.org/packages/fd/b8/d6c63cb470739cf0a326f90352dbdffcb94cb6b8cb05dedce8a8f969aba4/uharfbuzz-0.50.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d9430616fc5236b086da0473856bfbea047c361b4e96457486e21de05ba7666", size = 1540765, upload-time = "2025-05-10T20:43:06.221Z" },
- { url = "https://files.pythonhosted.org/packages/20/b5/8fbb2e8c886b173ea64141b3bff446c521a31cce42320bd99a4a0da2cd51/uharfbuzz-0.50.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:841bc4af29ebb3b60df12b402640dc274323a6fe92ad389ec8fa41e2efcbb717", size = 1394817, upload-time = "2025-05-10T20:43:07.387Z" },
- { url = "https://files.pythonhosted.org/packages/fc/02/2f6141d5a26df3869caf0ef59894914b79706a5e0e7011891d4da46bd48a/uharfbuzz-0.50.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06846e730d7d3da057100a8975289ebeff04857ecc23e8a8eba26dd052b3b0a7", size = 15160948, upload-time = "2025-05-10T20:43:08.803Z" },
- { url = "https://files.pythonhosted.org/packages/e5/5b/2b426826b1902d54e8388bfc368b2a2478d8e09426cb7a3dd6df589c1124/uharfbuzz-0.50.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14b6b01714af0ba3ecfc25ccba552327ef300b8d9adb10df797dc1a8df43d06a", size = 15283194, upload-time = "2025-05-10T20:43:10.774Z" },
- { url = "https://files.pythonhosted.org/packages/ca/5f/5fbb7b127262e73d4c33b02c024b204b9cf1c5e3e5ddbb4c822d313379ef/uharfbuzz-0.50.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3fea789226edd6e6bea9b962c12f2e8ef110013504a54a64ea848863675ba2f6", size = 15759822, upload-time = "2025-05-10T20:43:12.753Z" },
- { url = "https://files.pythonhosted.org/packages/13/9e/eadd74bfe036ebcfe28db41646f8e2c462026acc35725f3cd2eca8a744b6/uharfbuzz-0.50.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:659df009e176699cb2e04b37c70f50820562a948df224ba61d90e7d812f9cbe1", size = 16173575, upload-time = "2025-05-10T20:43:14.737Z" },
- { url = "https://files.pythonhosted.org/packages/c2/de/88697c7d7a3c2e7f69a375a7e5db1a0672eb080a0baef07d23b8d1218533/uharfbuzz-0.50.2-cp313-cp313-win32.whl", hash = "sha256:b6b66d914dab039f9e09166649a7dfd7528c687c06777f570a0ab38a6b658fd8", size = 977128, upload-time = "2025-05-10T20:43:16.526Z" },
- { url = "https://files.pythonhosted.org/packages/e2/2d/d89e9f180af834b2831cc0545f1d1f72ed22e8673f97741ea9b3bbb79de9/uharfbuzz-0.50.2-cp313-cp313-win_amd64.whl", hash = "sha256:20a510557e4c26d3c89dfda00ecf9d757694ad796b8b6332ae4848c8599464ae", size = 1226416, upload-time = "2025-05-10T20:43:17.712Z" },
- { url = "https://files.pythonhosted.org/packages/f8/0c/a5d25f6321aa6c86071362c6dbffecd8c470986508454c1053ead7730c5a/uharfbuzz-0.50.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:452a21f80657a72d31a036338fa80b0a884130088974be7a45a569a86f20f47f", size = 1300219, upload-time = "2025-05-10T20:43:53.451Z" },
- { url = "https://files.pythonhosted.org/packages/c2/c3/c386276f939921358645039a1cbb5efdd99f3290a608fc3f234f161dd5f7/uharfbuzz-0.50.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5d6d2a2d2cc81192c5d67c6cfd1cfbd253c8b7770771d33e3fe4ff5b508b8bfd", size = 1190273, upload-time = "2025-05-10T20:43:54.659Z" },
- { url = "https://files.pythonhosted.org/packages/83/9b/2e023f94deb91ebdda89cb2943f35ee396952ce09ad68ff4e3314cd3cec9/uharfbuzz-0.50.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c198065c7009e67c4bb4fd7aa174e471a37a77d9b30cde1e1e6b3d7c1c429fa2", size = 1418898, upload-time = "2025-05-10T20:43:55.958Z" },
- { url = "https://files.pythonhosted.org/packages/fc/cf/b81852558ef7c1226cfba667e87de598ad0154eb7070044c261413311980/uharfbuzz-0.50.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74373e59195b881c3b921284ddf7fc12c42042aaab99c666efd3e70e6fea0c3a", size = 1452244, upload-time = "2025-05-10T20:43:57.539Z" },
- { url = "https://files.pythonhosted.org/packages/18/95/5dfbf4f888922b5289520706ee63004b2d202814343be680c31f1ba14a06/uharfbuzz-0.50.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:27fdd88bc9b10b52b48bc632604e5acf713d33e464d8375645fa4e4ba4f6ea94", size = 1216599, upload-time = "2025-05-10T20:43:58.755Z" },
- { url = "https://files.pythonhosted.org/packages/37/a7/78599d7afe8c97ed282d13a9739decbc9f876bb01cbfb81b0e5747a7016c/uharfbuzz-0.50.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:47230968855c5080ff1fc5cf7cd128be2876f5f43adcb9022353abc69b3a6736", size = 1308722, upload-time = "2025-05-10T20:44:00.501Z" },
- { url = "https://files.pythonhosted.org/packages/38/01/bc7e50caacfec10011b189dff73b7f7dab78835a4b2aaa18b8e99f384f56/uharfbuzz-0.50.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:58f92b750f8823bb84922659ad6a4a4295ae3966e40403018896e78fb48900c0", size = 1195199, upload-time = "2025-05-10T20:44:01.848Z" },
- { url = "https://files.pythonhosted.org/packages/51/d6/3c610aeb809b226344452516f26dc76c2490980f03285a6d478687e42361/uharfbuzz-0.50.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5117b70ed72de248c8af391fbb0ea1b853d359d11885f07e3416e17818d7da2c", size = 1428326, upload-time = "2025-05-10T20:44:03.667Z" },
- { url = "https://files.pythonhosted.org/packages/16/e4/230fd0a13f2e32b24a1715a6693ae7828cef649bb043d28435e4b8355aef/uharfbuzz-0.50.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7521f71e48aeedf2cf64f6e0c8b08a98160178cc39b8c125854c01fef9f5f48a", size = 1460014, upload-time = "2025-05-10T20:44:05.03Z" },
- { url = "https://files.pythonhosted.org/packages/0d/a6/f65488843fc440e877d2bc96998daf51e9305d2008f77e06a7403a4cc4e3/uharfbuzz-0.50.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cf4e406836f433ce109c6263e149d48d374ec2aa6ec57100b3c7316ba0bf5580", size = 1216324, upload-time = "2025-05-10T20:44:06.42Z" },
-]
-
-[[package]]
-name = "urllib3"
-version = "2.5.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
-]
-
-[[package]]
-name = "uv"
-version = "0.7.20"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/78/97/1ff10c82d3f0b4246c3a94c09ab4b40d0f7d6dfaafb352175d169ef357e5/uv-0.7.20.tar.gz", hash = "sha256:6adf2ad333e8da133eecbdd2bdb4e8dfb6d4b2db2c3b4739b6705aa347c997ee", size = 3365382, upload-time = "2025-07-09T21:02:17.822Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/94/dc/640875ba8ab0b73401cf0cc72c96a568a817a55c71e939f3a93c5911d0ba/uv-0.7.20-py3-none-linux_armv6l.whl", hash = "sha256:9e59b3b0c62255ac87f3fd5b0c58133187983cac57ab86e127cde1b8a2ee32ff", size = 17685309, upload-time = "2025-07-09T21:01:33.49Z" },
- { url = "https://files.pythonhosted.org/packages/83/9a/ee440ac67678fad39c087d0494c1e84103cc1ff9bfb88c91b71c7fd5dea3/uv-0.7.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f4c7df0f4dfca809b403fb047ee23b3a35e1221df7be9ade8bbd4fb379f50dc2", size = 17725867, upload-time = "2025-07-09T21:01:37.172Z" },
- { url = "https://files.pythonhosted.org/packages/de/d0/5bcf679907e6d4fb864e4e30b573060734cc1c26afb38b355dac003ce452/uv-0.7.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b8e636777e0ed816461e73ac85445aedb01c3380a61d3f66fa59423582a7456a", size = 16413199, upload-time = "2025-07-09T21:01:39.774Z" },
- { url = "https://files.pythonhosted.org/packages/7c/83/0dbe7a1983bb6232ec51afb3bbba11721a31afcc731c56ce898dc91f6541/uv-0.7.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:9b6b95ccc34649c34a05821e3eb8dbc851ee14011e1ddc39b507460b8407a024", size = 16981463, upload-time = "2025-07-09T21:01:42.101Z" },
- { url = "https://files.pythonhosted.org/packages/5c/4f/502c5e0cac26bb36413abc99ab8d4d136f73864c4ec5fe7aee4cc170c5e5/uv-0.7.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d606d70cf79cd7f4bf8b940d331c863b33ac59266fa7dc8da2852187d1494334", size = 17381910, upload-time = "2025-07-09T21:01:44.882Z" },
- { url = "https://files.pythonhosted.org/packages/48/3c/13ce07214c41790d2fd99c839b1dedfcf2d4c5b6a9696e9822ef9a6014f5/uv-0.7.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:086918380296feea5d49abd82b80a324c2a6401e098db050b8338f6ca7a75e79", size = 18086428, upload-time = "2025-07-09T21:01:47.532Z" },
- { url = "https://files.pythonhosted.org/packages/84/26/5313099e0214087910fb09d14e9acb516db941d2f4fa67a8d983f5295952/uv-0.7.20-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:914dbd8e8a83303f6108cead85e4b83ea748b9cbb8cb03df030c4952b67f40fd", size = 19309012, upload-time = "2025-07-09T21:01:49.996Z" },
- { url = "https://files.pythonhosted.org/packages/6a/de/cf7fe214e420f8fc1b9eb7a09cca5bb3c05663fec73d6750613c9f68bc26/uv-0.7.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:20bfb4a8f42449c0ec7d4b0f1cc91e5a6713e5c55e8ec9b9de9628e21b4db74c", size = 18984850, upload-time = "2025-07-09T21:01:52.436Z" },
- { url = "https://files.pythonhosted.org/packages/57/cc/356c927be05e1dd725b0cc5b0d0d50eda724e2e22f610915b235ad40e559/uv-0.7.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96ad43a3fffbc97b5da90d221788d3fd5c086ec9b1dbdea89a5107a2b5d46fa0", size = 18566669, upload-time = "2025-07-09T21:01:54.741Z" },
- { url = "https://files.pythonhosted.org/packages/09/82/a9f8f31434ae10fc7c81a09cb562d08544a195db48bbf062702bdacbb2c8/uv-0.7.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f25ad1ca8cd756266797a14d153c74ff1d6c7705a63b5036ac5c51c63b6870b", size = 18416500, upload-time = "2025-07-09T21:01:57.699Z" },
- { url = "https://files.pythonhosted.org/packages/f9/fd/ea803971d83b3238d62859fc8cd62daa69fa4464eb669e21712bbf91f59e/uv-0.7.20-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:d50f2ce3e9d754dfef0b761a3dcc0cc60d045f525894a8b5d76641d9ccd0d257", size = 17229164, upload-time = "2025-07-09T21:02:00.254Z" },
- { url = "https://files.pythonhosted.org/packages/ac/c6/b3e38a77c759888584ee39fd3aebaca8c03fbe890cd1cb1d794cd628605e/uv-0.7.20-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:1d5a095a9ab9b5424cb5f6de75969402a3e0b3d40e04005e3379ebc5f493a582", size = 17327269, upload-time = "2025-07-09T21:02:02.82Z" },
- { url = "https://files.pythonhosted.org/packages/4b/10/422a537d2e983ac55c493d9d9d3fe33ad37784cbac2f4ef6bcb4b5c45200/uv-0.7.20-py3-none-musllinux_1_1_i686.whl", hash = "sha256:baa286b2f847edbb13f3f7baf01ebca73e4dad5b70900270abb20639f03d9770", size = 17585820, upload-time = "2025-07-09T21:02:05.202Z" },
- { url = "https://files.pythonhosted.org/packages/fd/77/696b2795f18ce3cc0bf4a1192564402f8b16c9e1e7b6c7061d1059c52a52/uv-0.7.20-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:67cf45da498955f46208d28c1a5fa58550553defc3f747156247335d65c5b4c7", size = 18557027, upload-time = "2025-07-09T21:02:07.593Z" },
- { url = "https://files.pythonhosted.org/packages/15/d3/6d5a2cb1cf0ed48442910c5ed0f1fdc984c66189107b42c85bb53f421332/uv-0.7.20-py3-none-win32.whl", hash = "sha256:246d45e7eb5934ffc23351c4f1d6e7385da21f63929e83d18855d901fd6f5ed4", size = 17609793, upload-time = "2025-07-09T21:02:10.198Z" },
- { url = "https://files.pythonhosted.org/packages/1f/6f/9412b857d5c311b57eaf40acdbc612524ac6caac2221303adcebca9a1875/uv-0.7.20-py3-none-win_amd64.whl", hash = "sha256:85bbdd6b40dc6f78c1c60a7b5c3c1dc992acdc7160c99801d1d4a4766dd42a4f", size = 19424736, upload-time = "2025-07-09T21:02:13.206Z" },
- { url = "https://files.pythonhosted.org/packages/df/fb/e23895a4d5980450d26602b1f4887ce67ccc07f21e943f348bd519c6596f/uv-0.7.20-py3-none-win_arm64.whl", hash = "sha256:693ad1f9ecb87f1ddc735682d6d96fcff41a4aa90ae663c57252c7a8e57d4459", size = 17976062, upload-time = "2025-07-09T21:02:15.818Z" },
-]
-
-[[package]]
-name = "verspec"
-version = "0.1.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e7/44/8126f9f0c44319b2efc65feaad589cadef4d77ece200ae3c9133d58464d0/verspec-0.1.0.tar.gz", hash = "sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e", size = 27123, upload-time = "2020-11-30T02:24:09.646Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31", size = 19640, upload-time = "2020-11-30T02:24:08.387Z" },
-]
-
-[[package]]
-name = "virtualenv"
-version = "20.31.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "distlib" },
- { name = "filelock" },
- { name = "platformdirs" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c3a104fd0c495184c4f2336d65baf398e3c75d72ea94/virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af", size = 6076316, upload-time = "2025-05-08T17:58:23.811Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" },
-]
-
-[[package]]
-name = "vispy"
-version = "0.15.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "freetype-py" },
- { name = "hsluv" },
- { name = "kiwisolver" },
- { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "numpy", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
- { name = "packaging" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/1b/2e/ef2697f963111cf1bc83568bbe55f262b7c7c8a72948a6e802a7c236f2c1/vispy-0.15.2.tar.gz", hash = "sha256:d52d10c0697f48990555cea2a2bad3f9f5a772391856fda364ea4bbc69fd075c", size = 2513383, upload-time = "2025-05-19T13:26:41.015Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/21/2b/a483bf80575e047173d55f51115c38f9c43962cfd3247a861ce033913bee/vispy-0.15.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6bc8a49f4e0b27e19be0da318877666d733e1afc7231407e635f948a8aabb095", size = 1478710, upload-time = "2025-05-19T13:26:00.997Z" },
- { url = "https://files.pythonhosted.org/packages/b6/bc/8c9a3cb402037d6eda521492c04dddf6a00f600c85b650b33342573fe82f/vispy-0.15.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:89a5d51cc980fed81f16373d515086d5b16da66868df8c1f76e71d4dfc17062c", size = 1472024, upload-time = "2025-05-19T13:26:03.055Z" },
- { url = "https://files.pythonhosted.org/packages/58/67/46111a528d63ad5308e4a547484c75e1c9982ec6a3709732ba0bcd343a7a/vispy-0.15.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7cbce48e14e2eeac491688be33b4d422d198515c67a1a87021622790309107e", size = 1845072, upload-time = "2025-05-19T13:26:04.226Z" },
- { url = "https://files.pythonhosted.org/packages/56/91/5c9d739410427c603fd0aa9e6a29b6c3e51edcc7f83f14bf243974808396/vispy-0.15.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8b0b6b55a781b6add58f946b95c368bfa203e7915fda234f4993c4db03d3c0", size = 1844568, upload-time = "2025-05-19T13:26:05.511Z" },
- { url = "https://files.pythonhosted.org/packages/8e/8e/f07f9d7a341b5058a99e0700f7cab7d222b159154a392b335f0e59cbf636/vispy-0.15.2-cp310-cp310-win_amd64.whl", hash = "sha256:0f2639656fd53ee4cbedf3e3caed0cef646f90e7ca4ec777ba18ad247234d8c9", size = 1467701, upload-time = "2025-05-19T13:26:07.193Z" },
- { url = "https://files.pythonhosted.org/packages/3e/e7/3590e46cec1d51a8bba0b2d5442df698aec41ff5757e9e29e04aea3cbe12/vispy-0.15.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4f7adab04c0a90ca0a1155f09ecc425ca44bee713e6b2d4970d85fc010b05ee", size = 1478729, upload-time = "2025-05-19T13:26:08.951Z" },
- { url = "https://files.pythonhosted.org/packages/bb/71/5871fe8d2f612502e5e148224426fb263431870a2f5fa5d2205fb9f2606a/vispy-0.15.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:725ab77b11bb3a4de5145a25b6e06f4555b0b7b1821baf7ffdd7f674f725dab2", size = 1472015, upload-time = "2025-05-19T13:26:10.246Z" },
- { url = "https://files.pythonhosted.org/packages/67/d8/067de34eac9aecf71a3291c54a738161caa9cbf30d62314b0500abff4117/vispy-0.15.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d107b07365bc1bdfa28fe6f8df05004cd73bfd6269230bef26b3862f2016fa64", size = 1872447, upload-time = "2025-05-19T13:26:11.844Z" },
- { url = "https://files.pythonhosted.org/packages/5c/1e/af88bdb15b20a690b3d5409fc89fbdbb79095a593abb8a37094a42428760/vispy-0.15.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c67fb06df462b63ba95c77eb00a3d5b7d53e90f69987b0e216ab67ad7d6d00e", size = 1876232, upload-time = "2025-05-19T13:26:13.094Z" },
- { url = "https://files.pythonhosted.org/packages/f1/4d/236c77644db2427f33330fd6372bac416262fd3de6771c555b969f67bd4f/vispy-0.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:46f8ca742958e19135cfa0f76ef1707666837ae6a9559a3fa3c2a3186967299e", size = 1467601, upload-time = "2025-05-19T13:26:15.199Z" },
- { url = "https://files.pythonhosted.org/packages/6f/7e/31102425ea26bafab27bb5a675b499c57d29e72b1cc25fe3ae8facdd374e/vispy-0.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c8cbc22bc789f238423abadfcc3ba7589b85729be6b179b321720b511012fbee", size = 1478919, upload-time = "2025-05-19T13:26:16.962Z" },
- { url = "https://files.pythonhosted.org/packages/8d/63/a601b8f1e8e418d3821d7b4a465c2eb6695ab8eccadfce886b99ffdfe92a/vispy-0.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bf995b86dcc1eab265fa2bf9a8a5ca5a225b86c3ed4ac63900f0c5e90d5e8100", size = 1471915, upload-time = "2025-05-19T13:26:18.441Z" },
- { url = "https://files.pythonhosted.org/packages/79/1f/ca0deb4a876148c0fa515bf03d03e1dfad4553be8e6f2ab51433c074310b/vispy-0.15.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0aa64fcab1cd1730a10a1c9188e9cef8aefe6d099a46f2f87e2458bcef719918", size = 1866848, upload-time = "2025-05-19T13:26:19.6Z" },
- { url = "https://files.pythonhosted.org/packages/ac/a4/dc6f335e54877f5d26ad4e4aac8f49b2d6e7719dee3e08f363d882c8aba4/vispy-0.15.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9d38228cedd23876cb2359ff568d523a97284d225259d450d5e5e2129e98eb1", size = 1870433, upload-time = "2025-05-19T13:26:20.794Z" },
- { url = "https://files.pythonhosted.org/packages/a8/73/5ffa34d7300c35e8423d51789d594d35eaa7256d210f9909afa9fdd0aa37/vispy-0.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:a84531c1a89f8b9d3992eb0d0ab74f84884f49d1f46a8be3755c809d7507cb01", size = 1468068, upload-time = "2025-05-19T13:26:22.073Z" },
- { url = "https://files.pythonhosted.org/packages/85/9a/6664b7f0d28e0008a28111bae847a1fa3eedd409458bf01ca45a6e99da6f/vispy-0.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ac46aab46e208b919dcf0bb26869bcd083a0a1a4927bcaf41631ba38c247639", size = 1477880, upload-time = "2025-05-19T13:26:23.321Z" },
- { url = "https://files.pythonhosted.org/packages/cf/6f/b9b36f841c5ff7320764f64822e79df3fea8a2c92270cda7f3a634d9a031/vispy-0.15.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7e197a012503850a77d47177964d572edab964b2a8ea0f9d998c35b81325256c", size = 1471035, upload-time = "2025-05-19T13:26:24.554Z" },
- { url = "https://files.pythonhosted.org/packages/4c/af/b892a3c9b4be29755ce4d1fc17ecb8249446bdd0e17bb5b2a9cb39bcf0f4/vispy-0.15.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d9eff397ecdbaf0052baae0563caa9e276272d6dc78fbaab3f5e51dbf5f7f92", size = 1860412, upload-time = "2025-05-19T13:26:26.212Z" },
- { url = "https://files.pythonhosted.org/packages/e8/13/8997d96bdc0a81f331dfd41b368935d79e8ea2917840266567e6dc40d684/vispy-0.15.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f13ee0cb95bbff6d7b235e1a8f96e620eee15ccb6d5ad74902427ab7e56dc8ab", size = 1865860, upload-time = "2025-05-19T13:26:27.928Z" },
- { url = "https://files.pythonhosted.org/packages/6e/37/abb30db1853b69aed4c32813cf312f301ec3641b8846193be9a6d892d607/vispy-0.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:f5955821e9b452980490706648a702da8cb2b82e762b6d6589e5872118301b84", size = 1467882, upload-time = "2025-05-19T13:26:29.608Z" },
-]
-
-[[package]]
-name = "watchdog"
-version = "6.0.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" },
- { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" },
- { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" },
- { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" },
- { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" },
- { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" },
- { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" },
- { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" },
- { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" },
- { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" },
- { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" },
- { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" },
- { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" },
- { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" },
- { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" },
- { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" },
- { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" },
- { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" },
- { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" },
- { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" },
- { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" },
- { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" },
- { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" },
- { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" },
-]
-
-[[package]]
-name = "wcwidth"
-version = "0.2.13"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" },
-]
-
-[[package]]
-name = "wgpu"
-version = "0.22.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "cffi" },
- { name = "rubicon-objc", marker = "sys_platform == 'darwin'" },
- { name = "sniffio" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/2a/66/1a96346cbcb7e2a2e6c94e80d343ade3d1c464c37f3ff3b3cc362f4b6d4a/wgpu-0.22.2.tar.gz", hash = "sha256:118ef8790b34b36b9b47c6a25f74ca334dd9c94508549d40bc81de712ea10ab2", size = 166882, upload-time = "2025-05-19T09:35:56.148Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7f/ff/05b778049c86c47e4c890f6177f72cb95f096f828fa87a036b0f2a86cbe0/wgpu-0.22.2-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:fa8829194bfbbb41b1cbd38f09bffe44de6d6ba5305b30b029911cc7ce815179", size = 2549722, upload-time = "2025-05-19T09:35:46.395Z" },
- { url = "https://files.pythonhosted.org/packages/e4/c5/8b67882a5ab62b24c4dfd25e2c29f57a53d8f99148c59a754c4b4751f2e8/wgpu-0.22.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c241478643a4c650c2eefeeb2cd99e86de4b73d602e06a2760aae5043f98cf17", size = 2468167, upload-time = "2025-05-19T09:35:48.419Z" },
- { url = "https://files.pythonhosted.org/packages/cd/b3/ca0c717e33d8ee33c13661384e32c2a9d16867d00326c9a85ddc346b0951/wgpu-0.22.2-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:5c70633804e6b65e43855705097abd5c87ee98c1c44936328c148bd0492ae63e", size = 3127327, upload-time = "2025-05-19T09:35:49.561Z" },
- { url = "https://files.pythonhosted.org/packages/f7/f1/2b8e72ae8cde34c05f97c53a8f1f5f1f1bbe80c285b67a5dccc2516ccdbf/wgpu-0.22.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:eebb136a7cb8edfc7159bfdab45f4740f921c7524a268dee0e583fb1378b41f7", size = 3174896, upload-time = "2025-05-19T09:35:50.742Z" },
- { url = "https://files.pythonhosted.org/packages/7d/2c/593b21dd4ef0dd6e44d1d150919a3683c0aff3747c2ad718a5051f4ff551/wgpu-0.22.2-py3-none-win32.whl", hash = "sha256:2a0015552b2c697a967b48d9d102f3f9b3f332faa61b16128e611d0f83da6f8d", size = 2783822, upload-time = "2025-05-19T09:35:52.372Z" },
- { url = "https://files.pythonhosted.org/packages/3f/de/d966bd1422d31c0bbc69793dbc8fe4a30cc383d38d5057b15eeec23194e3/wgpu-0.22.2-py3-none-win_amd64.whl", hash = "sha256:dab58b769b166337dc4cbc14a19120111f0f5879cb10c81f507206b2f89ed29e", size = 2961418, upload-time = "2025-05-19T09:35:53.898Z" },
- { url = "https://files.pythonhosted.org/packages/9f/c8/07c7469054b49148884dcb4421dcf2b9b6f9aafd26a709aae6f0cddb6598/wgpu-0.22.2-py3-none-win_arm64.whl", hash = "sha256:ea1649d99dc16148ba4564b539bff609199911edc55c76cd87f72623a8727d3e", size = 2803157, upload-time = "2025-05-19T09:35:55.041Z" },
-]
-
-[[package]]
-name = "zipp"
-version = "3.23.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
-]