plexus-python 0.5.2__tar.gz → 0.6.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. {plexus_python-0.5.2 → plexus_python-0.6.1}/.gitignore +6 -0
  2. {plexus_python-0.5.2 → plexus_python-0.6.1}/API.md +1 -1
  3. {plexus_python-0.5.2 → plexus_python-0.6.1}/CHANGELOG.md +15 -0
  4. {plexus_python-0.5.2 → plexus_python-0.6.1}/PKG-INFO +6 -1
  5. {plexus_python-0.5.2 → plexus_python-0.6.1}/SECURITY.md +2 -2
  6. plexus_python-0.6.1/examples/thermal_camera.py +88 -0
  7. plexus_python-0.6.1/plexus/cameras/__init__.py +25 -0
  8. plexus_python-0.6.1/plexus/cameras/thermal.py +388 -0
  9. {plexus_python-0.5.2 → plexus_python-0.6.1}/plexus/client.py +45 -0
  10. {plexus_python-0.5.2 → plexus_python-0.6.1}/plexus/ws.py +7 -0
  11. {plexus_python-0.5.2 → plexus_python-0.6.1}/pyproject.toml +7 -2
  12. plexus_python-0.6.1/tests/test_thermal.py +153 -0
  13. {plexus_python-0.5.2 → plexus_python-0.6.1}/uv.lock +185 -3
  14. {plexus_python-0.5.2 → plexus_python-0.6.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  15. {plexus_python-0.5.2 → plexus_python-0.6.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  16. {plexus_python-0.5.2 → plexus_python-0.6.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  17. {plexus_python-0.5.2 → plexus_python-0.6.1}/.github/workflows/ci.yml +0 -0
  18. {plexus_python-0.5.2 → plexus_python-0.6.1}/.github/workflows/publish.yml +0 -0
  19. {plexus_python-0.5.2 → plexus_python-0.6.1}/AGENTS.md +0 -0
  20. {plexus_python-0.5.2 → plexus_python-0.6.1}/CODE_OF_CONDUCT.md +0 -0
  21. {plexus_python-0.5.2 → plexus_python-0.6.1}/CONTRIBUTING.md +0 -0
  22. {plexus_python-0.5.2 → plexus_python-0.6.1}/LICENSE +0 -0
  23. {plexus_python-0.5.2 → plexus_python-0.6.1}/README.md +0 -0
  24. {plexus_python-0.5.2 → plexus_python-0.6.1}/examples/.python-version +0 -0
  25. {plexus_python-0.5.2 → plexus_python-0.6.1}/examples/README.md +0 -0
  26. {plexus_python-0.5.2 → plexus_python-0.6.1}/examples/basic.py +0 -0
  27. {plexus_python-0.5.2 → plexus_python-0.6.1}/examples/can.py +0 -0
  28. {plexus_python-0.5.2 → plexus_python-0.6.1}/examples/i2c_bme280.py +0 -0
  29. {plexus_python-0.5.2 → plexus_python-0.6.1}/examples/mac_metrics.py +0 -0
  30. {plexus_python-0.5.2 → plexus_python-0.6.1}/examples/mavlink.py +0 -0
  31. {plexus_python-0.5.2 → plexus_python-0.6.1}/examples/mqtt.py +0 -0
  32. {plexus_python-0.5.2 → plexus_python-0.6.1}/examples/pyproject.toml +0 -0
  33. {plexus_python-0.5.2 → plexus_python-0.6.1}/examples/uv.lock +0 -0
  34. {plexus_python-0.5.2 → plexus_python-0.6.1}/plexus/__init__.py +0 -0
  35. {plexus_python-0.5.2 → plexus_python-0.6.1}/plexus/_log.py +0 -0
  36. {plexus_python-0.5.2 → plexus_python-0.6.1}/plexus/buffer.py +0 -0
  37. {plexus_python-0.5.2 → plexus_python-0.6.1}/plexus/cli.py +0 -0
  38. {plexus_python-0.5.2 → plexus_python-0.6.1}/plexus/config.py +0 -0
  39. {plexus_python-0.5.2 → plexus_python-0.6.1}/scripts/plexus.service +0 -0
  40. {plexus_python-0.5.2 → plexus_python-0.6.1}/scripts/release.sh +0 -0
  41. {plexus_python-0.5.2 → plexus_python-0.6.1}/scripts/scan_buses.py +0 -0
  42. {plexus_python-0.5.2 → plexus_python-0.6.1}/scripts/setup.sh +0 -0
  43. {plexus_python-0.5.2 → plexus_python-0.6.1}/skills/plexus/SKILL.md +0 -0
  44. {plexus_python-0.5.2 → plexus_python-0.6.1}/skills/plexus/references/api.md +0 -0
  45. {plexus_python-0.5.2 → plexus_python-0.6.1}/skills/plexus/references/sdk.md +0 -0
  46. {plexus_python-0.5.2 → plexus_python-0.6.1}/tests/test_basic.py +0 -0
  47. {plexus_python-0.5.2 → plexus_python-0.6.1}/tests/test_buffer.py +0 -0
  48. {plexus_python-0.5.2 → plexus_python-0.6.1}/tests/test_config.py +0 -0
  49. {plexus_python-0.5.2 → plexus_python-0.6.1}/tests/test_retry.py +0 -0
  50. {plexus_python-0.5.2 → plexus_python-0.6.1}/tests/test_video.py +0 -0
  51. {plexus_python-0.5.2 → plexus_python-0.6.1}/tests/test_ws.py +0 -0
@@ -52,3 +52,9 @@ htmlcov/
52
52
  # Local config
53
53
  .env
54
54
  .env.local
55
+
56
+ # macOS
57
+ .DS_Store
58
+
59
+ # Logs
60
+ *.log
@@ -145,7 +145,7 @@ For real-time UI-controlled streaming, devices connect via WebSocket.
145
145
 
146
146
  ### Connection Flow
147
147
 
148
- 1. Device connects to PartyKit server
148
+ 1. Device connects to the gateway
149
149
  2. Device authenticates with API key
150
150
  3. Device reports available sensors
151
151
  4. Dashboard controls streaming via messages
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.1] - 2026-05-28 - Thermal camera streaming
4
+
5
+ ### Added
6
+
7
+ - `plexus.cameras.thermal` module with hardware-agnostic drivers for I2C sensors
8
+ (MLX90640, MLX90641) and USB cameras (Y16 pixel format). All drivers return a
9
+ unified `ThermalFrame` containing a colorized JPEG plus temperature metadata
10
+ (min, max, mean °C).
11
+ - `ThermalSource.open()` auto-detects connected hardware; pass `"sim"` for a
12
+ simulated source with no physical device required.
13
+ - `client.send_thermal_frame()` sends thermal frames over the existing WebSocket
14
+ connection. Small sensors (≤4096 pixels, e.g. 32×24 MLX90640) include the full
15
+ temperature array inline; larger USB sensors omit it to keep frame sizes manageable.
16
+ - `examples/thermal_camera.py` — end-to-end example for streaming a thermal camera.
17
+
3
18
  ## [0.5.2] - 2026-05-21 - DX hardening for hardware engineers
4
19
 
5
20
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plexus-python
3
- Version: 0.5.2
3
+ Version: 0.6.1
4
4
  Summary: Thin Python SDK for Plexus — send telemetry in one line
5
5
  Project-URL: Homepage, https://plexus.dev
6
6
  Project-URL: Documentation, https://docs.plexus.dev
@@ -23,10 +23,15 @@ Classifier: Topic :: System :: Hardware
23
23
  Requires-Python: >=3.10
24
24
  Requires-Dist: websocket-client>=1.7
25
25
  Provides-Extra: dev
26
+ Requires-Dist: numpy>=1.24; extra == 'dev'
27
+ Requires-Dist: opencv-python>=4.8; extra == 'dev'
26
28
  Requires-Dist: pytest-cov; extra == 'dev'
27
29
  Requires-Dist: pytest>=9.0.3; extra == 'dev'
28
30
  Requires-Dist: ruff; extra == 'dev'
29
31
  Requires-Dist: websockets>=12; extra == 'dev'
32
+ Provides-Extra: thermal
33
+ Requires-Dist: numpy>=1.24; extra == 'thermal'
34
+ Requires-Dist: opencv-python>=4.8; extra == 'thermal'
30
35
  Provides-Extra: video
31
36
  Requires-Dist: pillow>=12.2.0; extra == 'video'
32
37
  Description-Content-Type: text/markdown
@@ -23,8 +23,8 @@ Instead, email **support@plexus.company** with:
23
23
 
24
24
  | Version | Supported |
25
25
  | ------- | --------- |
26
- | 0.9.x | Yes |
27
- | < 0.9 | No |
26
+ | 0.5.x | Yes |
27
+ | < 0.5 | No |
28
28
 
29
29
  ## Disclosure Policy
30
30
 
@@ -0,0 +1,88 @@
1
+ """
2
+ Thermal camera streaming to Plexus.
3
+
4
+ Reads from any supported thermal camera and sends frames to Plexus using
5
+ the SDK's send_thermal_frame() method. The gateway relays colorized JPEG
6
+ frames to the app, along with temperature range and (for small sensors)
7
+ per-pixel temperature data.
8
+
9
+ Supported hardware (--camera argument):
10
+ sim Simulated camera — no hardware needed (default)
11
+ mlx90640 MLX90640 32×24 I2C sensor (pip install adafruit-circuitpython-mlx90640)
12
+ mlx90641 MLX90641 16×12 I2C sensor (pip install adafruit-circuitpython-mlx90641)
13
+ usb USB thermal camera in Y16 format (InfiRay, Topdon, Seek, etc.)
14
+
15
+ Run:
16
+ export PLEXUS_API_KEY=plx_xxx
17
+ python thermal_camera.py
18
+ python thermal_camera.py --camera mlx90640
19
+ python thermal_camera.py --camera usb --device 2
20
+ """
21
+
22
+ import argparse
23
+ import sys
24
+ import time
25
+
26
+ from plexus import Plexus
27
+ from plexus.cameras.thermal import NoCameraFound, ThermalSource
28
+
29
+ CAMERA_ID = "thermal"
30
+ FPS = 5
31
+
32
+
33
+ def main() -> None:
34
+ parser = argparse.ArgumentParser(description="Stream thermal camera to Plexus.")
35
+ parser.add_argument(
36
+ "--camera",
37
+ choices=["sim", "mlx90640", "mlx90641", "usb"],
38
+ default=None,
39
+ help="Camera driver (default: auto-detect)",
40
+ )
41
+ parser.add_argument(
42
+ "--device",
43
+ type=int,
44
+ default=0,
45
+ help="USB video device index (default: 0)",
46
+ )
47
+ args = parser.parse_args()
48
+
49
+ hint = args.device if args.camera == "usb" else args.camera
50
+ try:
51
+ cam = ThermalSource.open(hint)
52
+ except NoCameraFound as e:
53
+ print(f"Error: {e}", file=sys.stderr)
54
+ sys.exit(1)
55
+
56
+ px = Plexus(transport="ws")
57
+ px.wait_connected()
58
+
59
+ interval = 1.0 / FPS
60
+ frame_count = 0
61
+ print(f"Streaming {cam.width}×{cam.height} thermal at {FPS} fps — Ctrl-C to stop")
62
+
63
+ try:
64
+ while True:
65
+ t0 = time.time()
66
+
67
+ temps = cam.read_frame()
68
+ px.send_thermal_frame(temps, camera_id=CAMERA_ID)
69
+
70
+ frame_count += 1
71
+ if frame_count % 50 == 0:
72
+ print(f" {frame_count} frames sent")
73
+
74
+ elapsed = time.time() - t0
75
+ wait = interval - elapsed
76
+ if wait > 0:
77
+ time.sleep(wait)
78
+
79
+ except KeyboardInterrupt:
80
+ pass
81
+ finally:
82
+ cam.close()
83
+ px.stop()
84
+ print(f"Done. {frame_count} frames sent.")
85
+
86
+
87
+ if __name__ == "__main__":
88
+ main()
@@ -0,0 +1,25 @@
1
+ from plexus.cameras.thermal import (
2
+ NoCameraFound,
3
+ MLX90640Camera,
4
+ MLX90641Camera,
5
+ SimulatedThermalCamera,
6
+ ThermalCamera,
7
+ ThermalFrame,
8
+ ThermalSource,
9
+ USBThermalCamera,
10
+ build_thermal_frame,
11
+ encode_frame,
12
+ )
13
+
14
+ __all__ = [
15
+ "NoCameraFound",
16
+ "MLX90640Camera",
17
+ "MLX90641Camera",
18
+ "SimulatedThermalCamera",
19
+ "ThermalCamera",
20
+ "ThermalFrame",
21
+ "ThermalSource",
22
+ "USBThermalCamera",
23
+ "build_thermal_frame",
24
+ "encode_frame",
25
+ ]
@@ -0,0 +1,388 @@
1
+ """
2
+ Thermal camera drivers for Plexus.
3
+
4
+ Provides a hardware-agnostic interface over I2C sensors (MLX90640, MLX90641)
5
+ and USB thermal cameras (Y16 pixel format). All drivers return a unified
6
+ ThermalFrame containing a colorized JPEG image plus temperature metadata.
7
+
8
+ Usage:
9
+ from plexus.cameras.thermal import ThermalSource
10
+
11
+ cam = ThermalSource.open() # auto-detect
12
+ cam = ThermalSource.open("sim") # simulated, no hardware
13
+
14
+ while True:
15
+ px.send_thermal_frame(cam.read_frame(), camera_id="thermal")
16
+ time.sleep(1 / 5)
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import base64
22
+ import time
23
+ from abc import ABC, abstractmethod
24
+ from dataclasses import dataclass
25
+ from typing import Any, Dict, Optional
26
+
27
+ import cv2
28
+ import numpy as np
29
+
30
+ # Inferno is perceptually uniform and well-suited for thermal imaging.
31
+ # Swap for cv2.COLORMAP_HOT or cv2.COLORMAP_JET if preferred.
32
+ _COLORMAP = cv2.COLORMAP_INFERNO
33
+
34
+ # Sensors at or below this pixel count include the full temps array in the
35
+ # wire message. I2C sensors (32×24 = 768, 16×12 = 192) always qualify.
36
+ # USB thermal cameras (256×192 = 49 152) do not.
37
+ _TEMPS_INLINE_THRESHOLD = 4096
38
+
39
+ # Upscale sensors whose shortest side is below this threshold.
40
+ _MIN_DISPLAY_SIDE = 160
41
+
42
+
43
+ class NoCameraFound(RuntimeError):
44
+ pass
45
+
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Abstract interface
49
+ # ---------------------------------------------------------------------------
50
+
51
+
52
+ class ThermalCamera(ABC):
53
+ """Hardware-agnostic thermal camera interface.
54
+
55
+ Subclass and implement read_frame() to support any sensor.
56
+ read_frame() must return a 2-D float32 array of temperatures in Celsius,
57
+ shaped (height, width).
58
+ """
59
+
60
+ @property
61
+ @abstractmethod
62
+ def width(self) -> int: ...
63
+
64
+ @property
65
+ @abstractmethod
66
+ def height(self) -> int: ...
67
+
68
+ @abstractmethod
69
+ def read_frame(self) -> np.ndarray: ...
70
+
71
+ def close(self) -> None:
72
+ pass
73
+
74
+
75
+ # ---------------------------------------------------------------------------
76
+ # Unified output
77
+ # ---------------------------------------------------------------------------
78
+
79
+
80
+ @dataclass
81
+ class ThermalFrame:
82
+ """Encoded output from a ThermalCamera, ready to send to the gateway."""
83
+
84
+ image: np.ndarray # uint8 BGR colorized image (display size)
85
+ width: int # display width (may differ from sensor if upscaled)
86
+ height: int # display height
87
+ sensor_width: int # native sensor width
88
+ sensor_height: int # native sensor height
89
+ temp_min: float
90
+ temp_max: float
91
+ temps: Optional[np.ndarray] # native res; None when sensor > threshold
92
+ timestamp_ms: int
93
+
94
+ def to_message(
95
+ self, camera_id: str, source_id: Optional[str] = None, quality: int = 85
96
+ ) -> Dict[str, Any]:
97
+ """Build the gateway `video_frame` wire message for this frame."""
98
+ _, buf = cv2.imencode(".jpg", self.image, [cv2.IMWRITE_JPEG_QUALITY, quality])
99
+ b64 = base64.b64encode(buf.tobytes()).decode("ascii")
100
+
101
+ msg: Dict[str, Any] = {
102
+ "type": "video_frame",
103
+ "camera_id": camera_id,
104
+ "frame": b64,
105
+ "width": self.width,
106
+ "height": self.height,
107
+ "timestamp": self.timestamp_ms,
108
+ "video_type": "thermal",
109
+ "sensor_width": self.sensor_width,
110
+ "sensor_height": self.sensor_height,
111
+ "temp_min": self.temp_min,
112
+ "temp_max": self.temp_max,
113
+ }
114
+ if source_id is not None:
115
+ msg["source_id"] = source_id
116
+ if self.temps is not None:
117
+ msg["temps"] = self.temps.flatten().tolist()
118
+ return msg
119
+
120
+
121
+ # ---------------------------------------------------------------------------
122
+ # Encoding — single shared path used by both encode_frame() and the SDK's
123
+ # Plexus.send_thermal_frame().
124
+ # ---------------------------------------------------------------------------
125
+
126
+
127
+ def _upscale_size(sw: int, sh: int) -> tuple[int, int]:
128
+ if sw >= _MIN_DISPLAY_SIDE and sh >= _MIN_DISPLAY_SIDE:
129
+ return sw, sh
130
+ scale = max(_MIN_DISPLAY_SIDE / sw, _MIN_DISPLAY_SIDE / sh)
131
+ return round(sw * scale), round(sh * scale)
132
+
133
+
134
+ def build_thermal_frame(
135
+ temps: np.ndarray, timestamp_ms: Optional[int] = None
136
+ ) -> ThermalFrame:
137
+ """Colorize a temperature array into a ThermalFrame.
138
+
139
+ temps: 2-D float32 Celsius array, shape (height, width).
140
+ timestamp_ms: wire timestamp; defaults to current time.
141
+ """
142
+ sh, sw = temps.shape[:2]
143
+
144
+ temp_min = float(np.min(temps))
145
+ temp_max = float(np.max(temps))
146
+ span = temp_max - temp_min
147
+
148
+ normalized = (
149
+ np.zeros((sh, sw), dtype=np.uint8)
150
+ if span < 1e-6
151
+ else ((temps - temp_min) / span * 255).astype(np.uint8)
152
+ )
153
+
154
+ colored = cv2.applyColorMap(normalized, _COLORMAP)
155
+
156
+ dw, dh = _upscale_size(sw, sh)
157
+ if (dw, dh) != (sw, sh):
158
+ colored = cv2.resize(colored, (dw, dh), interpolation=cv2.INTER_CUBIC)
159
+
160
+ return ThermalFrame(
161
+ image=colored,
162
+ width=dw,
163
+ height=dh,
164
+ sensor_width=sw,
165
+ sensor_height=sh,
166
+ temp_min=round(temp_min, 2),
167
+ temp_max=round(temp_max, 2),
168
+ temps=temps if sw * sh <= _TEMPS_INLINE_THRESHOLD else None,
169
+ timestamp_ms=timestamp_ms if timestamp_ms is not None else int(time.time() * 1000),
170
+ )
171
+
172
+
173
+ def encode_frame(cam: ThermalCamera) -> ThermalFrame:
174
+ """Read one frame from a camera and colorize it."""
175
+ return build_thermal_frame(cam.read_frame())
176
+
177
+
178
+ # ---------------------------------------------------------------------------
179
+ # Drivers
180
+ # ---------------------------------------------------------------------------
181
+
182
+
183
+ class SimulatedThermalCamera(ThermalCamera):
184
+ """Simulated thermal camera for testing without hardware.
185
+
186
+ Produces a moving warm blob over a noisy background at 32×24.
187
+ """
188
+
189
+ def __init__(self, width: int = 32, height: int = 24) -> None:
190
+ self._width = width
191
+ self._height = height
192
+ self._t = 0.0
193
+
194
+ @property
195
+ def width(self) -> int:
196
+ return self._width
197
+
198
+ @property
199
+ def height(self) -> int:
200
+ return self._height
201
+
202
+ def read_frame(self) -> np.ndarray:
203
+ self._t += 0.1
204
+ x = np.linspace(0, 2 * np.pi, self._width)
205
+ y = np.linspace(0, 2 * np.pi, self._height)
206
+ xx, yy = np.meshgrid(x, y)
207
+ base = 22.0 + 3.0 * np.sin(xx + self._t) * np.cos(yy + self._t * 0.7)
208
+ cx = int((np.sin(self._t * 0.5) * 0.4 + 0.5) * self._width)
209
+ cy = int((np.cos(self._t * 0.3) * 0.4 + 0.5) * self._height)
210
+ yg, xg = np.ogrid[: self._height, : self._width]
211
+ hotspot = 15.0 * np.exp(-((xg - cx) ** 2 + (yg - cy) ** 2) / 20.0)
212
+ return (base + hotspot).astype(np.float32)
213
+
214
+
215
+ class USBThermalCamera(ThermalCamera):
216
+ """USB thermal camera via V4L2/UVC in Y16 pixel format.
217
+
218
+ Most USB thermal cameras (InfiRay, Topdon, Seek, some FLIR) present as
219
+ standard UVC video devices outputting Y16 frames where each uint16 pixel
220
+ encodes temperature as: celsius = (value / 100.0) - 273.15
221
+
222
+ Requires: pip install opencv-python
223
+ """
224
+
225
+ _KELVIN_SCALE = 100.0
226
+ _KELVIN_OFFSET = 273.15
227
+
228
+ def __init__(self, device_index: int = 0) -> None:
229
+ self._cap = cv2.VideoCapture(device_index)
230
+ if not self._cap.isOpened():
231
+ raise NoCameraFound(f"Cannot open video device {device_index}")
232
+ self._cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*"Y16 "))
233
+ ret, frame = self._cap.read()
234
+ if not ret or frame is None:
235
+ self._cap.release()
236
+ raise NoCameraFound(
237
+ f"Device {device_index} opened but could not read a frame. "
238
+ "Check that it supports Y16 format."
239
+ )
240
+ self._width = int(self._cap.get(cv2.CAP_PROP_FRAME_WIDTH))
241
+ self._height = int(self._cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
242
+
243
+ @property
244
+ def width(self) -> int:
245
+ return self._width
246
+
247
+ @property
248
+ def height(self) -> int:
249
+ return self._height
250
+
251
+ def read_frame(self) -> np.ndarray:
252
+ ret, raw = self._cap.read()
253
+ if not ret or raw is None:
254
+ raise RuntimeError("Failed to read frame from USB thermal camera")
255
+ u16 = raw.view(np.uint16).reshape(self._height, self._width)
256
+ return (u16.astype(np.float32) / self._KELVIN_SCALE) - self._KELVIN_OFFSET
257
+
258
+ def close(self) -> None:
259
+ self._cap.release()
260
+
261
+
262
+ class MLX90640Camera(ThermalCamera):
263
+ """MLX90640 32×24 thermal array via I2C.
264
+
265
+ Requires: pip install adafruit-circuitpython-mlx90640
266
+ Wiring: SCL → GPIO 3, SDA → GPIO 2, 3.3V power (Raspberry Pi).
267
+ """
268
+
269
+ def __init__(self) -> None:
270
+ import adafruit_mlx90640
271
+ import board
272
+ import busio
273
+
274
+ i2c = busio.I2C(board.SCL, board.SDA, frequency=800_000)
275
+ self._mlx = adafruit_mlx90640.MLX90640(i2c)
276
+ self._mlx.refresh_rate = adafruit_mlx90640.RefreshRate.REFRESH_4_HZ
277
+ self._buf = [0.0] * 768
278
+
279
+ @property
280
+ def width(self) -> int:
281
+ return 32
282
+
283
+ @property
284
+ def height(self) -> int:
285
+ return 24
286
+
287
+ def read_frame(self) -> np.ndarray:
288
+ self._mlx.getFrame(self._buf)
289
+ return np.array(self._buf, dtype=np.float32).reshape(24, 32)
290
+
291
+
292
+ class MLX90641Camera(ThermalCamera):
293
+ """MLX90641 16×12 thermal array via I2C.
294
+
295
+ Requires: pip install adafruit-circuitpython-mlx90641
296
+ Wiring: SCL → GPIO 3, SDA → GPIO 2, 3.3V power (Raspberry Pi).
297
+ """
298
+
299
+ def __init__(self) -> None:
300
+ import adafruit_mlx90641
301
+ import board
302
+ import busio
303
+
304
+ i2c = busio.I2C(board.SCL, board.SDA, frequency=400_000)
305
+ self._mlx = adafruit_mlx90641.MLX90641(i2c)
306
+ self._mlx.refresh_rate = adafruit_mlx90641.RefreshRate.REFRESH_4_HZ
307
+ self._buf = [0.0] * 192
308
+
309
+ @property
310
+ def width(self) -> int:
311
+ return 16
312
+
313
+ @property
314
+ def height(self) -> int:
315
+ return 12
316
+
317
+ def read_frame(self) -> np.ndarray:
318
+ self._mlx.getFrame(self._buf)
319
+ return np.array(self._buf, dtype=np.float32).reshape(12, 16)
320
+
321
+
322
+ # ---------------------------------------------------------------------------
323
+ # Auto-detection factory
324
+ # ---------------------------------------------------------------------------
325
+
326
+
327
+ class ThermalSource:
328
+ """Factory that opens the first available thermal camera.
329
+
330
+ Examples:
331
+ ThermalSource.open() # auto-detect
332
+ ThermalSource.open("sim") # simulated
333
+ ThermalSource.open("mlx90640") # force I2C MLX90640
334
+ ThermalSource.open("mlx90641") # force I2C MLX90641
335
+ ThermalSource.open("usb") # USB thermal at index 0
336
+ ThermalSource.open(2) # USB thermal at index 2
337
+ """
338
+
339
+ @staticmethod
340
+ def open(hint: str | int | None = None) -> ThermalCamera:
341
+ if hint == "sim":
342
+ return SimulatedThermalCamera()
343
+ if hint == "mlx90640":
344
+ return MLX90640Camera()
345
+ if hint == "mlx90641":
346
+ return MLX90641Camera()
347
+ if hint == "usb" or isinstance(hint, int):
348
+ return USBThermalCamera(0 if hint == "usb" else hint)
349
+ return ThermalSource._autodetect()
350
+
351
+ @staticmethod
352
+ def _autodetect() -> ThermalCamera:
353
+ cam = ThermalSource._try_i2c()
354
+ if cam:
355
+ return cam
356
+ cam = ThermalSource._try_usb()
357
+ if cam:
358
+ return cam
359
+ raise NoCameraFound(
360
+ "No thermal camera detected. Use ThermalSource.open('sim') to run without hardware."
361
+ )
362
+
363
+ @staticmethod
364
+ def _try_i2c() -> ThermalCamera | None:
365
+ try:
366
+ import board
367
+ import busio
368
+
369
+ i2c = busio.I2C(board.SCL, board.SDA)
370
+ i2c.try_lock()
371
+ addrs = i2c.scan()
372
+ i2c.unlock()
373
+ if 0x33 in addrs:
374
+ return MLX90640Camera()
375
+ if 0x60 in addrs:
376
+ return MLX90641Camera()
377
+ except Exception:
378
+ pass
379
+ return None
380
+
381
+ @staticmethod
382
+ def _try_usb() -> ThermalCamera | None:
383
+ for idx in range(10):
384
+ try:
385
+ return USBThermalCamera(idx)
386
+ except NoCameraFound:
387
+ continue
388
+ return None
@@ -590,6 +590,51 @@ class Plexus:
590
590
  self._normalize_ts_ms(timestamp),
591
591
  )
592
592
 
593
+ def send_thermal_frame(
594
+ self,
595
+ temps,
596
+ camera_id: str = "thermal:0",
597
+ quality: int = 85,
598
+ timestamp: Optional[float] = None,
599
+ ) -> bool:
600
+ """Send a thermal camera frame to Plexus (WebSocket transport only).
601
+
602
+ Args:
603
+ temps: 2-D float32 numpy array of temperatures in Celsius,
604
+ shape (height, width). Obtained from ThermalCamera.read_frame().
605
+ camera_id: Logical camera identifier.
606
+ quality: JPEG quality for the colorized image, 1-100.
607
+ timestamp: Unix timestamp in seconds. Defaults to current time.
608
+
609
+ Returns:
610
+ True if the frame was sent successfully.
611
+
612
+ Raises:
613
+ PlexusError: If transport is not 'ws'.
614
+ ImportError: If opencv-python is not installed.
615
+ """
616
+ if self.transport != "ws":
617
+ raise PlexusError("send_thermal_frame requires transport='ws'")
618
+
619
+ try:
620
+ from plexus.cameras.thermal import build_thermal_frame
621
+ except ImportError as e:
622
+ raise ImportError(
623
+ "send_thermal_frame requires opencv-python. "
624
+ "Install with: pip install opencv-python"
625
+ ) from e
626
+
627
+ frame = build_thermal_frame(temps, timestamp_ms=self._normalize_ts_ms(timestamp))
628
+ msg = frame.to_message(
629
+ camera_id=camera_id, source_id=self.source_id, quality=quality
630
+ )
631
+
632
+ ws = self._ensure_ws()
633
+ if not ws.is_authenticated:
634
+ ws.wait_authenticated(timeout=min(self.timeout, 5.0))
635
+
636
+ return ws.send_json_video_frame(msg)
637
+
593
638
  def stream_camera(
594
639
  self,
595
640
  url: str,
@@ -209,6 +209,13 @@ class WebSocketTransport:
209
209
  except queue.Full:
210
210
  return False
211
211
 
212
+ def send_json_video_frame(self, msg: Dict[str, Any]) -> bool:
213
+ """Send a JSON video_frame message. Used for frames that carry extra
214
+ metadata (e.g. thermal cameras) that the binary format cannot express."""
215
+ if not self._authenticated.is_set():
216
+ return False
217
+ return self._send_frame(msg)
218
+
212
219
  # ------------------------------------------------------------------ thread
213
220
 
214
221
  def _run(self) -> None:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "plexus-python"
7
- version = "0.5.2"
7
+ version = "0.6.1"
8
8
  description = "Thin Python SDK for Plexus — send telemetry in one line"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -31,7 +31,12 @@ dependencies = [
31
31
 
32
32
  [project.optional-dependencies]
33
33
  video = ["Pillow>=12.2.0"]
34
- dev = ["pytest>=9.0.3", "pytest-cov", "ruff", "websockets>=12"]
34
+ # Thermal cameras: colorization + Y16/USB capture. I2C sensors additionally
35
+ # need a board-specific driver, installed separately on the device, e.g.
36
+ # adafruit-circuitpython-mlx90640 (these require Raspberry Pi hardware libs
37
+ # and don't install on other platforms, so they're not listed here).
38
+ thermal = ["opencv-python>=4.8", "numpy>=1.24"]
39
+ dev = ["pytest>=9.0.3", "pytest-cov", "ruff", "websockets>=12", "opencv-python>=4.8", "numpy>=1.24"]
35
40
 
36
41
  [project.scripts]
37
42
  plexus = "plexus.cli:main"
@@ -0,0 +1,153 @@
1
+ """Tests for thermal camera encoding and the send_thermal_frame wire message."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+ # Thermal support requires opencv + numpy (the `thermal`/`dev` extras). Skip the
8
+ # whole module cleanly if they're not installed rather than erroring on import.
9
+ np = pytest.importorskip("numpy")
10
+ pytest.importorskip("cv2")
11
+
12
+ from plexus.cameras.thermal import ( # noqa: E402
13
+ SimulatedThermalCamera,
14
+ ThermalFrame,
15
+ _upscale_size,
16
+ build_thermal_frame,
17
+ encode_frame,
18
+ )
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # _upscale_size — pure geometry, no encoding
22
+ # ---------------------------------------------------------------------------
23
+
24
+
25
+ class TestUpscaleSize:
26
+ def test_large_sensor_unchanged(self):
27
+ assert _upscale_size(256, 192) == (256, 192)
28
+
29
+ def test_small_sensor_upscaled_to_min_side(self):
30
+ # MLX90640 32x24: shortest side (24) scaled up to 160.
31
+ dw, dh = _upscale_size(32, 24)
32
+ assert min(dw, dh) == 160
33
+ # Aspect ratio preserved (4:3).
34
+ assert dw == round(dh * 32 / 24)
35
+
36
+ def test_tiny_sensor_upscaled(self):
37
+ # MLX90641 16x12.
38
+ dw, dh = _upscale_size(16, 12)
39
+ assert min(dw, dh) == 160
40
+
41
+ def test_square_below_threshold(self):
42
+ assert _upscale_size(100, 100) == (160, 160)
43
+
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # SimulatedThermalCamera
47
+ # ---------------------------------------------------------------------------
48
+
49
+
50
+ class TestSimulatedCamera:
51
+ def test_frame_shape_and_dtype(self):
52
+ cam = SimulatedThermalCamera()
53
+ frame = cam.read_frame()
54
+ assert frame.shape == (24, 32) # (height, width)
55
+ assert frame.dtype == np.float32
56
+
57
+ def test_dimensions(self):
58
+ cam = SimulatedThermalCamera()
59
+ assert cam.width == 32
60
+ assert cam.height == 24
61
+
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # build_thermal_frame + ThermalFrame.to_message
65
+ # ---------------------------------------------------------------------------
66
+
67
+
68
+ class TestBuildThermalFrame:
69
+ def test_small_sensor_includes_temps(self):
70
+ temps = np.arange(768, dtype=np.float32).reshape(24, 32)
71
+ frame = build_thermal_frame(temps)
72
+ assert isinstance(frame, ThermalFrame)
73
+ assert frame.sensor_width == 32
74
+ assert frame.sensor_height == 24
75
+ assert frame.temp_min == 0.0
76
+ assert frame.temp_max == 767.0
77
+ # 768 px <= 4096 threshold → temps retained.
78
+ assert frame.temps is not None
79
+
80
+ def test_large_sensor_drops_temps(self):
81
+ # 100x100 = 10_000 px > 4096 threshold → temps omitted.
82
+ temps = np.zeros((100, 100), dtype=np.float32)
83
+ frame = build_thermal_frame(temps)
84
+ assert frame.temps is None
85
+
86
+ def test_image_upscaled_for_small_sensor(self):
87
+ temps = np.zeros((24, 32), dtype=np.float32)
88
+ frame = build_thermal_frame(temps)
89
+ assert min(frame.width, frame.height) == 160
90
+
91
+ def test_timestamp_passthrough(self):
92
+ temps = np.zeros((24, 32), dtype=np.float32)
93
+ frame = build_thermal_frame(temps, timestamp_ms=1_234_567_890)
94
+ assert frame.timestamp_ms == 1_234_567_890
95
+
96
+ def test_uniform_frame_does_not_crash(self):
97
+ # Zero span (all pixels equal) must not divide-by-zero.
98
+ temps = np.full((24, 32), 25.0, dtype=np.float32)
99
+ frame = build_thermal_frame(temps)
100
+ assert frame.temp_min == 25.0
101
+ assert frame.temp_max == 25.0
102
+
103
+
104
+ class TestToMessage:
105
+ def _msg(self, **kwargs):
106
+ temps = np.arange(768, dtype=np.float32).reshape(24, 32)
107
+ frame = build_thermal_frame(temps, timestamp_ms=1000)
108
+ return frame, frame.to_message(**kwargs)
109
+
110
+ def test_message_shape(self):
111
+ _, msg = self._msg(camera_id="thermal", source_id="dev1")
112
+ assert msg["type"] == "video_frame"
113
+ assert msg["camera_id"] == "thermal"
114
+ assert msg["source_id"] == "dev1"
115
+ assert msg["video_type"] == "thermal"
116
+ assert msg["sensor_width"] == 32
117
+ assert msg["sensor_height"] == 24
118
+ assert msg["timestamp"] == 1000
119
+ assert isinstance(msg["frame"], str) and msg["frame"] # base64 JPEG
120
+
121
+ def test_temps_flattened_and_preserved(self):
122
+ _, msg = self._msg(camera_id="thermal")
123
+ assert "temps" in msg
124
+ assert len(msg["temps"]) == 768
125
+ assert msg["temps"] == list(range(768))
126
+
127
+ def test_source_id_omitted_when_none(self):
128
+ _, msg = self._msg(camera_id="thermal")
129
+ assert "source_id" not in msg
130
+
131
+ def test_display_dims_match_upscaled_image(self):
132
+ frame, msg = self._msg(camera_id="thermal")
133
+ assert msg["width"] == frame.width
134
+ assert msg["height"] == frame.height
135
+ assert min(msg["width"], msg["height"]) == 160
136
+
137
+ def test_large_sensor_message_has_no_temps(self):
138
+ temps = np.zeros((100, 100), dtype=np.float32)
139
+ msg = build_thermal_frame(temps).to_message(camera_id="t")
140
+ assert "temps" not in msg
141
+
142
+
143
+ # ---------------------------------------------------------------------------
144
+ # encode_frame — camera → ThermalFrame
145
+ # ---------------------------------------------------------------------------
146
+
147
+
148
+ def test_encode_frame_reads_camera():
149
+ cam = SimulatedThermalCamera()
150
+ frame = encode_frame(cam)
151
+ assert frame.sensor_width == 32
152
+ assert frame.sensor_height == 24
153
+ assert frame.temps is not None
@@ -1,6 +1,10 @@
1
1
  version = 1
2
2
  revision = 2
3
3
  requires-python = ">=3.10"
4
+ resolution-markers = [
5
+ "python_full_version >= '3.11'",
6
+ "python_full_version < '3.11'",
7
+ ]
4
8
 
5
9
  [[package]]
6
10
  name = "colorama"
@@ -134,7 +138,7 @@ name = "exceptiongroup"
134
138
  version = "1.3.1"
135
139
  source = { registry = "https://pypi.org/simple" }
136
140
  dependencies = [
137
- { name = "typing-extensions", marker = "python_full_version < '3.13'" },
141
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
138
142
  ]
139
143
  sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
140
144
  wheels = [
@@ -150,6 +154,172 @@ wheels = [
150
154
  { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
151
155
  ]
152
156
 
157
+ [[package]]
158
+ name = "numpy"
159
+ version = "2.2.6"
160
+ source = { registry = "https://pypi.org/simple" }
161
+ resolution-markers = [
162
+ "python_full_version < '3.11'",
163
+ ]
164
+ 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" }
165
+ wheels = [
166
+ { 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" },
167
+ { 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" },
168
+ { 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" },
169
+ { 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" },
170
+ { 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" },
171
+ { 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" },
172
+ { 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" },
173
+ { 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" },
174
+ { 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" },
175
+ { 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" },
176
+ { 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" },
177
+ { 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" },
178
+ { 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" },
179
+ { 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" },
180
+ { 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" },
181
+ { 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" },
182
+ { 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" },
183
+ { 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" },
184
+ { 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" },
185
+ { 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" },
186
+ { 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" },
187
+ { 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" },
188
+ { 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" },
189
+ { 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" },
190
+ { 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" },
191
+ { 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" },
192
+ { 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" },
193
+ { 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" },
194
+ { 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" },
195
+ { 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" },
196
+ { 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" },
197
+ { 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" },
198
+ { 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" },
199
+ { 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" },
200
+ { 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" },
201
+ { 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" },
202
+ { 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" },
203
+ { 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" },
204
+ { 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" },
205
+ { 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" },
206
+ { 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" },
207
+ { 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" },
208
+ { 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" },
209
+ { 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" },
210
+ { 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" },
211
+ { 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" },
212
+ { 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" },
213
+ { 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" },
214
+ { 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" },
215
+ { 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" },
216
+ { 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" },
217
+ { 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" },
218
+ { 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" },
219
+ { 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" },
220
+ ]
221
+
222
+ [[package]]
223
+ name = "numpy"
224
+ version = "2.4.6"
225
+ source = { registry = "https://pypi.org/simple" }
226
+ resolution-markers = [
227
+ "python_full_version >= '3.11'",
228
+ ]
229
+ sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" }
230
+ wheels = [
231
+ { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" },
232
+ { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" },
233
+ { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" },
234
+ { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" },
235
+ { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" },
236
+ { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" },
237
+ { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" },
238
+ { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" },
239
+ { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" },
240
+ { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" },
241
+ { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" },
242
+ { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" },
243
+ { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" },
244
+ { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" },
245
+ { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" },
246
+ { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" },
247
+ { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" },
248
+ { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" },
249
+ { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" },
250
+ { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" },
251
+ { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" },
252
+ { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" },
253
+ { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" },
254
+ { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" },
255
+ { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" },
256
+ { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" },
257
+ { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" },
258
+ { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" },
259
+ { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" },
260
+ { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" },
261
+ { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" },
262
+ { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" },
263
+ { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" },
264
+ { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" },
265
+ { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" },
266
+ { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" },
267
+ { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" },
268
+ { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" },
269
+ { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" },
270
+ { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" },
271
+ { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" },
272
+ { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" },
273
+ { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" },
274
+ { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" },
275
+ { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" },
276
+ { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" },
277
+ { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" },
278
+ { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" },
279
+ { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" },
280
+ { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" },
281
+ { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" },
282
+ { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" },
283
+ { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" },
284
+ { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" },
285
+ { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" },
286
+ { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" },
287
+ { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" },
288
+ { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" },
289
+ { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" },
290
+ { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" },
291
+ { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" },
292
+ { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" },
293
+ { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" },
294
+ { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" },
295
+ { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" },
296
+ { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" },
297
+ { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" },
298
+ { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" },
299
+ { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" },
300
+ { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" },
301
+ { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" },
302
+ ]
303
+
304
+ [[package]]
305
+ name = "opencv-python"
306
+ version = "4.13.0.92"
307
+ source = { registry = "https://pypi.org/simple" }
308
+ dependencies = [
309
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
310
+ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
311
+ ]
312
+ wheels = [
313
+ { url = "https://files.pythonhosted.org/packages/fc/6f/5a28fef4c4a382be06afe3938c64cc168223016fa520c5abaf37e8862aa5/opencv_python-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:caf60c071ec391ba51ed00a4a920f996d0b64e3e46068aac1f646b5de0326a19", size = 46247052, upload-time = "2026-02-05T07:01:25.046Z" },
314
+ { url = "https://files.pythonhosted.org/packages/08/ac/6c98c44c650b8114a0fb901691351cfb3956d502e8e9b5cd27f4ee7fbf2f/opencv_python-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:5868a8c028a0b37561579bfb8ac1875babdc69546d236249fff296a8c010ccf9", size = 32568781, upload-time = "2026-02-05T07:01:41.379Z" },
315
+ { url = "https://files.pythonhosted.org/packages/3e/51/82fed528b45173bf629fa44effb76dff8bc9f4eeaee759038362dfa60237/opencv_python-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bc2596e68f972ca452d80f444bc404e08807d021fbba40df26b61b18e01838a", size = 47685527, upload-time = "2026-02-05T06:59:11.24Z" },
316
+ { url = "https://files.pythonhosted.org/packages/db/07/90b34a8e2cf9c50fe8ed25cac9011cde0676b4d9d9c973751ac7616223a2/opencv_python-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:402033cddf9d294693094de5ef532339f14ce821da3ad7df7c9f6e8316da32cf", size = 70460872, upload-time = "2026-02-05T06:59:19.162Z" },
317
+ { url = "https://files.pythonhosted.org/packages/02/6d/7a9cc719b3eaf4377b9c2e3edeb7ed3a81de41f96421510c0a169ca3cfd4/opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bccaabf9eb7f897ca61880ce2869dcd9b25b72129c28478e7f2a5e8dee945616", size = 46708208, upload-time = "2026-02-05T06:59:15.419Z" },
318
+ { url = "https://files.pythonhosted.org/packages/fd/55/b3b49a1b97aabcfbbd6c7326df9cb0b6fa0c0aefa8e89d500939e04aa229/opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:620d602b8f7d8b8dab5f4b99c6eb353e78d3fb8b0f53db1bd258bb1aa001c1d5", size = 72927042, upload-time = "2026-02-05T06:59:23.389Z" },
319
+ { url = "https://files.pythonhosted.org/packages/fb/17/de5458312bcb07ddf434d7bfcb24bb52c59635ad58c6e7c751b48949b009/opencv_python-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:372fe164a3148ac1ca51e5f3ad0541a4a276452273f503441d718fab9c5e5f59", size = 30932638, upload-time = "2026-02-05T07:02:14.98Z" },
320
+ { url = "https://files.pythonhosted.org/packages/e9/a5/1be1516390333ff9be3a9cb648c9f33df79d5096e5884b5df71a588af463/opencv_python-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:423d934c9fafb91aad38edf26efb46da91ffbc05f3f59c4b0c72e699720706f5", size = 40212062, upload-time = "2026-02-05T07:02:12.724Z" },
321
+ ]
322
+
153
323
  [[package]]
154
324
  name = "packaging"
155
325
  version = "26.2"
@@ -259,7 +429,7 @@ wheels = [
259
429
 
260
430
  [[package]]
261
431
  name = "plexus-python"
262
- version = "0.4.9"
432
+ version = "0.5.2"
263
433
  source = { editable = "." }
264
434
  dependencies = [
265
435
  { name = "websocket-client" },
@@ -267,17 +437,29 @@ dependencies = [
267
437
 
268
438
  [package.optional-dependencies]
269
439
  dev = [
440
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
441
+ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
442
+ { name = "opencv-python" },
270
443
  { name = "pytest" },
271
444
  { name = "pytest-cov" },
272
445
  { name = "ruff" },
273
446
  { name = "websockets" },
274
447
  ]
448
+ thermal = [
449
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
450
+ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
451
+ { name = "opencv-python" },
452
+ ]
275
453
  video = [
276
454
  { name = "pillow" },
277
455
  ]
278
456
 
279
457
  [package.metadata]
280
458
  requires-dist = [
459
+ { name = "numpy", marker = "extra == 'dev'", specifier = ">=1.24" },
460
+ { name = "numpy", marker = "extra == 'thermal'", specifier = ">=1.24" },
461
+ { name = "opencv-python", marker = "extra == 'dev'", specifier = ">=4.8" },
462
+ { name = "opencv-python", marker = "extra == 'thermal'", specifier = ">=4.8" },
281
463
  { name = "pillow", marker = "extra == 'video'", specifier = ">=12.2.0" },
282
464
  { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.3" },
283
465
  { name = "pytest-cov", marker = "extra == 'dev'" },
@@ -285,7 +467,7 @@ requires-dist = [
285
467
  { name = "websocket-client", specifier = ">=1.7" },
286
468
  { name = "websockets", marker = "extra == 'dev'", specifier = ">=12" },
287
469
  ]
288
- provides-extras = ["video", "dev"]
470
+ provides-extras = ["video", "thermal", "dev"]
289
471
 
290
472
  [[package]]
291
473
  name = "pluggy"
File without changes
File without changes
File without changes