Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1119733929 | |||
| e545c245d7 | |||
| 7d32fb4b65 | |||
| 5b1c924d35 | |||
| b34b920759 | |||
| a70764e08c | |||
| 2e4763510d | |||
| 7e584e08e8 | |||
| cea4fb782e | |||
| 5b186df888 | |||
| f89a735d51 | |||
| 1cd54e81fc | |||
| 12bd778f13 | |||
| 72334795da | |||
| b9f8032203 | |||
| 97ca58bfc2 | |||
| b52b983bb2 | |||
| ca1a514f23 | |||
| 59528eb6c9 | |||
| 9cd3170e8a | |||
| 9779baa468 | |||
| af170666e8 | |||
| 16d8bf48af |
+8
-2
@@ -5,6 +5,9 @@ __pycache__/
|
||||
*.pyd
|
||||
*.so
|
||||
*.a
|
||||
*.egg-info/
|
||||
.pytest_cache/
|
||||
.ruff_cache/
|
||||
|
||||
# Virtual environment
|
||||
.venv/
|
||||
@@ -14,10 +17,11 @@ env/
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
.qtcreator/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
docs
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -34,4 +38,6 @@ build/
|
||||
dist/
|
||||
*.spec
|
||||
*.icns
|
||||
*.ico
|
||||
*.ico
|
||||
# Superpowers brainstorming visual companion
|
||||
.superpowers/
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type QmlProjectModel 1.0
|
||||
|
||||
QmlProject {
|
||||
name: "ISC"
|
||||
version: "1.0"
|
||||
mainFile: "src/pygui/ISC/Main.qml"
|
||||
|
||||
dependencies {
|
||||
module: "QtQuick"
|
||||
minimumVersion: "6.0.0"
|
||||
}
|
||||
|
||||
importPaths: [
|
||||
"src/pygui"
|
||||
]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
# ===== ISC Tabs Module =====
|
||||
module ISC.Tabs
|
||||
|
||||
# ===== Tab Components =====
|
||||
SettingsTab 1.0 SettingsTab.qml
|
||||
@@ -16,34 +16,42 @@ python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install -e . # install the `pygui` package (src/ layout) in editable mode
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
python main.py
|
||||
python -m pygui # or run the `isc` console script
|
||||
```
|
||||
|
||||
This launches the Qt window and loads the `ISC` QML module from `ISC/Main.qml`.
|
||||
This launches the Qt window and loads the `ISC` QML module from `src/pygui/ISC/Main.qml`.
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `main.py`: PySide6 bootstrap; creates the Qt app and loads QML module `ISC/Main`.
|
||||
- `ISC/Main.qml`: top-level window definition.
|
||||
- `ISC/HomePage.qml`: main UI layout (left action rail, workspace panel, footer tabs).
|
||||
- `ISC/Theme.qml`: shared theme constants and dark/light mode tokens.
|
||||
- `ISC/qmldir`: QML module registration.
|
||||
The application lives under a `src/` layout as the `pygui` package:
|
||||
|
||||
- `src/pygui/__main__.py`: PySide6 bootstrap; creates the Qt app and loads QML module `ISC/Main`. Entry point for `python -m pygui`.
|
||||
- `src/pygui/backend/`: Qt-facing models and controllers (device, settings, file browser, wafer parsing).
|
||||
- `src/pygui/serialcomm/`: serial port, device service, and data-parser layer.
|
||||
- `src/pygui/ISC/`: the `ISC` QML module (UI).
|
||||
- `Main.qml`: top-level window definition.
|
||||
- `HomePage.qml`: main UI layout (left action rail, workspace panel, footer tabs).
|
||||
- `Theme.qml`: shared theme constants and dark/light mode tokens.
|
||||
- `qmldir`: QML module registration.
|
||||
- `tests/`: pytest suite.
|
||||
- `packaging/`: PyInstaller spec (`isc.spec`) and app icons.
|
||||
|
||||
## Window Configuration
|
||||
|
||||
Window dimensions and constraints are defined in `ISC/Main.qml`:
|
||||
Window dimensions and constraints are defined in `src/pygui/ISC/Main.qml`:
|
||||
|
||||
- **Default size**: 1400 × 820 pixels
|
||||
- **Minimum size**: 1100 × 700 pixels
|
||||
- **Title bar**: "ISenseCloud"
|
||||
|
||||
To adjust the window, edit the `Window` block in `ISC/Main.qml`:
|
||||
To adjust the window, edit the `Window` block in `src/pygui/ISC/Main.qml`:
|
||||
|
||||
```qml
|
||||
Window {
|
||||
@@ -58,8 +66,8 @@ Window {
|
||||
|
||||
## Customization
|
||||
|
||||
- Toggle dark/light mode in `ISC/Theme.qml` via `isDarkMode`.
|
||||
- Update sidebar and footer labels in `ISC/HomePage.qml` through `sideActions` and `bottomTabs`.
|
||||
- Toggle dark/light mode in `src/pygui/ISC/Theme.qml` via `isDarkMode`.
|
||||
- Update sidebar and footer labels in `src/pygui/ISC/HomePage.qml` through `sideActions` and `bottomTabs`.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# This will be the name of the output files
|
||||
name: aepwafer
|
||||
wafers: ["A", "E", "P"]
|
||||
|
||||
# round or square wafer
|
||||
shape: round
|
||||
|
||||
# diameter/edge size of the wafer
|
||||
size: 300
|
||||
|
||||
# large or small markers and labels
|
||||
marker_size: large
|
||||
font_size: large
|
||||
|
||||
# origin settings. x_origin can be left, right, or center. y_origin can
|
||||
# be top, bottom, or center. Using right or top implies that the X/Y values
|
||||
# for that axis are negative, or else we'll be drawing outside the image.
|
||||
x_origin: left
|
||||
y_origin: bottom
|
||||
|
||||
# sensor X and Y values, in mm relative to the origin
|
||||
X: [150, 204, 249, 280, 290, 280, 249, 204, 150, 97, 51, 21, 10, 21, 51, 97,
|
||||
150, 203, 241, 255, 241, 203, 150, 98, 59, 45, 59, 98, 171, 207, 228, 228,
|
||||
207, 171, 130, 94, 73, 73, 94, 130, 150, 186, 200, 186, 150, 115, 100, 115]
|
||||
Y: [290, 280, 249, 204, 150, 97, 51, 21, 10, 21, 51, 97, 150, 204, 249, 280,
|
||||
255, 241, 203, 150, 98, 59, 45, 59, 98, 150, 203, 241, 228, 207, 171, 130,
|
||||
94, 73, 73, 94, 130, 171, 207, 228, 200, 186, 150, 115, 100, 115, 150, 186]
|
||||
|
||||
# set this to either 0 or 1 for the initial sensor number (usually 1)
|
||||
start_sn: 1
|
||||
|
||||
# Normally, positive X is to the right, and positive Y is up.
|
||||
# These settings can be used to reverse one or both of these.
|
||||
reverse_x: false
|
||||
reverse_y: false
|
||||
|
||||
# these are sensors whose labels need to be repositioned. Sensor numbers
|
||||
# start at start_sn. Directions are up, down, left, and right, and values
|
||||
# are multiples of the marker size.
|
||||
label_exceptions: {
|
||||
4: { top: [0, 0] },
|
||||
5: { top: [0, 0] },
|
||||
6: { top: [0, 0] },
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
# This will be the name of the output files
|
||||
name: bcdwafer
|
||||
wafers: ["B", "C", "D"]
|
||||
|
||||
# round or square wafer
|
||||
shape: round
|
||||
|
||||
# diameter/edge size of the wafer
|
||||
size: 300
|
||||
|
||||
# large or small markers and labels
|
||||
marker_size: large
|
||||
font_size: large
|
||||
|
||||
# origin settings. x_origin can be left, right, or center. y_origin can
|
||||
# be top, bottom, or center. Using right or top implies that the X/Y values
|
||||
# for that axis are negative, or else we'll be drawing outside the image.
|
||||
x_origin: center
|
||||
y_origin: center
|
||||
|
||||
# sensor X and Y values, in mm relative to the origin
|
||||
X: [0, 72.50, 125.57, 145.00, 125.57, 72.50, 0, -72.50, -125.57, -145.00,
|
||||
-125.57, -72.50, 0, 47.50, 82.27, 95.00, 82.27, 47.50, 0, -47.50, -82.27,
|
||||
-95.00, -82.27, -47.50, 38.97, 22.50, -38.97, -22.50, 0]
|
||||
Y: [145.00, 125.57, 72.50, 0, -72.50, -125.57, -145.00, -125.57, -72.50, 0,
|
||||
72.50, 125.57, 95.00, 82.27, 47.50, 0, -47.50, -82.27, -95.00, -82.27,
|
||||
-47.50, 0, 47.50, 82.27, 22.50, -38.97, -22.50, 38.97, 0]
|
||||
|
||||
# set this to either 0 or 1 for the initial sensor number (usually 1)
|
||||
start_sn: 1
|
||||
|
||||
# Normally, positive X is to the right, and positive Y is up.
|
||||
# These settings can be used to reverse one or both of these.
|
||||
reverse_x: false
|
||||
reverse_y: false
|
||||
|
||||
# these are sensors whose labels need to be repositioned. Sensor numbers
|
||||
# start at start_sn. Directions are up, down, left, and right, and values
|
||||
# are multiples of the marker size.
|
||||
label_exceptions: {
|
||||
2: { left: [1, 0] },
|
||||
3: { left: [1, 0] },
|
||||
4: { left: [1, 0] },
|
||||
5: { left: [1, 0] },
|
||||
6: { left: [1, 0] },
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
# This will be the name of the output files
|
||||
name: fwafer
|
||||
wafers: ["F"]
|
||||
|
||||
# round or square wafer
|
||||
shape: round
|
||||
|
||||
# diameter/edge size of the wafer
|
||||
size: 200
|
||||
|
||||
# large or small markers and labels
|
||||
marker_size: large
|
||||
font_size: large
|
||||
|
||||
# origin settings. x_origin can be left, right, or center. y_origin can
|
||||
# be top, bottom, or center. Using right or top implies that the X/Y values
|
||||
# for that axis are negative, or else we'll be drawing outside the image.
|
||||
x_origin: center
|
||||
y_origin: center
|
||||
|
||||
# sensor X and Y values, in mm relative to the origin
|
||||
X: [0.0, 47.5, 82.3, 95.0, 82.3, 47.5, 0.0, -47.5, -82.3, -95.0, -82.3, -47.5,
|
||||
-55.0, -14.8, 39.7, 55.0, 14.8, -39.7, -14.7, 25.5, 14.7, -25.5]
|
||||
Y: [95.0, 82.3, 47.5, 0.0, -47.5, -82.3, -95.0, -82.3, -47.5, 0.0, 47.5, 82.3,
|
||||
13.3, 54.4, 40.0, -13.3, -54.4, -40.0, 25.5, 14.7, -25.5, -14.7]
|
||||
|
||||
# set this to either 0 or 1 for the initial sensor number (usually 1)
|
||||
start_sn: 1
|
||||
|
||||
# Normally, positive X is to the right, and positive Y is up.
|
||||
# These settings can be used to reverse one or both of these.
|
||||
reverse_x: false
|
||||
reverse_y: false
|
||||
|
||||
# these are sensors whose labels need to be repositioned. Sensor numbers
|
||||
# start at start_sn. Directions are up, down, left, and right, and values
|
||||
# are multiples of the marker size.
|
||||
label_exceptions: {
|
||||
2: { left: [1, 0] },
|
||||
3: { left: [1, 0] },
|
||||
4: { left: [1, 0] },
|
||||
5: { left: [1, 0] },
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
# This will be the name of the output files
|
||||
name: xwafer
|
||||
wafers: ["X"]
|
||||
|
||||
# round or square wafer
|
||||
shape: square
|
||||
|
||||
# diameter/edge size of the wafer
|
||||
size: 310
|
||||
|
||||
# large or small markers and labels
|
||||
marker_size: small
|
||||
font_size: small
|
||||
|
||||
# origin settings. x_origin can be left, right, or center. y_origin can
|
||||
# be top, bottom, or center. Using right or top implies that the X/Y values
|
||||
# for that axis are negative, or else we'll be drawing outside the image.
|
||||
x_origin: left
|
||||
y_origin: bottom
|
||||
|
||||
# sensor X and Y values, in mm relative to the origin
|
||||
X: [ 3.00, 3.00, 3.00, 3.00, 3.00, 3.00, 3.00, 3.00, 3.00, 3.00, 3.00, 3.00,
|
||||
13.00, 23.00, 46.43, 89.86, 133.29, 176.72, 220.15, 263.58, 287.01, 297.01,
|
||||
307.01, 307.01, 307.01, 307.01, 307.01, 307.01, 307.01, 307.01, 307.01,
|
||||
307.01, 307.01, 307.01, 297.01, 287.01, 263.58, 220.15, 176.72, 133.29,
|
||||
89.86, 46.43, 23.00, 13.00, 46.43, 46.43, 46.43, 46.43, 46.43, 46.43,
|
||||
89.86, 133.29, 176.72, 220.15, 263.58, 263.58, 263.58, 263.58, 263.58,
|
||||
263.58, 220.15, 176.72, 133.29, 89.86, 89.86, 89.86, 89.86, 89.86,
|
||||
133.29, 176.72, 220.15, 220.15, 220.15, 220.15, 176.72, 133.29, 133.29,
|
||||
133.29, 176.72, 176.72]
|
||||
Y: [ 3.00, 13.07, 23.07, 46.50, 89.93, 133.36, 176.79, 220.22, 263.65, 287.08,
|
||||
297.08, 307.08, 307.08, 307.08, 307.08, 307.08, 307.08, 307.08, 307.08,
|
||||
307.08, 307.08, 307.08, 307.08, 297.08, 287.08, 263.65, 220.22, 176.79,
|
||||
133.36, 89.93, 46.50, 23.07, 13.07, 3.00, 3.00, 3.00, 3.00, 3.00, 3.00,
|
||||
3.00, 3.00, 3.00, 3.00, 3.00, 46.50, 89.93, 133.36, 176.79, 220.22, 263.65,
|
||||
263.65, 263.65, 263.65, 263.65, 263.65, 220.22, 176.79, 133.36, 89.93,
|
||||
46.50, 46.50, 46.50, 46.50, 46.50, 89.93, 133.36, 176.79, 220.22, 220.22,
|
||||
220.22, 220.22, 176.79, 133.36, 89.93, 89.93, 89.93, 133.36, 176.79,
|
||||
176.79, 133.36 ]
|
||||
|
||||
# set this to either 0 or 1 for the initial sensor number (usually 1)
|
||||
start_sn: 1
|
||||
|
||||
# Normally, positive X is to the right, and positive Y is up.
|
||||
# These settings can be used to reverse one or both of these.
|
||||
reverse_x: false
|
||||
reverse_y: false
|
||||
|
||||
# these are sensors whose labels need to be repositioned. Sensor numbers
|
||||
# start at start_sn. Directions are up, down, left, and right, and values
|
||||
# are multiples of the marker size.
|
||||
label_exceptions: {
|
||||
21: { left: [0, 0] },
|
||||
22: { left: [0, 0] },
|
||||
23: { left: [0, 0] },
|
||||
24: { left: [0, 0] },
|
||||
25: { left: [0, 0] },
|
||||
26: { left: [0, 0] },
|
||||
27: { left: [0, 0] },
|
||||
28: { left: [0, 0] },
|
||||
29: { left: [0, 0] },
|
||||
30: { left: [0, 0] },
|
||||
31: { left: [0, 0] },
|
||||
32: { left: [0, 0] },
|
||||
33: { left: [0, 0] },
|
||||
34: { left: [0, 0] },
|
||||
35: { left: [0, 0] },
|
||||
36: { left: [0, 0] }
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
# This will be the name of the output files
|
||||
name: zwafer
|
||||
wafers: ["Z"]
|
||||
|
||||
# round or square wafer
|
||||
shape: round
|
||||
|
||||
# diameter/size of the wafer
|
||||
size: 300
|
||||
|
||||
# large or small markers and labels
|
||||
marker_size: small
|
||||
font_size: small
|
||||
|
||||
# origin settings. x_origin can be left, right, or center. y_origin can
|
||||
# be top, bottom, or center. Using right or top probably also needs
|
||||
# reverse_x/reverse_y to be set as well.
|
||||
x_origin: center
|
||||
y_origin: center
|
||||
|
||||
# sensor X and Y values, in mm relative to the wafer center
|
||||
X: [0.00, -1.17, 8.92, 1.17, -8.92, 22.52, 29.35, -22.52, -29.35, 45.05, 73.37,
|
||||
58.71, 9.66, -45.05, -73.37, -58.71, -9.66, 79.85, 89.06, 46.10, -23.86,
|
||||
-79.85, -89.06, -46.10, 23.86, 66.96, 109.06, 87.27, 14.36, -66.96,
|
||||
-109.06, -87.27, -14.36, 121.24, 135.23, 70.00, -36.23, -121.24, -135.23,
|
||||
-70.00, 36.23, 124.71, 139.09, 72.00, -37.27, -124.71, -139.09, -72.00,
|
||||
37.27, 89.49, 127.31, 145.74, 141.99, 116.62, 73.50, 19.19, -38.05, -89.49,
|
||||
-127.31, -145.74, -141.99, -116.62, -73.50, -19.19, 38.05]
|
||||
Y: [0.00, -8.92, -1.17, 8.92, 1.17, -29.35, 22.52, 29.35, -22.52, -58.71,
|
||||
-9.66, 45.05, 73.37, 58.71, 9.66, -45.05, -73.37, -46.10, 23.86, 79.85,
|
||||
89.06, 46.10, -23.86, -79.85, -89.06, -87.27, -14.36, 66.96, 109.06, 87.27,
|
||||
14.36, -66.96, -109.06, -70.00, 36.23, 121.24, 135.23, 70.00, -36.23,
|
||||
-121.24, -135.23, -72.00, 37.27, 124.71, 139.09, 72.00, -37.27, -124.71,
|
||||
-139.09, -116.62, -73.50, -19.19, 38.05, 89.49, 127.31, 145.74, 141.99,
|
||||
116.62, 73.50, 19.19, -38.05, -89.49, -127.31, -145.74, -141.99]
|
||||
|
||||
# set this to either 0 or 1 for the initial sensor number (usually 1)
|
||||
start_sn: 0
|
||||
|
||||
# If one or both axes are reversed, this can be used to fix it
|
||||
# without having to edit all of the coordinates.
|
||||
# These should be false for most normal wafers.
|
||||
reverse_x: false
|
||||
reverse_y: false
|
||||
|
||||
# these are sensors whose labels need to be repositioned. Sensor numbers
|
||||
# start at start_sn. Directions are up, down, left, and right, and values
|
||||
# are multiples of the marker size.
|
||||
label_exceptions: {
|
||||
33: { left: [0, 0] },
|
||||
34: { left: [0, 0] },
|
||||
41: { top: [0, 0] },
|
||||
42: { bottom: [0, 0] },
|
||||
45: { top: [1, 0] },
|
||||
46: { bottom: [0.5, 0] },
|
||||
49: { left: [0, 0] },
|
||||
50: { bottom: [-1.75, -0.5] },
|
||||
51: { top: [-1, 0] },
|
||||
52: { top: [-1, 0] },
|
||||
53: { bottom: [0, 0] },
|
||||
54: { left: [0, -0.75] },
|
||||
55: { right: [0, 0.5] },
|
||||
58: { bottom: [-0.75, 0] },
|
||||
60: { top: [0, 0] },
|
||||
62: { right: [0, 1] },
|
||||
63: { right: [0, 0] },
|
||||
64: { left: [0, 0.25] }
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
# This will be the name of the output files
|
||||
name: zwafer_rev
|
||||
wafers: [] # the reversed version doesn't represent any real wafer
|
||||
|
||||
# round or square wafer
|
||||
shape: round
|
||||
|
||||
# diameter/size of the wafer
|
||||
size: 300
|
||||
|
||||
# large or small markers and labels
|
||||
marker_size: small
|
||||
font_size: small
|
||||
|
||||
# origin settings. x_origin can be left, right, or center. y_origin can
|
||||
# be top, bottom, or center. Using right or top probably also needs
|
||||
# reverse_x/reverse_y to be set as well.
|
||||
x_origin: center
|
||||
y_origin: center
|
||||
|
||||
# sensor X and Y values, in mm relative to the wafer center
|
||||
X: [0.00, -1.17, 8.92, 1.17, -8.92, 22.52, 29.35, -22.52, -29.35, 45.05, 73.37,
|
||||
58.71, 9.66, -45.05, -73.37, -58.71, -9.66, 79.85, 89.06, 46.10, -23.86,
|
||||
-79.85, -89.06, -46.10, 23.86, 66.96, 109.06, 87.27, 14.36, -66.96,
|
||||
-109.06, -87.27, -14.36, 121.24, 135.23, 70.00, -36.23, -121.24, -135.23,
|
||||
-70.00, 36.23, 124.71, 139.09, 72.00, -37.27, -124.71, -139.09, -72.00,
|
||||
37.27, 89.49, 127.31, 145.74, 141.99, 116.62, 73.50, 19.19, -38.05, -89.49,
|
||||
-127.31, -145.74, -141.99, -116.62, -73.50, -19.19, 38.05]
|
||||
Y: [0.00, -8.92, -1.17, 8.92, 1.17, -29.35, 22.52, 29.35, -22.52, -58.71,
|
||||
-9.66, 45.05, 73.37, 58.71, 9.66, -45.05, -73.37, -46.10, 23.86, 79.85,
|
||||
89.06, 46.10, -23.86, -79.85, -89.06, -87.27, -14.36, 66.96, 109.06, 87.27,
|
||||
14.36, -66.96, -109.06, -70.00, 36.23, 121.24, 135.23, 70.00, -36.23,
|
||||
-121.24, -135.23, -72.00, 37.27, 124.71, 139.09, 72.00, -37.27, -124.71,
|
||||
-139.09, -116.62, -73.50, -19.19, 38.05, 89.49, 127.31, 145.74, 141.99,
|
||||
116.62, 73.50, 19.19, -38.05, -89.49, -127.31, -145.74, -141.99]
|
||||
|
||||
# set this to either 0 or 1 for the initial sensor number (usually 1)
|
||||
start_sn: 0
|
||||
|
||||
# If one or both axes are reversed, this can be used to fix it
|
||||
# without having to edit all of the coordinates.
|
||||
# These should be false for most normal wafers.
|
||||
reverse_x: true
|
||||
reverse_y: true
|
||||
|
||||
# these are sensors whose labels need to be repositioned. Sensor numbers
|
||||
# start at start_sn. Directions are up, down, left, and right, and values
|
||||
# are multiples of the marker size.
|
||||
label_exceptions: {
|
||||
37: { left: [0, 0] },
|
||||
38: { left: [0, 0] },
|
||||
39: { left: [0, 0] },
|
||||
41: { top: [1.5, 0] },
|
||||
42: { bottom: [1, 0] },
|
||||
43: { left: [0, -1] },
|
||||
45: { bottom: [-1, -1] },
|
||||
46: { bottom: [0, -1] },
|
||||
47: { right: [0, 1] },
|
||||
48: { left: [0, 0] },
|
||||
50: { bottom: [-1, 0] },
|
||||
52: { top: [0, 0] },
|
||||
54: { right: [0.5, 1] },
|
||||
56: { left: [0, 0.5] },
|
||||
57: { left: [0, 0] },
|
||||
58: { top: [0.5, 0] },
|
||||
59: { left: [0, 0] },
|
||||
60: { top: [-1, 0] },
|
||||
61: { left: [0, 0] },
|
||||
62: { left: [0, -1] },
|
||||
63: { right: [0, 1] },
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
# ===== Backend Package Marker =====
|
||||
+83
-1
@@ -1,16 +1,98 @@
|
||||
# ===== Build System =====
|
||||
[build-system]
|
||||
requires = ["setuptools>=61"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
# ===== Project Metadata =====
|
||||
[project]
|
||||
name = "pygui"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"PySide6>=6.6.0",
|
||||
"pyqtgraph>=0.13.0",
|
||||
"numpy>=1.24.0",
|
||||
"pyserial>=3.5",
|
||||
"pandas>=2.0.0",
|
||||
"matplotlib>=3.7.0",
|
||||
"scipy>=1.17.1",
|
||||
"pyyaml>=6.0.3",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = ["pytest"]
|
||||
|
||||
# ===== Console Entry Point =====
|
||||
[project.scripts]
|
||||
isc = "pygui.__main__:main"
|
||||
|
||||
# ===== Package Discovery (src layout) =====
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
# Ship the QML module alongside the Python package.
|
||||
pygui = ["ISC/**/*.qml", "ISC/**/qmldir"]
|
||||
|
||||
# ===== PySide Build Inputs =====
|
||||
[tool.pyside6-project]
|
||||
files = ["ISC/HomePage.qml", "ISC/Main.qml", "ISC/Tabs/DataTab.qml", "ISC/Tabs/GraphTab.qml", "ISC/Tabs/SelectFileDialog.qml", "ISC/Tabs/SettingsTab.qml", "ISC/Tabs/StatusTab.qml", "ISC/Tabs/qmldir", "ISC/Theme.qml", "ISC/qmldir", "backend/data_model.py", "backend/device_controller.py", "backend/graph_view.py", "backend/file_browser.py", "backend/local_settings.py", "backend/local_settings_model.py", "main.py", "serialcomm/__init__.py", "serialcomm/data_parser.py", "serialcomm/serial_port.py", "serialcomm/device_service.py"]
|
||||
files = [
|
||||
"src/pygui/ISC/HomePage.qml",
|
||||
"src/pygui/ISC/Main.qml",
|
||||
"src/pygui/ISC/Theme.qml",
|
||||
"src/pygui/ISC/qmldir",
|
||||
"src/pygui/ISC/Tabs/DataTab.qml",
|
||||
"src/pygui/ISC/Tabs/SelectFileDialog.qml",
|
||||
"src/pygui/ISC/Tabs/SettingsTab.qml",
|
||||
"src/pygui/ISC/Tabs/StatusTab.qml",
|
||||
"src/pygui/ISC/Tabs/qmldir",
|
||||
"src/pygui/__main__.py",
|
||||
"src/pygui/backend/contour_models.py",
|
||||
"src/pygui/backend/crypto_helper.py",
|
||||
"src/pygui/backend/csv_file_metadata.py",
|
||||
"src/pygui/backend/data_model.py",
|
||||
"src/pygui/backend/data_segment.py",
|
||||
"src/pygui/backend/device_controller.py",
|
||||
"src/pygui/backend/file_browser.py",
|
||||
"src/pygui/backend/frame.py",
|
||||
"src/pygui/backend/graph_view.py",
|
||||
"src/pygui/backend/local_settings.py",
|
||||
"src/pygui/backend/local_settings_model.py",
|
||||
"src/pygui/backend/marching_squares.py",
|
||||
"src/pygui/backend/zwafer_models.py",
|
||||
"src/pygui/backend/zwafer_parser.py",
|
||||
"src/pygui/backend/frame_stats.py",
|
||||
"src/pygui/backend/threshold_classifier.py",
|
||||
"src/pygui/backend/stability_detector.py",
|
||||
"src/pygui/backend/csv_recorder.py",
|
||||
"src/pygui/backend/frame_player.py",
|
||||
"src/pygui/backend/stream_reader.py",
|
||||
"src/pygui/backend/session_model.py",
|
||||
"src/pygui/backend/session_controller.py",
|
||||
"src/pygui/backend/wafer_layouts.py",
|
||||
"src/pygui/backend/rbf_heatmap.py",
|
||||
"src/pygui/backend/wafer_map_item.py",
|
||||
"src/pygui/serialcomm/__init__.py",
|
||||
"src/pygui/serialcomm/data_parser.py",
|
||||
"src/pygui/serialcomm/device_service.py",
|
||||
"src/pygui/serialcomm/serial_port.py",
|
||||
"src/pygui/ISC/Tabs/WaferMapTab.qml",
|
||||
"src/pygui/ISC/Tabs/components/WaferMapView.qml",
|
||||
"src/pygui/ISC/Tabs/components/SourcePanel.qml",
|
||||
"src/pygui/ISC/Tabs/components/ReadoutPanel.qml",
|
||||
"src/pygui/ISC/Tabs/components/TransportBar.qml",
|
||||
"src/pygui/ISC/Tabs/components/ReplaceSensorDialog.qml",
|
||||
"src/pygui/ISC/Tabs/components/qmldir",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest>=9.0.3",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
environments = [
|
||||
"sys_platform == 'darwin'",
|
||||
"sys_platform == 'win32'",
|
||||
"sys_platform == 'linux'",
|
||||
]
|
||||
|
||||
@@ -3,3 +3,7 @@ PySide6>=6.6.0
|
||||
pyqtgraph>=0.13.0
|
||||
numpy>=1.24.0
|
||||
pyserial>=3.5
|
||||
pandas>=2.0.0
|
||||
matplotlib>=3.7.0
|
||||
scipy>=1.10.0
|
||||
pyyaml>=6.0
|
||||
|
||||
@@ -24,7 +24,42 @@ Rectangle {
|
||||
property int selectedTabIndex: 0
|
||||
property int selectedSideActionIndex: -1 // nothing active on startup
|
||||
|
||||
// ===== Main Two-Column Layout =====
|
||||
property bool memoryRead: false
|
||||
property bool waferDetected: false
|
||||
|
||||
Connections {
|
||||
target: deviceController
|
||||
function onDetectResult(result){
|
||||
//result is a waferInfo dict on success, None on failure
|
||||
root.waferDetected = (result != null && result!= undefined)
|
||||
}
|
||||
function onParsedDataReady(result) {
|
||||
if (result.success && result.csv_path) {
|
||||
// Load the freshly read CSV into the player
|
||||
streamController.loadFile(result.csv_path)
|
||||
|
||||
// Automatically switch to the "Wafer Map Tab" (index 3)
|
||||
root.selectedTabIndex = 3
|
||||
}
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: deviceController
|
||||
function onReadResult(result) {
|
||||
// result has "success" key on success, "error" key on failure
|
||||
root.memoryRead = (result !== null &&
|
||||
result !== undefined &&
|
||||
result.success === true)
|
||||
|
||||
if (root.memoryRead) {
|
||||
console.log(`[P1.1] Memory read complete: ${result.bytes} bytes`)
|
||||
} else {
|
||||
console.log(`[P1.1] Read failed: ${result.error || "unknown"}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Main Two-Column Layout ======
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
@@ -53,6 +88,16 @@ Rectangle {
|
||||
Button {
|
||||
id: control
|
||||
text: modelData
|
||||
enabled: {
|
||||
switch (index) {
|
||||
case 0: return true // DETECT WAFER
|
||||
case 1: return root.waferDetected // READ MEMORY
|
||||
case 2: return root.memoryRead // OPEN CSV IN EXCEL
|
||||
case 3: return root.waferDetected // ERASE MEMORY
|
||||
case 4: return root.memoryRead // IMPORT DATA
|
||||
default: return true // STORED DATA
|
||||
}
|
||||
}
|
||||
property bool isActive: index === root.selectedSideActionIndex
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Math.max(Theme.sideButtonMinHeight, sideRail.computedButtonHeight)
|
||||
@@ -60,9 +105,20 @@ Rectangle {
|
||||
onClicked: {
|
||||
root.selectedSideActionIndex = index
|
||||
root.selectedTabIndex = 0 // always jump to Status tab
|
||||
if (index === 0) deviceController.detectWafer()
|
||||
else if (index === 1) deviceController.readMemoryAsync()
|
||||
else if (index === 2) deviceController.openCsvFile()
|
||||
if (index === 0) {
|
||||
root.memoryRead = false
|
||||
streamController.setMode("review")
|
||||
streamController.stopStream()
|
||||
deviceController.detectWafer()
|
||||
}
|
||||
else if (index === 1) {
|
||||
streamController.setMode("review")
|
||||
streamController.stopStream()
|
||||
deviceController.readMemoryAsync()
|
||||
}
|
||||
else if (index === 2) {
|
||||
deviceController.openCsvFile()
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
@@ -158,11 +214,17 @@ Rectangle {
|
||||
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
visible: parent.tabName !== "Settings" && parent.tabName !== "Status" && parent.tabName !== "Data"
|
||||
visible: parent.tabName !== "Settings" && parent.tabName !== "Status" && parent.tabName !== "Data" && parent.tabName !== "Wafer Map"
|
||||
text: parent.tabName + " content"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 20
|
||||
}
|
||||
|
||||
Loader{
|
||||
anchors.fill: parent
|
||||
active: parent.tabName === "Wafer Map"
|
||||
source: parent.tabName === "Wafer Map" ? "Tabs/WaferMapTab.qml" : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,6 +238,7 @@ Rectangle {
|
||||
color: Theme.tabBarBackground
|
||||
border.color: Theme.workspaceBorder
|
||||
border.width: Theme.borderThin
|
||||
radius: Theme.radiusMd
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
@@ -107,25 +107,29 @@ Item {
|
||||
x: toggle.leftPadding
|
||||
y: parent.height / 2 - height / 2
|
||||
radius: Theme.radiusXs
|
||||
color: toggle.checked ? Theme.primaryAccent : Theme.fieldBackground
|
||||
color: toggle.checked ? Theme.primaryAccent : "transparent"
|
||||
border.width: Theme.borderThin
|
||||
border.color: toggle.checked ? Theme.primaryAccent : Theme.fieldBorder
|
||||
|
||||
Rectangle {
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
width: 9
|
||||
height: 9
|
||||
radius: Theme.radiusXs
|
||||
text: "✓"
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
color: Theme.panelBackground
|
||||
visible: toggle.checked
|
||||
color: Theme.fieldBackground
|
||||
// Small nudge to center the checkmark perfectly
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: toggle.text
|
||||
color: Theme.checkboxText
|
||||
color: Theme.headingColor
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: toggle.indicator.width + toggle.spacing
|
||||
font.pixelSize: 13
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Dialogs
|
||||
import ISC
|
||||
|
||||
// ===== Status Tab =====
|
||||
@@ -29,6 +30,28 @@ ColumnLayout {
|
||||
property string csvPath: ""
|
||||
property bool dataParsed: false
|
||||
|
||||
function cleanFolderUrl(url) {
|
||||
return decodeURIComponent(String(url).replace(/^file:\/\//, ""))
|
||||
}
|
||||
|
||||
function currentFamilyCode() {
|
||||
var info = deviceController.lastWaferInfo
|
||||
return info && info.length > 0 ? (info[0] || "") : ""
|
||||
}
|
||||
|
||||
function parseAndSavePendingRead() {
|
||||
deviceController.parseAndSaveData(root.currentFamilyCode(), deviceController.selectedPort || "")
|
||||
}
|
||||
|
||||
FolderDialog {
|
||||
id: saveDirDialog
|
||||
title: "Choose CSV Save Folder"
|
||||
onAccepted: {
|
||||
deviceController.setSaveDataDir(root.cleanFolderUrl(selectedFolder))
|
||||
root.parseAndSavePendingRead()
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Empty state — nothing shown until detect fires =====
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
@@ -47,11 +70,7 @@ ColumnLayout {
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 86
|
||||
color: {
|
||||
if (deviceController.connectionStatus === "Connected") return Theme.statusSuccessColor + "22"
|
||||
if (deviceController.connectionStatus === "Disconnected") return Theme.statusErrorColor + "22"
|
||||
return Theme.cardBackground
|
||||
}
|
||||
color: Theme.cardBackground
|
||||
border.color: {
|
||||
if (deviceController.connectionStatus === "Connected") return Theme.statusSuccessColor
|
||||
if (deviceController.connectionStatus === "Disconnected") return Theme.statusErrorColor
|
||||
@@ -187,27 +206,105 @@ ColumnLayout {
|
||||
}
|
||||
|
||||
// --- Activity Log ---
|
||||
GroupBox {
|
||||
title: "Activity Log"
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: Theme.cardBackground
|
||||
border.color: Theme.cardBorder
|
||||
radius: Theme.radiusMd
|
||||
clip: true
|
||||
|
||||
ScrollView {
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
spacing: 0
|
||||
|
||||
TextArea {
|
||||
id: activityLog
|
||||
width: parent.width
|
||||
text: ""
|
||||
readOnly: true
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 12
|
||||
wrapMode: TextArea.WordWrap
|
||||
leftPadding: 4
|
||||
rightPadding: 4
|
||||
color: Theme.bodyColor
|
||||
}
|
||||
// Header
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 40
|
||||
color: Theme.subtleSectionBackground
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.panelPadding
|
||||
anchors.rightMargin: Theme.panelPadding
|
||||
|
||||
Label {
|
||||
text: "ACTIVITY LOG"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 10
|
||||
font.letterSpacing: 1.8
|
||||
font.weight: Font.Medium
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "REFRESH SESSION"
|
||||
font.pixelSize: 10
|
||||
implicitHeight: 24
|
||||
hoverEnabled: true
|
||||
background: Rectangle {
|
||||
color: parent.hovered ? Theme.buttonNeutralHover : "transparent"
|
||||
radius: Theme.radiusSm
|
||||
}
|
||||
contentItem: Text {
|
||||
text: parent.text
|
||||
color: Theme.bodyColor
|
||||
font: parent.font
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
onClicked: deviceController.clearSession()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "CLEAR LOG"
|
||||
font.pixelSize: 10
|
||||
implicitHeight: 24
|
||||
hoverEnabled: true
|
||||
background: Rectangle {
|
||||
color: parent.hovered ? Theme.buttonNeutralHover : "transparent"
|
||||
radius: Theme.radiusSm
|
||||
}
|
||||
contentItem: Text {
|
||||
text: parent.text
|
||||
color: Theme.bodyColor
|
||||
font: parent.font
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
onClicked: deviceController.clearActivityLog()
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 1
|
||||
color: Theme.cardBorder
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.margins: Theme.panelPadding
|
||||
clip: true
|
||||
|
||||
TextArea {
|
||||
id: activityLog
|
||||
width: parent.width
|
||||
text: ""
|
||||
readOnly: true
|
||||
font.family: "monospace"
|
||||
font.pixelSize: 12
|
||||
wrapMode: TextArea.WordWrap
|
||||
color: Theme.bodyColor
|
||||
background: null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
@@ -271,6 +368,9 @@ ColumnLayout {
|
||||
root.dataRows = 0
|
||||
root.dataCols = 0
|
||||
root.csvPath = ""
|
||||
|
||||
if (result && result.success === true)
|
||||
saveDirDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import ISC
|
||||
import ISC.Tabs.components
|
||||
|
||||
Item {
|
||||
id: root
|
||||
anchors.fill: parent
|
||||
|
||||
// Live elapsed-time counter
|
||||
property int _liveSecs: 0
|
||||
Timer {
|
||||
id: liveTimer
|
||||
interval: 1000
|
||||
repeat: true
|
||||
running: streamController.mode === "live" && streamController.state !== "idle"
|
||||
onTriggered: root._liveSecs++
|
||||
onRunningChanged: if (!running) root._liveSecs = 0
|
||||
}
|
||||
function fmtTime(s) {
|
||||
var m = Math.floor(s / 60)
|
||||
var ss = s % 60
|
||||
return (m < 10 ? "0" : "") + m + ":" + (ss < 10 ? "0" : "") + ss
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 16
|
||||
|
||||
// ── Toolbar ───────────────────────────────────────────────────────
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
|
||||
// Mode toggle
|
||||
TabBar {
|
||||
id: modeBar
|
||||
currentIndex: 1 // Default to "Review"
|
||||
spacing: 2
|
||||
padding: 2
|
||||
background: Rectangle {
|
||||
color: Theme.subtleSectionBackground
|
||||
radius: Theme.radiusSm
|
||||
border.color: Theme.cardBorder
|
||||
border.width: Theme.borderThin
|
||||
}
|
||||
TabButton {
|
||||
text: "Live"
|
||||
enabled: deviceController.connectionStatus === "Connected"
|
||||
opacity: enabled ? 1.0 : 0.4
|
||||
implicitWidth: 58; implicitHeight: 28
|
||||
background: Rectangle {
|
||||
color: parent.checked ? Theme.tabActiveBackground : "transparent"
|
||||
radius: Theme.radiusSm - 1
|
||||
}
|
||||
contentItem: Text {
|
||||
text: parent.text
|
||||
color: parent.checked ? Theme.headingColor : Theme.bodyColor
|
||||
font.pixelSize: 11
|
||||
font.weight: parent.checked ? Font.Medium : Font.Normal
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
TabButton {
|
||||
text: "Review"
|
||||
implicitWidth: 64; implicitHeight: 28
|
||||
background: Rectangle {
|
||||
color: parent.checked ? Theme.tabActiveBackground : "transparent"
|
||||
radius: Theme.radiusSm - 1
|
||||
}
|
||||
contentItem: Text {
|
||||
text: parent.text
|
||||
color: parent.checked ? Theme.headingColor : Theme.bodyColor
|
||||
font.pixelSize: 11
|
||||
font.weight: parent.checked ? Font.Medium : Font.Normal
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
onCurrentIndexChanged:
|
||||
if (currentIndex === 0){
|
||||
// Entering Live mode, only work when wafer detected/connected
|
||||
streamController.setMode("live")
|
||||
if (deviceController.connectionStatus === "Connected") {
|
||||
var fc = ""
|
||||
if (deviceController.lastWaferInfo && deviceController.lastWaferInfo.length > 0) {
|
||||
fc = deviceController.lastWaferInfo[0]
|
||||
}
|
||||
streamController.startStream(deviceController.selectedPort, fc)
|
||||
}
|
||||
} else {
|
||||
// Entering Review Mode (automatically calls stopStream in backend)
|
||||
streamController.setMode("review")
|
||||
}
|
||||
}
|
||||
|
||||
// Live source info: WiFi icon + port · serial
|
||||
RowLayout {
|
||||
visible: streamController.mode === "live"
|
||||
spacing: 5
|
||||
// WiFi icon (Unicode approximation; swap for SVG if available)
|
||||
Label {
|
||||
id: liveIndicator
|
||||
text: "◉"
|
||||
color: (deviceController.connectionStatus === "Connected" && streamController.state !== "idle")
|
||||
? Theme.liveColor : Theme.bodyColor
|
||||
font.pixelSize: 13
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
running: deviceController.connectionStatus === "Connected" && streamController.state !== "idle"
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation { to: 0.2; duration: 600; easing.type: Easing.InOutQuad }
|
||||
NumberAnimation { to: 1.0; duration: 600; easing.type: Easing.InOutQuad }
|
||||
onRunningChanged: {
|
||||
if (!running) liveIndicator.opacity = 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
Label {
|
||||
text: "Live stream"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
// SET badge + avg temp
|
||||
Rectangle {
|
||||
visible: streamController.state === "set"
|
||||
height: 22; radius: Theme.radiusSm
|
||||
color: "transparent"
|
||||
border.color: Theme.liveColor
|
||||
border.width: 1
|
||||
implicitWidth: setLabel.implicitWidth + 16
|
||||
Label {
|
||||
id: setLabel
|
||||
anchors.centerIn: parent
|
||||
text: "SET " + (streamController.stats.avg !== undefined
|
||||
? streamController.stats.avg + " avg" : "")
|
||||
color: Theme.liveColor
|
||||
font.pixelSize: 10
|
||||
font.weight: Font.Medium
|
||||
font.letterSpacing: 0.8
|
||||
}
|
||||
}
|
||||
|
||||
// Record indicator
|
||||
RowLayout {
|
||||
spacing: 5
|
||||
visible: streamController.recording
|
||||
Rectangle {
|
||||
width: 7; height: 7; radius: 4
|
||||
color: Theme.recordColor
|
||||
SequentialAnimation on opacity {
|
||||
running: streamController.recording
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation { to: 0.2; duration: 600 }
|
||||
NumberAnimation { to: 1.0; duration: 600 }
|
||||
}
|
||||
}
|
||||
Label {
|
||||
text: "REC"
|
||||
color: Theme.recordColor
|
||||
font.pixelSize: 10
|
||||
font.weight: Font.Medium
|
||||
font.letterSpacing: 1.2
|
||||
}
|
||||
}
|
||||
|
||||
// LIVE timer
|
||||
RowLayout {
|
||||
spacing: 5
|
||||
visible: streamController.mode === "live" && streamController.state !== "idle"
|
||||
Rectangle {
|
||||
width: 7; height: 7; radius: 4
|
||||
color: Theme.liveColor
|
||||
}
|
||||
Label {
|
||||
text: "LIVE " + root.fmtTime(root._liveSecs)
|
||||
color: Theme.liveColor
|
||||
font.pixelSize: 10
|
||||
font.weight: Font.Medium
|
||||
font.letterSpacing: 0.8
|
||||
}
|
||||
}
|
||||
|
||||
// IDLE label (review / not connected)
|
||||
Label {
|
||||
visible: streamController.mode === "review" || streamController.state === "idle"
|
||||
text: streamController.state.toUpperCase()
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 11
|
||||
font.weight: Font.Medium
|
||||
font.letterSpacing: 1.2
|
||||
}
|
||||
}
|
||||
|
||||
// ── Body: 3 zones ─────────────────────────────────────────────────
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: 16
|
||||
|
||||
// Source panel — bordered card
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 220
|
||||
Layout.fillHeight: true
|
||||
color: Theme.cardBackground
|
||||
border.color: Theme.cardBorder
|
||||
border.width: 1
|
||||
radius: Theme.radiusMd
|
||||
clip: true
|
||||
|
||||
SourcePanel {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: 8
|
||||
|
||||
WaferMapView {
|
||||
id: waferView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
blend: readoutPanel.heatmapBlend
|
||||
showLabels: readoutPanel.showLabels
|
||||
}
|
||||
|
||||
// Horizontal separation line with vertical padding
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 32
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.cardBorder
|
||||
}
|
||||
}
|
||||
|
||||
TransportBar { Layout.fillWidth: true }
|
||||
}
|
||||
|
||||
// Readout panel — bordered card
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 220
|
||||
Layout.fillHeight: true
|
||||
color: Theme.cardBackground
|
||||
border.color: Theme.cardBorder
|
||||
border.width: 1
|
||||
radius: Theme.radiusMd
|
||||
clip: true
|
||||
|
||||
ReadoutPanel {
|
||||
id: readoutPanel
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import ISC
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
property var s: streamController.stats
|
||||
property alias showLabels: labelsToggle.checked
|
||||
property alias heatmapBlend: heatmapSlider.value
|
||||
|
||||
component PanelCheckBox: CheckBox {
|
||||
id: toggle
|
||||
indicator: Rectangle {
|
||||
implicitWidth: 18
|
||||
implicitHeight: 18
|
||||
x: toggle.leftPadding
|
||||
y: parent.height / 2 - height / 2
|
||||
radius: Theme.radiusXs
|
||||
color: toggle.checked ? Theme.primaryAccent : "transparent"
|
||||
border.width: Theme.borderThin
|
||||
border.color: toggle.checked ? Theme.primaryAccent : Theme.fieldBorder
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "✓"
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
color: Theme.panelBackground
|
||||
visible: toggle.checked
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: toggle.text
|
||||
color: Theme.headingColor
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: toggle.indicator.width + toggle.spacing
|
||||
font.pixelSize: toggle.font.pixelSize || 11
|
||||
}
|
||||
}
|
||||
|
||||
// ── READOUT ───────────────────────────────────────────────────────────
|
||||
Label {
|
||||
text: "READOUT"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 10
|
||||
font.letterSpacing: 1.8
|
||||
font.weight: Font.Medium
|
||||
topPadding: 6
|
||||
bottomPadding: 8
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: statsLayout.implicitHeight + 20
|
||||
color: Theme.subtleSectionBackground
|
||||
radius: Theme.radiusSm
|
||||
border.color: Theme.cardBorder
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
id: statsLayout
|
||||
anchors { fill: parent; margins: 10 }
|
||||
spacing: 8
|
||||
|
||||
// Min Temp Row
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Label { text: "Min Temp"; color: Theme.bodyColor; font.pixelSize: 11 }
|
||||
Item { Layout.fillWidth: true }
|
||||
RowLayout {
|
||||
spacing: 4
|
||||
Label {
|
||||
text: s.min !== undefined ? s.min : "—"
|
||||
color: Theme.sensorLow
|
||||
font.pixelSize: 13; font.weight: Font.Bold
|
||||
}
|
||||
Label {
|
||||
text: s.minIndex !== undefined ? "#" + s.minIndex : ""
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Max Temp Row
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Label { text: "Max Temp"; color: Theme.bodyColor; font.pixelSize: 11 }
|
||||
Item { Layout.fillWidth: true }
|
||||
RowLayout {
|
||||
spacing: 4
|
||||
Label {
|
||||
text: s.max !== undefined ? s.max : "—"
|
||||
color: Theme.sensorHigh
|
||||
font.pixelSize: 13; font.weight: Font.Bold
|
||||
}
|
||||
Label {
|
||||
text: s.maxIndex !== undefined ? "#" + s.maxIndex : ""
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder }
|
||||
|
||||
// Differential Row
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Label { text: "Differential"; color: Theme.bodyColor; font.pixelSize: 11 }
|
||||
Item { Layout.fillWidth: true }
|
||||
Label {
|
||||
text: s.diff !== undefined ? s.diff : "—"
|
||||
color: Theme.headingColor
|
||||
font.pixelSize: 13; font.weight: Font.Bold
|
||||
}
|
||||
}
|
||||
|
||||
// Average Row
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Label { text: "Average"; color: Theme.bodyColor; font.pixelSize: 11 }
|
||||
Item { Layout.fillWidth: true }
|
||||
Label {
|
||||
text: s.avg !== undefined ? s.avg : "—"
|
||||
color: Theme.headingColor
|
||||
font.pixelSize: 13; font.weight: Font.Bold
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder }
|
||||
|
||||
// Sigma Row
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Label { text: "Sigma (σ)"; color: Theme.bodyColor; font.pixelSize: 11 }
|
||||
Item { Layout.fillWidth: true }
|
||||
Label {
|
||||
text: s.sigma !== undefined ? s.sigma : "—"
|
||||
color: Theme.headingColor
|
||||
font.pixelSize: 13; font.weight: Font.Bold
|
||||
}
|
||||
}
|
||||
|
||||
// 3-Sigma Row
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Label { text: "3σ Value"; color: Theme.bodyColor; font.pixelSize: 11 }
|
||||
Item { Layout.fillWidth: true }
|
||||
Label {
|
||||
text: s.threeSigma !== undefined ? s.threeSigma : "—"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 12; font.weight: Font.Medium
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { height: 12 }
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder }
|
||||
Item { height: 12 }
|
||||
|
||||
// ── DISPLAY ───────────────────────────────────────────────────────────
|
||||
Label {
|
||||
text: "DISPLAY"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 10
|
||||
font.letterSpacing: 1.8
|
||||
font.weight: Font.Medium
|
||||
topPadding: 6
|
||||
bottomPadding: 8
|
||||
}
|
||||
|
||||
PanelCheckBox {
|
||||
id: labelsToggle
|
||||
text: "Labels"
|
||||
checked: true
|
||||
font.pixelSize: 11
|
||||
}
|
||||
|
||||
PanelCheckBox {
|
||||
id: clusterAverageToggle
|
||||
text: "Average Clusters"
|
||||
checked: streamController.clusterAveragingEnabled
|
||||
font.pixelSize: 11
|
||||
onCheckedChanged: streamController.clusterAveragingEnabled = checked
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
Label {
|
||||
text: "Heatmap"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 11
|
||||
Layout.preferredWidth: 52
|
||||
}
|
||||
Slider {
|
||||
id: heatmapSlider
|
||||
from: 0; to: 1; value: 0
|
||||
Layout.fillWidth: true
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: Math.round(value * 100) + "%"
|
||||
}
|
||||
}
|
||||
|
||||
Item { height: 12 }
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder }
|
||||
Item { height: 12 }
|
||||
|
||||
// ── THRESHOLDS ────────────────────────────────────────────────────────
|
||||
Label {
|
||||
text: "THRESHOLDS"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 10
|
||||
font.letterSpacing: 1.8
|
||||
font.weight: Font.Medium
|
||||
topPadding: 6
|
||||
bottomPadding: 8
|
||||
}
|
||||
|
||||
Label {
|
||||
text: "Set Point (°C)"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 11
|
||||
bottomPadding: 3
|
||||
}
|
||||
TextField {
|
||||
id: spField
|
||||
Layout.fillWidth: true
|
||||
text: "149.0"
|
||||
font.pixelSize: 12
|
||||
inputMethodHints: Qt.ImhFormattedNumbersOnly
|
||||
color: Theme.fieldText
|
||||
placeholderTextColor: Theme.fieldPlaceholder
|
||||
selectedTextColor: Theme.fieldBackground
|
||||
selectionColor: Theme.fieldText
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusXs
|
||||
color: Theme.fieldBackground
|
||||
border.width: spField.activeFocus ? Theme.borderStrong : Theme.borderThin
|
||||
border.color: spField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
|
||||
}
|
||||
onEditingFinished: pushThresholds()
|
||||
}
|
||||
|
||||
Item { height: 10 }
|
||||
|
||||
Label {
|
||||
text: "Margin (±°C)"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 11
|
||||
bottomPadding: 3
|
||||
}
|
||||
TextField {
|
||||
id: mgField
|
||||
Layout.fillWidth: true
|
||||
text: "1.0"
|
||||
font.pixelSize: 12
|
||||
inputMethodHints: Qt.ImhFormattedNumbersOnly
|
||||
color: Theme.fieldText
|
||||
placeholderTextColor: Theme.fieldPlaceholder
|
||||
selectedTextColor: Theme.fieldBackground
|
||||
selectionColor: Theme.fieldText
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusXs
|
||||
color: Theme.fieldBackground
|
||||
border.width: mgField.activeFocus ? Theme.borderStrong : Theme.borderThin
|
||||
border.color: mgField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
|
||||
}
|
||||
onEditingFinished: pushThresholds()
|
||||
}
|
||||
|
||||
Item { height: 10 }
|
||||
|
||||
PanelCheckBox {
|
||||
id: autoCheck
|
||||
text: "Auto range (mean ± 1σ)"
|
||||
checked: true
|
||||
font.pixelSize: 11
|
||||
onCheckedChanged: pushThresholds()
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
|
||||
function pushThresholds() {
|
||||
var sp = parseFloat(spField.text) || 149.0
|
||||
var mg = parseFloat(mgField.text) || 1.0
|
||||
streamController.setThresholds(sp, mg, autoCheck.checked)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import ISC
|
||||
|
||||
Dialog {
|
||||
id: root
|
||||
title: "Sensor Override"
|
||||
modal: true
|
||||
standardButtons: Dialog.NoButton
|
||||
width: 300
|
||||
|
||||
// called by WaferMapView's TapHandler
|
||||
property int sensorIndex: -1
|
||||
property string sensorLabel: ""
|
||||
property real currentValue: 0.0
|
||||
property bool hasOverride: streamController.overriddenSensors.indexOf(sensorIndex) >= 0
|
||||
|
||||
function openFor(index) {
|
||||
sensorIndex = index
|
||||
var dot = streamController.sensorDots[index]
|
||||
if (!dot) return
|
||||
sensorLabel = dot.label !== undefined ? String(dot.label) : String(index + 1)
|
||||
currentValue = dot.value
|
||||
valueField.text = ""
|
||||
offsetField.text = ""
|
||||
open()
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Theme.cardBackground
|
||||
radius: Theme.radiusLg
|
||||
border.color: Theme.cardBorder
|
||||
border.width: Theme.borderThin
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.rightPaneGap
|
||||
|
||||
// ── header ───────────────────────────────────────────────
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Label {
|
||||
text: "Sensor #" + (root.sensorIndex + 1) +
|
||||
(root.sensorLabel ? " (" + root.sensorLabel + ")" : "")
|
||||
color: Theme.headingColor
|
||||
font.bold: true
|
||||
font.pixelSize: 13
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Rectangle {
|
||||
visible: root.hasOverride
|
||||
width: overrideTag.implicitWidth + 12
|
||||
height: overrideTag.implicitHeight + 6
|
||||
radius: 4
|
||||
color: Theme.statusWarningColor
|
||||
opacity: 0.85
|
||||
Label {
|
||||
id: overrideTag
|
||||
anchors.centerIn: parent
|
||||
text: "OVERRIDE"
|
||||
color: "white"
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
font.letterSpacing: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: "Live: " + root.currentValue.toFixed(2)
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder }
|
||||
|
||||
// ── replace field ─────────────────────────────────────────
|
||||
Label { text: "Replace with value"; color: Theme.bodyColor; font.pixelSize: 11 }
|
||||
TextField {
|
||||
id: valueField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "100"
|
||||
inputMethodHints: Qt.ImhFormattedNumbersOnly
|
||||
color: Theme.fieldText
|
||||
placeholderTextColor: Theme.fieldPlaceholder
|
||||
selectedTextColor: Theme.fieldBackground
|
||||
selectionColor: Theme.fieldText
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusXs
|
||||
color: Theme.fieldBackground
|
||||
border.width: valueField.activeFocus ? Theme.borderStrong : Theme.borderThin
|
||||
border.color: valueField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
|
||||
}
|
||||
}
|
||||
|
||||
// ── offset field ──────────────────────────────────────────
|
||||
Label { text: "Or add offset (± delta)"; color: Theme.bodyColor; font.pixelSize: 11 }
|
||||
TextField {
|
||||
id: offsetField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "e.g. +0.5 or -0.5 (blank = skip)"
|
||||
inputMethodHints: Qt.ImhFormattedNumbersOnly
|
||||
color: Theme.fieldText
|
||||
placeholderTextColor: Theme.fieldPlaceholder
|
||||
selectedTextColor: Theme.fieldBackground
|
||||
selectionColor: Theme.fieldText
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusXs
|
||||
color: Theme.fieldBackground
|
||||
border.width: offsetField.activeFocus ? Theme.borderStrong : Theme.borderThin
|
||||
border.color: offsetField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder }
|
||||
|
||||
// ── buttons ───────────────────────────────────────────────
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Button {
|
||||
id: applyBtn
|
||||
text: "Apply"
|
||||
Layout.fillWidth: true
|
||||
hoverEnabled: true
|
||||
enabled: valueField.text !== "" || offsetField.text !== ""
|
||||
opacity: enabled ? 1.0 : 0.4
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusSm
|
||||
color: applyBtn.down ? Theme.buttonNeutralPressed : applyBtn.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground
|
||||
border.width: Theme.borderThin
|
||||
border.color: Theme.fieldBorder
|
||||
}
|
||||
contentItem: Text {
|
||||
text: applyBtn.text
|
||||
color: Theme.buttonNeutralText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.bold: true
|
||||
font.pixelSize: 13
|
||||
}
|
||||
onClicked: {
|
||||
if (valueField.text !== "") {
|
||||
var v = parseFloat(valueField.text)
|
||||
if (!isNaN(v))
|
||||
streamController.replaceSensor(root.sensorIndex, v)
|
||||
}
|
||||
if (offsetField.text !== "") {
|
||||
var d = parseFloat(offsetField.text)
|
||||
if (!isNaN(d))
|
||||
streamController.offsetSensor(root.sensorIndex, d)
|
||||
}
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
Button {
|
||||
id: clearBtn
|
||||
text: "Clear"
|
||||
Layout.fillWidth: true
|
||||
hoverEnabled: true
|
||||
enabled: root.hasOverride
|
||||
opacity: enabled ? 1.0 : 0.4
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusSm
|
||||
color: clearBtn.down ? Theme.buttonNeutralPressed : clearBtn.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground
|
||||
border.width: Theme.borderThin
|
||||
border.color: Theme.fieldBorder
|
||||
}
|
||||
contentItem: Text {
|
||||
text: clearBtn.text
|
||||
color: Theme.buttonNeutralText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.bold: true
|
||||
font.pixelSize: 13
|
||||
}
|
||||
onClicked: {
|
||||
streamController.clearSensorEdit(root.sensorIndex)
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
Button {
|
||||
id: cancelBtn
|
||||
text: "Cancel"
|
||||
hoverEnabled: true
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusSm
|
||||
color: cancelBtn.down ? Theme.buttonNeutralPressed : cancelBtn.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground
|
||||
border.width: Theme.borderThin
|
||||
border.color: Theme.fieldBorder
|
||||
}
|
||||
contentItem: Text {
|
||||
text: cancelBtn.text
|
||||
color: Theme.buttonNeutralText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.bold: true
|
||||
font.pixelSize: 13
|
||||
}
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import ISC
|
||||
import ".."
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 6
|
||||
|
||||
// ── Section label & Refresh ───────────────────────────────────────────
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
|
||||
Label {
|
||||
text: "SOURCE"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 10
|
||||
font.letterSpacing: 1.8
|
||||
font.weight: Font.Medium
|
||||
topPadding: 6
|
||||
bottomPadding: 8
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Button {
|
||||
id: editBtn
|
||||
text: "✎"
|
||||
implicitWidth: 26
|
||||
implicitHeight: 26
|
||||
hoverEnabled: true
|
||||
font.pixelSize: 14
|
||||
background: Rectangle {
|
||||
color: editBtn.pressed ? Theme.buttonPressed
|
||||
: editBtn.hovered ? Theme.buttonNeutralHover
|
||||
: "transparent"
|
||||
radius: Theme.radiusSm
|
||||
}
|
||||
contentItem: Text {
|
||||
text: parent.text
|
||||
color: Theme.bodyColor
|
||||
font: parent.font
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
onClicked: {
|
||||
file_browser.refreshFiles()
|
||||
csvEditorDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
id: refreshBtn
|
||||
text: "⟳"
|
||||
implicitWidth: 26
|
||||
implicitHeight: 26
|
||||
hoverEnabled: true
|
||||
font.pixelSize: 14
|
||||
background: Rectangle {
|
||||
color: refreshBtn.pressed ? Theme.buttonPressed
|
||||
: refreshBtn.hovered ? Theme.buttonNeutralHover
|
||||
: "transparent"
|
||||
radius: Theme.radiusSm
|
||||
}
|
||||
contentItem: Text {
|
||||
text: parent.text
|
||||
color: Theme.bodyColor
|
||||
font: parent.font
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
onClicked: file_browser.refreshFiles()
|
||||
}
|
||||
}
|
||||
|
||||
// ── Directory button ──────────────────────────────────────────────────
|
||||
Button {
|
||||
id: dirBtn
|
||||
Layout.fillWidth: true
|
||||
hoverEnabled: true
|
||||
implicitHeight: 32
|
||||
leftPadding: 8
|
||||
rightPadding: 8
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusSm
|
||||
color: dirBtn.pressed ? Theme.buttonNeutralPressed
|
||||
: dirBtn.hovered ? Theme.buttonNeutralHover
|
||||
: Theme.buttonNeutralBackground
|
||||
border.width: Theme.borderThin
|
||||
border.color: Theme.fieldBorder
|
||||
}
|
||||
contentItem: RowLayout {
|
||||
spacing: 6
|
||||
Label {
|
||||
text: "▤"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 12
|
||||
}
|
||||
Label {
|
||||
text: file_browser.currentDirectory.split("/").pop() || file_browser.currentDirectory
|
||||
color: Theme.headingColor
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideMiddle
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
onClicked: file_browser.chooseDirectory()
|
||||
}
|
||||
|
||||
// ── Filter ────────────────────────────────────────────────────────────
|
||||
TextField {
|
||||
id: filter
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "Filter…"
|
||||
font.pixelSize: 11
|
||||
color: Theme.fieldText
|
||||
placeholderTextColor: Theme.fieldPlaceholder
|
||||
selectedTextColor: Theme.fieldBackground
|
||||
selectionColor: Theme.fieldText
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusXs
|
||||
color: Theme.fieldBackground
|
||||
border.width: filter.activeFocus ? Theme.borderStrong : Theme.borderThin
|
||||
border.color: filter.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
|
||||
}
|
||||
}
|
||||
|
||||
// ── File list ─────────────────────────────────────────────────────────
|
||||
ListView {
|
||||
id: fileList
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
spacing: 2
|
||||
highlight: null
|
||||
highlightFollowsCurrentItem: false
|
||||
|
||||
model: file_browser.files
|
||||
|
||||
delegate: ItemDelegate {
|
||||
id: fileItem
|
||||
width: ListView.view.width
|
||||
padding: 8
|
||||
highlighted: false
|
||||
focusPolicy: Qt.NoFocus
|
||||
|
||||
readonly property bool matchesFilter: {
|
||||
if (filter.text === "") return true
|
||||
var q = filter.text.toLowerCase()
|
||||
return (modelData.baseName + modelData.waferType + modelData.date)
|
||||
.toLowerCase().indexOf(q) >= 0
|
||||
}
|
||||
|
||||
// Active only in review mode; no highlight during live streaming
|
||||
readonly property bool isActive:
|
||||
streamController.mode === "review" &&
|
||||
streamController.loadedFile !== "" &&
|
||||
modelData.fileName === streamController.loadedFile
|
||||
|
||||
visible: matchesFilter
|
||||
height: matchesFilter ? implicitHeight : 0
|
||||
|
||||
onClicked: streamController.loadFile(modelData.fileName)
|
||||
|
||||
background: Rectangle {
|
||||
color: fileItem.isActive
|
||||
? Qt.rgba(1, 1, 1, 0.06)
|
||||
: fileItem.hovered ? Qt.rgba(1, 1, 1, 0.04) : "transparent"
|
||||
radius: Theme.radiusSm
|
||||
|
||||
// Left accent bar — shown only when active
|
||||
Rectangle {
|
||||
visible: fileItem.isActive
|
||||
width: 3
|
||||
radius: 1.5
|
||||
anchors {
|
||||
top: parent.top; bottom: parent.bottom; left: parent.left
|
||||
topMargin: 5; bottomMargin: 5
|
||||
}
|
||||
color: {
|
||||
var t = modelData.waferType
|
||||
if (t === "A" || t === "E" || t === "P") return "#3B82F6"
|
||||
if (t === "B" || t === "C" || t === "D") return "#10B981"
|
||||
if (t === "Z") return "#8B5CF6"
|
||||
return Theme.headingColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: 8
|
||||
|
||||
// Wafer-type avatar (circle)
|
||||
Rectangle {
|
||||
implicitWidth: 28; implicitHeight: 28
|
||||
Layout.preferredWidth: 28
|
||||
Layout.preferredHeight: 28
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
radius: 14
|
||||
color: {
|
||||
var t = modelData.waferType
|
||||
if (t === "A" || t === "E" || t === "P") return "#1D4ED8"
|
||||
if (t === "B" || t === "C" || t === "D") return "#065F46"
|
||||
if (t === "Z") return "#7C3AED"
|
||||
return "#374151"
|
||||
}
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
text: modelData.waferType || "?"
|
||||
font.pixelSize: 12
|
||||
font.weight: Font.Bold
|
||||
color: "#FFFFFF"
|
||||
}
|
||||
}
|
||||
|
||||
// Typography hierarchy
|
||||
ColumnLayout {
|
||||
spacing: 1
|
||||
Layout.fillWidth: true
|
||||
|
||||
// Primary: wafer type + serial number
|
||||
Label {
|
||||
text: modelData.serialNumber ? (modelData.waferType + modelData.serialNumber) : modelData.baseName
|
||||
color: Theme.headingColor
|
||||
font.pixelSize: 13
|
||||
font.weight: Font.Bold
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Secondary: date · time
|
||||
RowLayout {
|
||||
spacing: 3
|
||||
Layout.fillWidth: true
|
||||
Label {
|
||||
text: {
|
||||
var d = modelData.date || ""
|
||||
return d.length >= 10 ? d.substring(0, 10) : (d || "—")
|
||||
}
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 10
|
||||
}
|
||||
Label {
|
||||
visible: (modelData.timeStr || "") !== ""
|
||||
text: "·"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 10
|
||||
opacity: 0.45
|
||||
}
|
||||
Label {
|
||||
visible: (modelData.timeStr || "") !== ""
|
||||
text: modelData.timeStr || ""
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 10
|
||||
opacity: 0.65
|
||||
}
|
||||
}
|
||||
|
||||
// Tertiary: edited metadata (chamber · notes) if any
|
||||
Label {
|
||||
visible: {
|
||||
var c = modelData.chamber || ""
|
||||
var n = modelData.notes || ""
|
||||
return c !== "" || n !== ""
|
||||
}
|
||||
text: {
|
||||
var parts = []
|
||||
if (modelData.chamber) parts.push(modelData.chamber)
|
||||
if (modelData.notes) parts.push(modelData.notes)
|
||||
return parts.join(" · ")
|
||||
}
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 9
|
||||
opacity: 0.7
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Footer ────────────────────────────────────────────────────────────
|
||||
Label {
|
||||
text: file_browser.files.length + " files · CSV only"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 10
|
||||
}
|
||||
|
||||
SelectFileDialog {
|
||||
id: csvEditorDialog
|
||||
tableModel: file_browser.files
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import ISC
|
||||
|
||||
RowLayout {
|
||||
spacing: 2
|
||||
|
||||
component TBtn: Button {
|
||||
implicitWidth: 44
|
||||
implicitHeight: 32
|
||||
hoverEnabled: true
|
||||
font.pixelSize: 13
|
||||
background: Rectangle {
|
||||
color: parent.pressed ? Theme.transportButtonHover
|
||||
: parent.hovered ? Theme.transportButtonBg
|
||||
: Theme.transportBackground
|
||||
radius: Theme.radiusSm
|
||||
}
|
||||
contentItem: Text {
|
||||
text: parent.text
|
||||
color: Theme.headingColor
|
||||
font: parent.font
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
// Frame counter badge on the left
|
||||
Rectangle {
|
||||
id: frameCounter
|
||||
implicitWidth: frameCounterText.implicitWidth + 16
|
||||
implicitHeight: 32
|
||||
color: Theme.fieldBackground
|
||||
border.color: Theme.cardBorder
|
||||
border.width: Theme.borderThin
|
||||
radius: Theme.radiusSm
|
||||
|
||||
Text {
|
||||
id: frameCounterText
|
||||
anchors.centerIn: parent
|
||||
text: "Frame " + (streamController.frameIndex + 1) + " / " + streamController.frameTotal
|
||||
color: Theme.headingColor
|
||||
font.pixelSize: 11
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
}
|
||||
|
||||
// Left fill — centers the button cluster
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
TBtn { text: "⏮"; onClicked: streamController.step(-1) }
|
||||
TBtn { text: "⏹"; onClicked: streamController.stop() }
|
||||
TBtn { text: "⏸"; onClicked: streamController.pause() }
|
||||
// Play: slightly wider; leftPadding nudge compensates for ▶ optical offset
|
||||
TBtn {
|
||||
text: "▶"
|
||||
implicitWidth: 52
|
||||
contentItem: Text {
|
||||
text: parent.text
|
||||
color: Theme.headingColor
|
||||
font: parent.font
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
leftPadding: 2
|
||||
}
|
||||
onClicked: streamController.play()
|
||||
}
|
||||
TBtn { text: "⏭"; onClicked: streamController.step(1) }
|
||||
|
||||
// Right fill — balances centering
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
// Speed cycle
|
||||
Button {
|
||||
id: speedBtn
|
||||
property var speeds: [1, 2, 5]
|
||||
property int idx: 0
|
||||
implicitWidth: 44
|
||||
implicitHeight: 32
|
||||
hoverEnabled: true
|
||||
text: speeds[idx] + "×"
|
||||
font.pixelSize: 11
|
||||
font.weight: Font.Medium
|
||||
background: Rectangle {
|
||||
color: parent.pressed ? Theme.transportButtonHover
|
||||
: parent.hovered ? Theme.transportButtonBg
|
||||
: Theme.transportBackground
|
||||
radius: Theme.radiusSm
|
||||
}
|
||||
contentItem: Text {
|
||||
text: parent.text
|
||||
color: Theme.bodyColor
|
||||
font: parent.font
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
onClicked: {
|
||||
idx = (idx + 1) % speeds.length
|
||||
streamController.setSpeed(speeds[idx])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import ISC
|
||||
import ISC.Wafer
|
||||
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property real blend: 0.0
|
||||
property bool showLabels: true
|
||||
|
||||
WaferMapItem {
|
||||
id: map
|
||||
anchors.fill: parent
|
||||
sensors: streamController.sensorLayout // [{label,x,y}]
|
||||
values: streamController.sensorValues // [float]
|
||||
bands: streamController.sensorBands // ["in_range"|"high"|"low"]
|
||||
shape: streamController.waferShape
|
||||
size: streamController.waferSize
|
||||
target: streamController.target
|
||||
margin: streamController.margin
|
||||
blend: root.blend
|
||||
showLabels: root.showLabels
|
||||
// Bind to Theme so colors update on dark/light mode switch
|
||||
ringColor: Theme.waferRingColor
|
||||
axisColor: Theme.waferAxisColor
|
||||
lowColor: Theme.sensorLow
|
||||
inRangeColor: Theme.sensorInRange
|
||||
highColor: Theme.sensorHigh
|
||||
textColor: Theme.headingColor
|
||||
|
||||
|
||||
TapHandler {
|
||||
onTapped: (ev) => {
|
||||
var idx = map.which_marker(ev.position.x, ev.position.y)
|
||||
if (idx >= 0) replaceDialog.openFor(idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ReplaceSensorDialog { id: replaceDialog}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# ===== ISC Tabs Components Module =====
|
||||
module ISC.Tabs.components
|
||||
SourcePanel 1.0 SourcePanel.qml
|
||||
ReadoutPanel 1.0 ReadoutPanel.qml
|
||||
TransportBar 1.0 TransportBar.qml
|
||||
WaferMapView 1.0 WaferMapView.qml
|
||||
ReplaceSensorDialog 1.0 ReplaceSensorDialog.qml
|
||||
@@ -0,0 +1,8 @@
|
||||
# ===== ISC Tabs Module =====
|
||||
module ISC.Tabs
|
||||
|
||||
WaferMapTab 1.0 WaferMapTab.qml
|
||||
DataTab 1.0 DataTab.qml
|
||||
SettingsTab 1.0 SettingsTab.qml
|
||||
StatusTab 1.0 StatusTab.qml
|
||||
SelectFileDialog 1.0 SelectFileDialog.qml
|
||||
@@ -21,15 +21,18 @@ import QtQuick
|
||||
|
||||
QtObject {
|
||||
// ── 1. Mode ──────────────────────────────────────────────────────────────
|
||||
property bool isDarkMode: false
|
||||
property bool isDarkMode: true
|
||||
|
||||
// ── 2. Tone palette (base values; consume via semantic tokens below) ─────
|
||||
readonly property color tone100: isDarkMode ? "#111111" : "#FAFAFA"
|
||||
readonly property color tone200: isDarkMode ? "#1A1A1A" : "#F2F2F2"
|
||||
readonly property color tone300: isDarkMode ? "#242424" : "#E8E8E8"
|
||||
readonly property color tone150: isDarkMode ? "#161616" : "#F6F6F6"
|
||||
readonly property color tone200: isDarkMode ? "#1A1A1A" : "#F0F0F0"
|
||||
readonly property color tone250: isDarkMode ? "#212121" : "#EAEAEA"
|
||||
readonly property color tone300: isDarkMode ? "#282828" : "#E2E2E2"
|
||||
readonly property color tone350: isDarkMode ? "#303030" : "#D6D6D6"
|
||||
readonly property color toneText: isDarkMode ? "#F2F2F2" : "#111111"
|
||||
readonly property color toneMute: isDarkMode ? "#A8A8A8" : "#8A8A8A"
|
||||
readonly property color toneBorder: isDarkMode ? "#2B2B2B" : "#D8D8D8"
|
||||
readonly property color toneBorder: isDarkMode ? "#2E2E2E" : "#D4D4D4"
|
||||
|
||||
// ── 3. Surfaces ──────────────────────────────────────────────────────────
|
||||
readonly property color pageBackground: tone100
|
||||
@@ -41,11 +44,12 @@ QtObject {
|
||||
|
||||
// ── 4. Borders ───────────────────────────────────────────────────────────
|
||||
readonly property color cardBorder: toneBorder
|
||||
readonly property color cardSurfaceBorder: isDarkMode ? "#3C3C3C" : "#C8C8C8"
|
||||
readonly property color responseBorder: toneBorder
|
||||
readonly property color workspaceBorder: toneBorder
|
||||
readonly property color innerFrameBorder: toneBorder
|
||||
readonly property color outerFrameBorder: isDarkMode ? "#343434" : "#CECECE"
|
||||
readonly property color softBorder: isDarkMode ? "#222222" : "#E2E2E2"
|
||||
readonly property color outerFrameBorder: isDarkMode ? "#383838" : "#CACACA"
|
||||
readonly property color softBorder: isDarkMode ? "#252525" : "#E0E0E0"
|
||||
|
||||
// ── 5. Text ──────────────────────────────────────────────────────────────
|
||||
readonly property color headingColor: toneText
|
||||
@@ -77,7 +81,7 @@ QtObject {
|
||||
// ── 8. Tabs (footer tab bar) ─────────────────────────────────────────────
|
||||
readonly property color tabBarBackground: tone200
|
||||
readonly property color tabBackground: "transparent"
|
||||
readonly property color tabActiveBackground: "transparent"
|
||||
readonly property color tabActiveBackground: isDarkMode ? "#333333" : "#FFFFFF"
|
||||
readonly property color tabHoverBackground: tone300
|
||||
readonly property color tabBorder: "transparent"
|
||||
readonly property color tabText: toneMute
|
||||
@@ -87,17 +91,31 @@ QtObject {
|
||||
readonly property color sideRailBackground: tone200
|
||||
readonly property color sideActiveBackground: tone300
|
||||
|
||||
// ── 10a. Transport / toolbar surfaces ────────────────────────────────────
|
||||
readonly property color transportBackground: isDarkMode ? "#0D0D0D" : "#E8E8E8"
|
||||
readonly property color transportButtonBg: isDarkMode ? "#2A2A2A" : "#D4D4D4"
|
||||
readonly property color transportButtonHover: isDarkMode ? "#3A3A3A" : "#C4C4C4"
|
||||
readonly property color liveColor: isDarkMode ? "#22C55E" : "#16A34A"
|
||||
readonly property color recordColor: isDarkMode ? "#EF4444" : "#DC2626"
|
||||
|
||||
// ── 10. Status ───────────────────────────────────────────────────────────
|
||||
readonly property color statusSuccessColor: isDarkMode ? "#63D471" : "#2E9E44"
|
||||
readonly property color statusWarningColor: isDarkMode ? "#F5C15C" : "#C88A18"
|
||||
readonly property color statusErrorColor: isDarkMode ? "#FF6B6B" : "#D64545"
|
||||
|
||||
// -- 10b. Sensor bands (wafer map dots)
|
||||
readonly property color sensorInRange: statusSuccessColor
|
||||
readonly property color sensorHigh: statusErrorColor
|
||||
readonly property color sensorLow: isDarkMode ? "#5B9DF5" : "#2F6FE0"
|
||||
readonly property color waferRingColor: toneBorder
|
||||
readonly property color waferAxisColor: softBorder
|
||||
|
||||
// ── 11. Geometry ─────────────────────────────────────────────────────────
|
||||
// Radius
|
||||
readonly property int radiusXs: 4 // fields, tight elements
|
||||
readonly property int radiusSm: 6 // buttons
|
||||
readonly property int radiusMd: 10 // cards, group boxes
|
||||
readonly property int radiusLg: 14 // large panels / dialogs
|
||||
readonly property int radiusXs: 6 // fields, tight elements
|
||||
readonly property int radiusSm: 8 // buttons
|
||||
readonly property int radiusMd: 12 // cards, group boxes
|
||||
readonly property int radiusLg: 18 // large panels / dialogs
|
||||
// Border width
|
||||
readonly property int borderThin: 1
|
||||
readonly property int borderStrong: 2
|
||||
@@ -114,7 +132,7 @@ QtObject {
|
||||
readonly property int tabBarHeight: 34
|
||||
readonly property int tabBarPadding: 6
|
||||
readonly property int tabSpacing: 2
|
||||
readonly property int tabRadius: 2
|
||||
readonly property int tabRadius: 8
|
||||
readonly property int tabFontSize: 12
|
||||
readonly property int tabButtonMinWidth: 80
|
||||
// Settings page layout
|
||||
@@ -1,17 +1,25 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import logging
|
||||
|
||||
from PySide6.QtQml import QQmlApplicationEngine
|
||||
from PySide6.QtQuickControls2 import QQuickStyle
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from backend.device_controller import DeviceController
|
||||
from backend.local_settings import LocalSettings
|
||||
from backend.local_settings_model import LocalSettingsModel
|
||||
from backend.file_browser import FileBrowser
|
||||
from pygui.backend.controllers.device_controller import DeviceController
|
||||
from pygui.backend.data.local_settings import LocalSettings
|
||||
from pygui.backend.data.local_settings_model import LocalSettingsModel
|
||||
from pygui.backend.data.file_browser import FileBrowser
|
||||
from pygui.backend.controllers.session_controller import SessionController
|
||||
# Importing wafer_map_item registers the @QmlElement WaferMapItem (QML: import ISC.Wafer).
|
||||
from pygui.backend.visualization.wafer_map_item import WaferMapItem
|
||||
|
||||
# ===== Application Entry Point =====
|
||||
if __name__ == "__main__":
|
||||
def main() -> int:
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
|
||||
)
|
||||
# ===== UI Style Setup =====
|
||||
# Use a non-native controls style so our custom QML button backgrounds are supported.
|
||||
QQuickStyle.setStyle("Basic")
|
||||
@@ -34,12 +42,22 @@ if __name__ == "__main__":
|
||||
device_controller = DeviceController(raw_settings, data_dir)
|
||||
engine.rootContext().setContextProperty("deviceController", device_controller)
|
||||
|
||||
# ===== Session Controller (live/review wafer dashboard) =====
|
||||
stream_controller = SessionController()
|
||||
engine.rootContext().setContextProperty("streamController", stream_controller)
|
||||
|
||||
# ===== QML Startup =====
|
||||
# The "ISC" QML module lives alongside this file (src/pygui/ISC), so the
|
||||
# package directory is the import path the engine searches for qmldir.
|
||||
engine.addImportPath(Path(__file__).parent)
|
||||
engine.loadFromModule("ISC", "Main")
|
||||
|
||||
# ===== Exit Handling =====
|
||||
if not engine.rootObjects():
|
||||
sys.exit(-1)
|
||||
return -1
|
||||
|
||||
sys.exit(app.exec())
|
||||
return app.exec()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,44 @@
|
||||
# This will be the name of the output files
|
||||
name: aepwafer
|
||||
wafers: ["A", "E", "P"]
|
||||
|
||||
# round or square wafer
|
||||
shape: round
|
||||
|
||||
# diameter/edge size of the wafer
|
||||
size: 300
|
||||
|
||||
# large or small markers and labels
|
||||
marker_size: large
|
||||
font_size: large
|
||||
|
||||
# origin settings. x_origin can be left, right, or center. y_origin can
|
||||
# be top, bottom, or center. Using right or top implies that the X/Y values
|
||||
# for that axis are negative, or else we'll be drawing outside the image.
|
||||
x_origin: left
|
||||
y_origin: bottom
|
||||
|
||||
# sensor X and Y values, in mm relative to the origin
|
||||
X: [150, 204, 249, 280, 290, 280, 249, 204, 150, 97, 51, 21, 10, 21, 51, 97,
|
||||
150, 203, 241, 255, 241, 203, 150, 98, 59, 45, 59, 98, 171, 207, 228, 228,
|
||||
207, 171, 130, 94, 73, 73, 94, 130, 150, 186, 200, 186, 150, 115, 100, 115]
|
||||
Y: [290, 280, 249, 204, 150, 97, 51, 21, 10, 21, 51, 97, 150, 204, 249, 280,
|
||||
255, 241, 203, 150, 98, 59, 45, 59, 98, 150, 203, 241, 228, 207, 171, 130,
|
||||
94, 73, 73, 94, 130, 171, 207, 228, 200, 186, 150, 115, 100, 115, 150, 186]
|
||||
|
||||
# set this to either 0 or 1 for the initial sensor number (usually 1)
|
||||
start_sn: 1
|
||||
|
||||
# Normally, positive X is to the right, and positive Y is up.
|
||||
# These settings can be used to reverse one or both of these.
|
||||
reverse_x: false
|
||||
reverse_y: false
|
||||
|
||||
# these are sensors whose labels need to be repositioned. Sensor numbers
|
||||
# start at start_sn. Directions are up, down, left, and right, and values
|
||||
# are multiples of the marker size.
|
||||
label_exceptions: {
|
||||
4: { top: [0, 0] },
|
||||
5: { top: [0, 0] },
|
||||
6: { top: [0, 0] },
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
# This will be the name of the output files
|
||||
name: bcdwafer
|
||||
wafers: ["B", "C", "D"]
|
||||
|
||||
# round or square wafer
|
||||
shape: round
|
||||
|
||||
# diameter/edge size of the wafer
|
||||
size: 300
|
||||
|
||||
# large or small markers and labels
|
||||
marker_size: large
|
||||
font_size: large
|
||||
|
||||
# origin settings. x_origin can be left, right, or center. y_origin can
|
||||
# be top, bottom, or center. Using right or top implies that the X/Y values
|
||||
# for that axis are negative, or else we'll be drawing outside the image.
|
||||
x_origin: center
|
||||
y_origin: center
|
||||
|
||||
# sensor X and Y values, in mm relative to the origin
|
||||
X: [0, 72.50, 125.57, 145.00, 125.57, 72.50, 0, -72.50, -125.57, -145.00,
|
||||
-125.57, -72.50, 0, 47.50, 82.27, 95.00, 82.27, 47.50, 0, -47.50, -82.27,
|
||||
-95.00, -82.27, -47.50, 38.97, 22.50, -38.97, -22.50, 0]
|
||||
Y: [145.00, 125.57, 72.50, 0, -72.50, -125.57, -145.00, -125.57, -72.50, 0,
|
||||
72.50, 125.57, 95.00, 82.27, 47.50, 0, -47.50, -82.27, -95.00, -82.27,
|
||||
-47.50, 0, 47.50, 82.27, 22.50, -38.97, -22.50, 38.97, 0]
|
||||
|
||||
# set this to either 0 or 1 for the initial sensor number (usually 1)
|
||||
start_sn: 1
|
||||
|
||||
# Normally, positive X is to the right, and positive Y is up.
|
||||
# These settings can be used to reverse one or both of these.
|
||||
reverse_x: false
|
||||
reverse_y: false
|
||||
|
||||
# these are sensors whose labels need to be repositioned. Sensor numbers
|
||||
# start at start_sn. Directions are up, down, left, and right, and values
|
||||
# are multiples of the marker size.
|
||||
label_exceptions: {
|
||||
2: { left: [1, 0] },
|
||||
3: { left: [1, 0] },
|
||||
4: { left: [1, 0] },
|
||||
5: { left: [1, 0] },
|
||||
6: { left: [1, 0] },
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
# This will be the name of the output files
|
||||
name: fwafer
|
||||
wafers: ["F"]
|
||||
|
||||
# round or square wafer
|
||||
shape: round
|
||||
|
||||
# diameter/edge size of the wafer
|
||||
size: 200
|
||||
|
||||
# large or small markers and labels
|
||||
marker_size: large
|
||||
font_size: large
|
||||
|
||||
# origin settings. x_origin can be left, right, or center. y_origin can
|
||||
# be top, bottom, or center. Using right or top implies that the X/Y values
|
||||
# for that axis are negative, or else we'll be drawing outside the image.
|
||||
x_origin: center
|
||||
y_origin: center
|
||||
|
||||
# sensor X and Y values, in mm relative to the origin
|
||||
X: [0.0, 47.5, 82.3, 95.0, 82.3, 47.5, 0.0, -47.5, -82.3, -95.0, -82.3, -47.5,
|
||||
-55.0, -14.8, 39.7, 55.0, 14.8, -39.7, -14.7, 25.5, 14.7, -25.5]
|
||||
Y: [95.0, 82.3, 47.5, 0.0, -47.5, -82.3, -95.0, -82.3, -47.5, 0.0, 47.5, 82.3,
|
||||
13.3, 54.4, 40.0, -13.3, -54.4, -40.0, 25.5, 14.7, -25.5, -14.7]
|
||||
|
||||
# set this to either 0 or 1 for the initial sensor number (usually 1)
|
||||
start_sn: 1
|
||||
|
||||
# Normally, positive X is to the right, and positive Y is up.
|
||||
# These settings can be used to reverse one or both of these.
|
||||
reverse_x: false
|
||||
reverse_y: false
|
||||
|
||||
# these are sensors whose labels need to be repositioned. Sensor numbers
|
||||
# start at start_sn. Directions are up, down, left, and right, and values
|
||||
# are multiples of the marker size.
|
||||
label_exceptions: {
|
||||
2: { left: [1, 0] },
|
||||
3: { left: [1, 0] },
|
||||
4: { left: [1, 0] },
|
||||
5: { left: [1, 0] },
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
# This will be the name of the output files
|
||||
name: xwafer
|
||||
wafers: ["X"]
|
||||
|
||||
# round or square wafer
|
||||
shape: square
|
||||
|
||||
# diameter/edge size of the wafer
|
||||
size: 310
|
||||
|
||||
# large or small markers and labels
|
||||
marker_size: small
|
||||
font_size: small
|
||||
|
||||
# origin settings. x_origin can be left, right, or center. y_origin can
|
||||
# be top, bottom, or center. Using right or top implies that the X/Y values
|
||||
# for that axis are negative, or else we'll be drawing outside the image.
|
||||
x_origin: left
|
||||
y_origin: bottom
|
||||
|
||||
# sensor X and Y values, in mm relative to the origin
|
||||
X: [ 3.00, 3.00, 3.00, 3.00, 3.00, 3.00, 3.00, 3.00, 3.00, 3.00, 3.00, 3.00,
|
||||
13.00, 23.00, 46.43, 89.86, 133.29, 176.72, 220.15, 263.58, 287.01, 297.01,
|
||||
307.01, 307.01, 307.01, 307.01, 307.01, 307.01, 307.01, 307.01, 307.01,
|
||||
307.01, 307.01, 307.01, 297.01, 287.01, 263.58, 220.15, 176.72, 133.29,
|
||||
89.86, 46.43, 23.00, 13.00, 46.43, 46.43, 46.43, 46.43, 46.43, 46.43,
|
||||
89.86, 133.29, 176.72, 220.15, 263.58, 263.58, 263.58, 263.58, 263.58,
|
||||
263.58, 220.15, 176.72, 133.29, 89.86, 89.86, 89.86, 89.86, 89.86,
|
||||
133.29, 176.72, 220.15, 220.15, 220.15, 220.15, 176.72, 133.29, 133.29,
|
||||
133.29, 176.72, 176.72]
|
||||
Y: [ 3.00, 13.07, 23.07, 46.50, 89.93, 133.36, 176.79, 220.22, 263.65, 287.08,
|
||||
297.08, 307.08, 307.08, 307.08, 307.08, 307.08, 307.08, 307.08, 307.08,
|
||||
307.08, 307.08, 307.08, 307.08, 297.08, 287.08, 263.65, 220.22, 176.79,
|
||||
133.36, 89.93, 46.50, 23.07, 13.07, 3.00, 3.00, 3.00, 3.00, 3.00, 3.00,
|
||||
3.00, 3.00, 3.00, 3.00, 3.00, 46.50, 89.93, 133.36, 176.79, 220.22, 263.65,
|
||||
263.65, 263.65, 263.65, 263.65, 263.65, 220.22, 176.79, 133.36, 89.93,
|
||||
46.50, 46.50, 46.50, 46.50, 46.50, 89.93, 133.36, 176.79, 220.22, 220.22,
|
||||
220.22, 220.22, 176.79, 133.36, 89.93, 89.93, 89.93, 133.36, 176.79,
|
||||
176.79, 133.36 ]
|
||||
|
||||
# set this to either 0 or 1 for the initial sensor number (usually 1)
|
||||
start_sn: 1
|
||||
|
||||
# Normally, positive X is to the right, and positive Y is up.
|
||||
# These settings can be used to reverse one or both of these.
|
||||
reverse_x: false
|
||||
reverse_y: false
|
||||
|
||||
# these are sensors whose labels need to be repositioned. Sensor numbers
|
||||
# start at start_sn. Directions are up, down, left, and right, and values
|
||||
# are multiples of the marker size.
|
||||
label_exceptions: {
|
||||
21: { left: [0, 0] },
|
||||
22: { left: [0, 0] },
|
||||
23: { left: [0, 0] },
|
||||
24: { left: [0, 0] },
|
||||
25: { left: [0, 0] },
|
||||
26: { left: [0, 0] },
|
||||
27: { left: [0, 0] },
|
||||
28: { left: [0, 0] },
|
||||
29: { left: [0, 0] },
|
||||
30: { left: [0, 0] },
|
||||
31: { left: [0, 0] },
|
||||
32: { left: [0, 0] },
|
||||
33: { left: [0, 0] },
|
||||
34: { left: [0, 0] },
|
||||
35: { left: [0, 0] },
|
||||
36: { left: [0, 0] }
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
# This will be the name of the output files
|
||||
name: zwafer
|
||||
wafers: ["Z"]
|
||||
|
||||
# round or square wafer
|
||||
shape: round
|
||||
|
||||
# diameter/size of the wafer
|
||||
size: 300
|
||||
|
||||
# large or small markers and labels
|
||||
marker_size: small
|
||||
font_size: small
|
||||
|
||||
# origin settings. x_origin can be left, right, or center. y_origin can
|
||||
# be top, bottom, or center. Using right or top probably also needs
|
||||
# reverse_x/reverse_y to be set as well.
|
||||
x_origin: center
|
||||
y_origin: center
|
||||
|
||||
# sensor X and Y values, in mm relative to the wafer center
|
||||
X: [0.00, -1.17, 8.92, 1.17, -8.92, 22.52, 29.35, -22.52, -29.35, 45.05, 73.37,
|
||||
58.71, 9.66, -45.05, -73.37, -58.71, -9.66, 79.85, 89.06, 46.10, -23.86,
|
||||
-79.85, -89.06, -46.10, 23.86, 66.96, 109.06, 87.27, 14.36, -66.96,
|
||||
-109.06, -87.27, -14.36, 121.24, 135.23, 70.00, -36.23, -121.24, -135.23,
|
||||
-70.00, 36.23, 124.71, 139.09, 72.00, -37.27, -124.71, -139.09, -72.00,
|
||||
37.27, 89.49, 127.31, 145.74, 141.99, 116.62, 73.50, 19.19, -38.05, -89.49,
|
||||
-127.31, -145.74, -141.99, -116.62, -73.50, -19.19, 38.05]
|
||||
Y: [0.00, -8.92, -1.17, 8.92, 1.17, -29.35, 22.52, 29.35, -22.52, -58.71,
|
||||
-9.66, 45.05, 73.37, 58.71, 9.66, -45.05, -73.37, -46.10, 23.86, 79.85,
|
||||
89.06, 46.10, -23.86, -79.85, -89.06, -87.27, -14.36, 66.96, 109.06, 87.27,
|
||||
14.36, -66.96, -109.06, -70.00, 36.23, 121.24, 135.23, 70.00, -36.23,
|
||||
-121.24, -135.23, -72.00, 37.27, 124.71, 139.09, 72.00, -37.27, -124.71,
|
||||
-139.09, -116.62, -73.50, -19.19, 38.05, 89.49, 127.31, 145.74, 141.99,
|
||||
116.62, 73.50, 19.19, -38.05, -89.49, -127.31, -145.74, -141.99]
|
||||
|
||||
# set this to either 0 or 1 for the initial sensor number (usually 1)
|
||||
start_sn: 0
|
||||
|
||||
# If one or both axes are reversed, this can be used to fix it
|
||||
# without having to edit all of the coordinates.
|
||||
# These should be false for most normal wafers.
|
||||
reverse_x: false
|
||||
reverse_y: false
|
||||
|
||||
# these are sensors whose labels need to be repositioned. Sensor numbers
|
||||
# start at start_sn. Directions are up, down, left, and right, and values
|
||||
# are multiples of the marker size.
|
||||
label_exceptions: {
|
||||
33: { left: [0, 0] },
|
||||
34: { left: [0, 0] },
|
||||
41: { top: [0, 0] },
|
||||
42: { bottom: [0, 0] },
|
||||
45: { top: [1, 0] },
|
||||
46: { bottom: [0.5, 0] },
|
||||
49: { left: [0, 0] },
|
||||
50: { bottom: [-1.75, -0.5] },
|
||||
51: { top: [-1, 0] },
|
||||
52: { top: [-1, 0] },
|
||||
53: { bottom: [0, 0] },
|
||||
54: { left: [0, -0.75] },
|
||||
55: { right: [0, 0.5] },
|
||||
58: { bottom: [-0.75, 0] },
|
||||
60: { top: [0, 0] },
|
||||
62: { right: [0, 1] },
|
||||
63: { right: [0, 0] },
|
||||
64: { left: [0, 0.25] }
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
# This will be the name of the output files
|
||||
name: zwafer_rev
|
||||
wafers: [] # the reversed version doesn't represent any real wafer
|
||||
|
||||
# round or square wafer
|
||||
shape: round
|
||||
|
||||
# diameter/size of the wafer
|
||||
size: 300
|
||||
|
||||
# large or small markers and labels
|
||||
marker_size: small
|
||||
font_size: small
|
||||
|
||||
# origin settings. x_origin can be left, right, or center. y_origin can
|
||||
# be top, bottom, or center. Using right or top probably also needs
|
||||
# reverse_x/reverse_y to be set as well.
|
||||
x_origin: center
|
||||
y_origin: center
|
||||
|
||||
# sensor X and Y values, in mm relative to the wafer center
|
||||
X: [0.00, -1.17, 8.92, 1.17, -8.92, 22.52, 29.35, -22.52, -29.35, 45.05, 73.37,
|
||||
58.71, 9.66, -45.05, -73.37, -58.71, -9.66, 79.85, 89.06, 46.10, -23.86,
|
||||
-79.85, -89.06, -46.10, 23.86, 66.96, 109.06, 87.27, 14.36, -66.96,
|
||||
-109.06, -87.27, -14.36, 121.24, 135.23, 70.00, -36.23, -121.24, -135.23,
|
||||
-70.00, 36.23, 124.71, 139.09, 72.00, -37.27, -124.71, -139.09, -72.00,
|
||||
37.27, 89.49, 127.31, 145.74, 141.99, 116.62, 73.50, 19.19, -38.05, -89.49,
|
||||
-127.31, -145.74, -141.99, -116.62, -73.50, -19.19, 38.05]
|
||||
Y: [0.00, -8.92, -1.17, 8.92, 1.17, -29.35, 22.52, 29.35, -22.52, -58.71,
|
||||
-9.66, 45.05, 73.37, 58.71, 9.66, -45.05, -73.37, -46.10, 23.86, 79.85,
|
||||
89.06, 46.10, -23.86, -79.85, -89.06, -87.27, -14.36, 66.96, 109.06, 87.27,
|
||||
14.36, -66.96, -109.06, -70.00, 36.23, 121.24, 135.23, 70.00, -36.23,
|
||||
-121.24, -135.23, -72.00, 37.27, 124.71, 139.09, 72.00, -37.27, -124.71,
|
||||
-139.09, -116.62, -73.50, -19.19, 38.05, 89.49, 127.31, 145.74, 141.99,
|
||||
116.62, 73.50, 19.19, -38.05, -89.49, -127.31, -145.74, -141.99]
|
||||
|
||||
# set this to either 0 or 1 for the initial sensor number (usually 1)
|
||||
start_sn: 0
|
||||
|
||||
# If one or both axes are reversed, this can be used to fix it
|
||||
# without having to edit all of the coordinates.
|
||||
# These should be false for most normal wafers.
|
||||
reverse_x: true
|
||||
reverse_y: true
|
||||
|
||||
# these are sensors whose labels need to be repositioned. Sensor numbers
|
||||
# start at start_sn. Directions are up, down, left, and right, and values
|
||||
# are multiples of the marker size.
|
||||
label_exceptions: {
|
||||
37: { left: [0, 0] },
|
||||
38: { left: [0, 0] },
|
||||
39: { left: [0, 0] },
|
||||
41: { top: [1.5, 0] },
|
||||
42: { bottom: [1, 0] },
|
||||
43: { left: [0, -1] },
|
||||
45: { bottom: [-1, -1] },
|
||||
46: { bottom: [0, -1] },
|
||||
47: { right: [0, 1] },
|
||||
48: { left: [0, 0] },
|
||||
50: { bottom: [-1, 0] },
|
||||
52: { top: [0, 0] },
|
||||
54: { right: [0.5, 1] },
|
||||
56: { left: [0, 0.5] },
|
||||
57: { left: [0, 0] },
|
||||
58: { top: [0.5, 0] },
|
||||
59: { left: [0, 0] },
|
||||
60: { top: [-1, 0] },
|
||||
61: { left: [0, 0] },
|
||||
62: { left: [0, -1] },
|
||||
63: { right: [0, 1] },
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
# ===== Backend Package =====
|
||||
# Backward-compatible re-exports so existing callers keep working
|
||||
# while imports are migrated to the new sub-package paths.
|
||||
|
||||
from pygui.backend.controllers.device_controller import DeviceController # noqa: F401
|
||||
from pygui.backend.controllers.session_controller import SessionController # noqa: F401
|
||||
|
||||
from pygui.backend.models.frame import Frame # noqa: F401
|
||||
from pygui.backend.models.frame_player import FramePlayer, frames_from_wafer_data # noqa: F401
|
||||
from pygui.backend.models.frame_stats import Stats, compute_stats # noqa: F401
|
||||
from pygui.backend.models.session_model import SessionModel # noqa: F401
|
||||
from pygui.backend.models.data_model import TemperatureTableModel # noqa: F401
|
||||
from pygui.backend.models.sensor_editor import SensorEditor # noqa: F401
|
||||
from pygui.backend.models.threshold_classifier import ThresholdConfig # noqa: F401
|
||||
from pygui.backend.models.stability_detector import StabilityDetector # noqa: F401
|
||||
|
||||
from pygui.backend.data.csv_recorder import CsvRecorder # noqa: F401
|
||||
from pygui.backend.data.csv_file_metadata import CSVFileMetadata # noqa: F401
|
||||
from pygui.backend.data.data_records import read_data_records # noqa: F401
|
||||
from pygui.backend.data.data_segment import DataSegment # noqa: F401
|
||||
from pygui.backend.data.file_browser import FileBrowser # noqa: F401
|
||||
from pygui.backend.data.local_settings import LocalSettings # noqa: F401
|
||||
from pygui.backend.data.local_settings_model import LocalSettingsModel # noqa: F401
|
||||
|
||||
from pygui.backend.visualization.wafer_map_item import WaferMapItem # noqa: F401
|
||||
from pygui.backend.visualization.graph_view import GraphView # noqa: F401
|
||||
from pygui.backend.visualization.rbf_heatmap import interpolate_field # noqa: F401
|
||||
from pygui.backend.visualization.contour_models import ContourLine, ContourSegment # noqa: F401
|
||||
|
||||
from pygui.backend.wafer.zwafer_models import ZWaferData, Sensor, DataRecord # noqa: F401
|
||||
from pygui.backend.wafer.zwafer_parser import ZWaferParser # noqa: F401
|
||||
from pygui.backend.wafer.wafer_layouts import load_layout, available_families # noqa: F401
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Cluster Average utility for wafer sensor values."""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
import math
|
||||
from pygui.backend.wafer.zwafer_models import Sensor
|
||||
|
||||
|
||||
def average_clusters(values: list[float], clusters: list[list[int]]) -> list[float]:
|
||||
"""Return a new list of values where members of each cluster are replaced by their mean.
|
||||
|
||||
NaN values are filtered out before computing the mean.
|
||||
If all values in a cluster are NaN, they remain NaN.
|
||||
"""
|
||||
result = list(values)
|
||||
for cluster in clusters:
|
||||
valid_vals = []
|
||||
for idx in cluster:
|
||||
if idx < len(values):
|
||||
val = values[idx]
|
||||
if not math.isnan(val):
|
||||
valid_vals.append(val)
|
||||
|
||||
if valid_vals:
|
||||
mean_val = sum(valid_vals) / len(valid_vals)
|
||||
for idx in cluster:
|
||||
if idx < len(result):
|
||||
result[idx] = mean_val
|
||||
return result
|
||||
|
||||
|
||||
def group_sensors_by_radius(sensors: list[Sensor], tolerance: float = 2.0) -> list[list[int]]:
|
||||
"""Group sensor indices into clusters if their radial distances are within tolerance."""
|
||||
# Compute radii for all sensors
|
||||
radii = [(i, math.hypot(s.x, s.y)) for i, s in enumerate(sensors)]
|
||||
# Filter out center sensors (r < 1.0)
|
||||
non_center = [item for item in radii if item[1] >= 1.0]
|
||||
if not non_center:
|
||||
return []
|
||||
|
||||
# Sort by radius
|
||||
non_center.sort(key=lambda x: x[1])
|
||||
|
||||
clusters: list[list[int]] = []
|
||||
current_cluster: list[int] = [non_center[0][0]]
|
||||
last_r = non_center[0][1]
|
||||
|
||||
for idx, r in non_center[1:]:
|
||||
if r - last_r <= tolerance:
|
||||
current_cluster.append(idx)
|
||||
else:
|
||||
if len(current_cluster) > 1:
|
||||
clusters.append(current_cluster)
|
||||
current_cluster = [idx]
|
||||
last_r = r
|
||||
|
||||
if len(current_cluster) > 1:
|
||||
clusters.append(current_cluster)
|
||||
|
||||
return clusters
|
||||
@@ -0,0 +1,6 @@
|
||||
# ===== Controllers Sub-package =====
|
||||
from pygui.backend.controllers.device_controller import DeviceController
|
||||
from pygui.backend.controllers.session_controller import SessionController
|
||||
|
||||
__all__ = ["DeviceController", "SessionController"]
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
"""QML-exposed controller for wafer device communication.
|
||||
|
||||
Bridges DeviceService (serialcomm) to QML via @Slot methods and signals.
|
||||
All hardware operations run synchronously on the main thread (matching
|
||||
C# Form1.cs per-operation open/close pattern). For long operations
|
||||
(erase ~15s, read up to 120s) the caller should show a loading state.
|
||||
"""
|
||||
"""QML-exposed controller for wafer device communication."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -15,17 +9,22 @@ from typing import Any, Optional
|
||||
|
||||
from PySide6.QtCore import QObject, Property, Qt, Signal, Slot
|
||||
|
||||
from backend.data_model import TemperatureTableModel
|
||||
from backend.graph_view import GraphView
|
||||
from backend.local_settings import LocalSettings
|
||||
from serialcomm.data_parser import (
|
||||
from pygui.backend.models.data_model import TemperatureTableModel
|
||||
from pygui.backend.visualization.graph_view import GraphView
|
||||
from pygui.backend.data.local_settings import LocalSettings
|
||||
from pygui.serialcomm.data_parser import (
|
||||
convert_to_temperatures,
|
||||
parse_binary_data,
|
||||
remove_trailing_zeros,
|
||||
save_to_csv,
|
||||
)
|
||||
from serialcomm.device_service import DeviceService
|
||||
from serialcomm.serial_port import WaferInfo
|
||||
from pygui.serialcomm.device_service import DeviceService
|
||||
from pygui.serialcomm.serial_port import WaferInfo
|
||||
from pygui.backend.controllers.session_controller import SessionController
|
||||
# import pygui.backend.wafer_map_item
|
||||
|
||||
stream_controller = SessionController()
|
||||
# engine.rootContext().setContextProperty("streamController", stream_controller)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -62,9 +61,8 @@ class DeviceController(QObject):
|
||||
self._operation_in_progress = False
|
||||
self._activity_log: list[str] = getattr(settings, 'activity_log', [])
|
||||
self._raw_bytes: Optional[bytes] = None
|
||||
# Default save dir lives next to the settings file (~/Documents/isc_data/csv)
|
||||
from pathlib import Path
|
||||
self._save_data_dir: str = getattr(settings, 'save_data_dir', str(Path.home() / "Documents" / "isc_data" / "csv"))
|
||||
self._save_data_dir: str = getattr(settings, 'save_data_dir', "") or str(Path(self._data_dir) / "csv")
|
||||
self._last_wafer_info: dict[str, Any] = getattr(settings, 'last_wafer_info', {})
|
||||
self._last_csv_path: str = getattr(settings, 'last_csv_path', "")
|
||||
self._selected_port: str = getattr(settings, 'selected_port', "")
|
||||
@@ -88,8 +86,8 @@ class DeviceController(QObject):
|
||||
self.statusRestored.emit()
|
||||
|
||||
# Marshal worker-thread results onto the Qt main thread.
|
||||
self._detectFinished.connect(self._handle_detect_finished, Qt.QueuedConnection)
|
||||
self._readFinished.connect(self._handle_read_finished, Qt.QueuedConnection)
|
||||
self._detectFinished.connect(self._handle_detect_finished, Qt.ConnectionType.QueuedConnection)
|
||||
self._readFinished.connect(self._handle_read_finished, Qt.ConnectionType.QueuedConnection)
|
||||
|
||||
# ---- Properties ----
|
||||
@Property(list, notify=portsUpdated)
|
||||
@@ -204,6 +202,28 @@ class DeviceController(QObject):
|
||||
self._activity_log = self._activity_log[-200:]
|
||||
self.activityLogUpdated.emit("\n".join(self._activity_log))
|
||||
|
||||
@Slot()
|
||||
def clearActivityLog(self) -> None:
|
||||
"""Clear the activity log."""
|
||||
self._activity_log.clear()
|
||||
self.activityLogUpdated.emit("")
|
||||
self._save_status()
|
||||
|
||||
@Slot()
|
||||
def clearSession(self) -> None:
|
||||
"""Clear the current session state, allowing a clean detect."""
|
||||
self._connection_status = "Disconnected"
|
||||
self._selected_port = ""
|
||||
self._last_wafer_info = {}
|
||||
self._data_row_count = 0
|
||||
self._data_col_count = 0
|
||||
self._raw_bytes = None
|
||||
self._operation_in_progress = False
|
||||
self.detectResult.emit(None)
|
||||
self.portsUpdated.emit(self.availablePorts)
|
||||
self._append_log("Session refreshed/cleared")
|
||||
self._save_status()
|
||||
|
||||
def _set_operation_progress(self, in_progress: bool) -> None:
|
||||
"""Set operation progress state and notify QML.
|
||||
|
||||
@@ -283,7 +303,8 @@ class DeviceController(QObject):
|
||||
"""Read wafer memory in a background thread.
|
||||
|
||||
Uses the family code and port from the last detect.
|
||||
Emits readResult on completion, then auto-chains to parseAndSaveData.
|
||||
Emits readResult on completion; QML prompts for the save directory
|
||||
before calling parseAndSaveData.
|
||||
"""
|
||||
if self._operation_in_progress:
|
||||
self._append_log("Already busy — ignoring read request")
|
||||
@@ -330,9 +351,7 @@ class DeviceController(QObject):
|
||||
self._raw_bytes = data
|
||||
self.readResult.emit({"success": True, "bytes": len(data)})
|
||||
self._set_operation_progress(False)
|
||||
# Auto-chain: read → parse → save (matches C# behavior).
|
||||
# Parse runs on main thread — it's CPU-bound but bounded (~1s).
|
||||
self.parseAndSaveData(family_code, port)
|
||||
self._append_log("Memory read complete - choose save directory to save CSV")
|
||||
self._save_status()
|
||||
return
|
||||
self._connection_status = "Disconnected"
|
||||
@@ -419,12 +438,10 @@ class DeviceController(QObject):
|
||||
return
|
||||
|
||||
if not self._save_data_dir:
|
||||
self._append_log("No save data directory set")
|
||||
self.parsedDataReady.emit({
|
||||
"success": False,
|
||||
"error": "No save data directory set",
|
||||
})
|
||||
return
|
||||
from pathlib import Path
|
||||
self._save_data_dir = str(Path(self._data_dir) / "csv")
|
||||
self._append_log(f"Auto-set save directory to: {self._save_data_dir}")
|
||||
self._save_status()
|
||||
|
||||
fc = family_code or (
|
||||
self._last_wafer_info.get("familyCode", "")
|
||||
@@ -471,6 +488,7 @@ class DeviceController(QObject):
|
||||
return
|
||||
|
||||
self._append_log(f"Saved CSV: {csv_path}")
|
||||
self._last_csv_path = csv_path
|
||||
|
||||
# Load data into the QAbstractTableModel
|
||||
self._data_model.load_data(temp_data, cols)
|
||||
@@ -504,7 +522,7 @@ class DeviceController(QObject):
|
||||
self._settings.last_csv_path = self._last_csv_path
|
||||
|
||||
# Save to disk
|
||||
LocalSettings.save_settings(LocalSettings._settings_path(self._data_dir), self._settings)
|
||||
LocalSettings.save_settings(self._data_dir, self._settings)
|
||||
|
||||
# ---- Helpers ----
|
||||
|
||||
@@ -0,0 +1,460 @@
|
||||
"""QML-facing controller for the live/review wafer dashboard."""
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, Optional
|
||||
|
||||
from PySide6.QtCore import QObject, Property, QTimer, Qt, Signal, Slot
|
||||
|
||||
from pygui.backend.data.csv_recorder import CsvRecorder
|
||||
from pygui.backend.models.frame import Frame
|
||||
from pygui.backend.models.frame_player import FramePlayer, frames_from_wafer_data
|
||||
from pygui.backend.models.session_model import SessionModel
|
||||
from pygui.backend.models.threshold_classifier import ThresholdConfig
|
||||
from pygui.backend.wafer.zwafer_models import Sensor
|
||||
from pygui.backend.wafer.zwafer_parser import ZWaferParser
|
||||
from pygui.serialcomm.stream_reader import StreamReader
|
||||
from pygui.backend.models.sensor_editor import SensorEditor
|
||||
from pygui.backend.cluster_average import average_clusters, group_sensors_by_radius
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
MODE_LIVE = "live"
|
||||
MODE_REVIEW = "review"
|
||||
|
||||
|
||||
class SessionController(QObject):
|
||||
# public signals
|
||||
frameUpdated = Signal()
|
||||
modeChanged = Signal()
|
||||
stateChanged = Signal()
|
||||
recordingChanged = Signal()
|
||||
sensorsChanged = Signal()
|
||||
loadedFileChanged = Signal()
|
||||
clusterAveragingEnabledChanged = Signal()
|
||||
# private: marshal a worker-thread frame onto the main thread
|
||||
_liveFrame = Signal(object) # Frame
|
||||
|
||||
def __init__(self, parent: QObject | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._mode = MODE_REVIEW
|
||||
self._model = SessionModel()
|
||||
self._player = FramePlayer()
|
||||
self._reader: Optional[StreamReader] = None
|
||||
self._recorder = CsvRecorder()
|
||||
self._sensors: list[Sensor] = []
|
||||
self._last = None # last SessionUpdate
|
||||
self._elapsed = 0.0
|
||||
|
||||
self._play_timer = QTimer(self)
|
||||
self._play_timer.timeout.connect(self._advance)
|
||||
self._speed = 1.0
|
||||
|
||||
# Q1: coalesce live repaints to ~20 Hz; data is still processed every frame.
|
||||
self._repaint_timer = QTimer(self)
|
||||
self._repaint_timer.setInterval(50) # ~20 Hz
|
||||
self._repaint_timer.timeout.connect(self._flush_repaint)
|
||||
self._dirty = False
|
||||
|
||||
self._liveFrame.connect(self._on_live_frame, Qt.ConnectionType.QueuedConnection)
|
||||
|
||||
self._sensor_editor = SensorEditor()
|
||||
self._last_raw_frame: Frame | None = None
|
||||
self._loaded_file: str = ""
|
||||
self._cluster_averaging_enabled = False
|
||||
self._active_clusters: list[list[int]] = []
|
||||
|
||||
# ---- properties QML binds to ----
|
||||
@Property(str, notify=modeChanged)
|
||||
def mode(self) -> str: return self._mode
|
||||
|
||||
@Property(int, notify=frameUpdated)
|
||||
def frameIndex(self) -> int: return self._player.index
|
||||
|
||||
@Property(int, notify=frameUpdated)
|
||||
def frameTotal(self) -> int: return self._player.total
|
||||
|
||||
@Property(str, notify=stateChanged)
|
||||
def state(self) -> str:
|
||||
if self._last:
|
||||
return self._last.state
|
||||
# IF we dont have data yet, but the reader is running -> Streaming
|
||||
if self._mode == MODE_LIVE and self._reader is not None:
|
||||
return "streaming"
|
||||
|
||||
return "idle"
|
||||
|
||||
@Property(bool, notify=recordingChanged)
|
||||
def recording(self) -> bool: return self._recorder.is_recording
|
||||
|
||||
@Property(list, notify=frameUpdated)
|
||||
def sensorDots(self) -> list[dict]:
|
||||
"""Per-sensor render data for the radial map."""
|
||||
if not self._last:
|
||||
return []
|
||||
out = []
|
||||
for i, s in enumerate(self._sensors):
|
||||
v = self._last.values[i] if i < len(self._last.values) else 0.0
|
||||
band = self._last.bands[i] if i < len(self._last.bands) else "in_range"
|
||||
out.append({"label": s.label, "x": s.x, "y": s.y,
|
||||
"value": round(v, 2), "band": band, "index": i})
|
||||
return out
|
||||
|
||||
@Property("QVariantList", notify=sensorsChanged)
|
||||
def sensorLayout(self) -> list:
|
||||
"""[{label, x, y, side, offset_x, offset_y}] for WaferMapItem.sensors."""
|
||||
return [
|
||||
{
|
||||
"label": s.label,
|
||||
"x": s.x,
|
||||
"y": s.y,
|
||||
"side": getattr(s, "side", "right"),
|
||||
"offset_x": getattr(s, "offset_x", 0.0),
|
||||
"offset_y": getattr(s, "offset_y", 0.0),
|
||||
}
|
||||
for s in self._sensors
|
||||
]
|
||||
|
||||
@Property(str, notify=sensorsChanged)
|
||||
def waferShape(self) -> str:
|
||||
"""Wafer shape: 'round' or 'square'."""
|
||||
return getattr(self._sensors, "shape", "round")
|
||||
|
||||
@Property(float, notify=sensorsChanged)
|
||||
def waferSize(self) -> float:
|
||||
"""Wafer size in mm."""
|
||||
return getattr(self._sensors, "size", 300.0)
|
||||
|
||||
@Property("QVariantList", notify=frameUpdated)
|
||||
def sensorValues(self) -> list:
|
||||
"""[float] in sensor order."""
|
||||
if not self._last:
|
||||
return []
|
||||
return [round(v, 3) for v in self._last.values]
|
||||
|
||||
@Property("QVariantList", notify=frameUpdated)
|
||||
def sensorBands(self) -> list:
|
||||
"""['in_range'|'high'|'low'] in sensor order."""
|
||||
if not self._last:
|
||||
return []
|
||||
return list(self._last.bands)
|
||||
|
||||
@Property(float, notify=frameUpdated)
|
||||
def target(self) -> float:
|
||||
"""Resolved band center (frame mean in auto mode, else set_point)."""
|
||||
return self._last.target if self._last else 149.0
|
||||
|
||||
@Property(float, notify=frameUpdated)
|
||||
def margin(self) -> float:
|
||||
"""Resolved band half-width (frame 1σ in auto mode, else margin)."""
|
||||
return self._last.margin if self._last else 1.0
|
||||
|
||||
@Property(dict, notify=frameUpdated)
|
||||
def stats(self) -> dict:
|
||||
if not self._last:
|
||||
return {}
|
||||
s = self._last.stats
|
||||
return {"min": round(s.min, 2), "minIndex": s.min_index + 1,
|
||||
"max": round(s.max, 2), "maxIndex": s.max_index + 1,
|
||||
"diff": round(s.diff, 2), "avg": round(s.avg, 2),
|
||||
"sigma": round(s.sigma, 2), "threeSigma": round(s.three_sigma, 2)}
|
||||
|
||||
@Property(str, notify=loadedFileChanged)
|
||||
def loadedFile(self) -> str: return self._loaded_file
|
||||
|
||||
@Property("QVariantList", notify=frameUpdated)
|
||||
def overriddenSensors(self) -> list[int]:
|
||||
"""Indices of sensors that currently have a replacement or offset."""
|
||||
return self._sensor_editor.active_indices()
|
||||
|
||||
@Property(bool, notify=clusterAveragingEnabledChanged)
|
||||
def clusterAveragingEnabled(self) -> bool:
|
||||
return self._cluster_averaging_enabled
|
||||
|
||||
@clusterAveragingEnabled.setter
|
||||
def clusterAveragingEnabled(self, value: bool) -> None:
|
||||
if self._cluster_averaging_enabled == value:
|
||||
return
|
||||
self._cluster_averaging_enabled = value
|
||||
self.clusterAveragingEnabledChanged.emit()
|
||||
self._reprocess_current()
|
||||
|
||||
# ---- mode + thresholds ----
|
||||
@Slot(str)
|
||||
def setMode(self, mode: str) -> None:
|
||||
if mode == self._mode:
|
||||
return
|
||||
self.stopStream()
|
||||
self._play_timer.stop()
|
||||
self._mode = mode
|
||||
self.modeChanged.emit()
|
||||
|
||||
@Slot(float, float, bool)
|
||||
def setThresholds(self, set_point: float, margin: float, auto: bool) -> None:
|
||||
self._model.set_thresholds(ThresholdConfig(set_point, margin, auto))
|
||||
if self._last: # re-band current frame in place
|
||||
self._reprocess_current()
|
||||
|
||||
# ---- review: file load + playback ----
|
||||
@Slot(str)
|
||||
def loadFile(self, file_path: str) -> None:
|
||||
from pathlib import Path
|
||||
from pygui.backend.data.data_records import is_official_csv, read_data_records, read_official_csv
|
||||
from pygui.backend.wafer.wafer_layouts import WaferLayout, resolve_shape_and_size
|
||||
|
||||
frames = []
|
||||
|
||||
if is_official_csv(file_path):
|
||||
sensors, records = read_official_csv(file_path)
|
||||
frames = frames_from_wafer_data(None, records)
|
||||
else:
|
||||
try:
|
||||
data, _ = ZWaferParser().parse(file_path)
|
||||
except (ValueError, KeyError) as exc:
|
||||
log.warning("Could not parse %s: %s", file_path, exc)
|
||||
return
|
||||
if data is None or not data.sensors:
|
||||
log.warning("Could not parse %s", file_path)
|
||||
return
|
||||
records = read_data_records(file_path)
|
||||
sensors = data.sensors
|
||||
frames = frames_from_wafer_data(data, records)
|
||||
|
||||
if not sensors or not frames:
|
||||
log.warning("No sensors or data in %s", file_path)
|
||||
return
|
||||
|
||||
wafer_id = ""
|
||||
if not is_official_csv(file_path):
|
||||
wafer_id = data.serial if (data and data.serial) else ""
|
||||
else:
|
||||
stem = Path(file_path).stem
|
||||
wafer_id = stem.split("-")[0] if "-" in stem else stem
|
||||
|
||||
shape, size = resolve_shape_and_size(sensors, wafer_id)
|
||||
self._sensors = WaferLayout(sensors, shape=shape, size=size)
|
||||
self._active_clusters = getattr(self._sensors, 'clusters', [])
|
||||
if not self._active_clusters:
|
||||
self._active_clusters = group_sensors_by_radius(self._sensors)
|
||||
self._player.load(frames)
|
||||
self._model.reset()
|
||||
self._loaded_file = file_path
|
||||
self.loadedFileChanged.emit()
|
||||
self.sensorsChanged.emit()
|
||||
self._emit_current()
|
||||
|
||||
@Slot()
|
||||
def play(self) -> None:
|
||||
self._play_timer.start(self._next_interval_ms())
|
||||
|
||||
@Slot()
|
||||
def pause(self) -> None: self._play_timer.stop()
|
||||
|
||||
@Slot()
|
||||
def stop(self) -> None:
|
||||
self._play_timer.stop()
|
||||
self._player.seek(0)
|
||||
self._emit_current()
|
||||
|
||||
@Slot(int)
|
||||
def step(self, delta: int) -> None:
|
||||
self._play_timer.stop()
|
||||
self._player.step(delta)
|
||||
self._emit_current()
|
||||
|
||||
@Slot(int)
|
||||
def seek(self, i: int) -> None:
|
||||
self._player.seek(i)
|
||||
self._emit_current()
|
||||
|
||||
@Slot(float)
|
||||
def setSpeed(self, x: float) -> None:
|
||||
self._speed = max(0.1, x)
|
||||
if self._play_timer.isActive():
|
||||
self._play_timer.start(self._next_interval_ms())
|
||||
|
||||
def _next_interval_ms(self) -> int:
|
||||
"""Timer interval in ms for the next frame, scaled by playback speed. Falls back to 500ms."""
|
||||
ms = self._player.next_frame_ms()
|
||||
if ms <= 0:
|
||||
ms = 500.0
|
||||
return max(1, int(ms / self._speed))
|
||||
|
||||
def _advance(self) -> None:
|
||||
if self._player.at_end:
|
||||
self._play_timer.stop()
|
||||
return
|
||||
self._player.step(1)
|
||||
self._emit_current()
|
||||
self._play_timer.start(self._next_interval_ms())
|
||||
|
||||
def _apply_pipeline(self, raw_values: list[float]) -> list[float]:
|
||||
"""Apply override first, then optional cluster averaging."""
|
||||
edited = self._sensor_editor.apply(raw_values)
|
||||
if getattr(self, "_cluster_averaging_enabled", False):
|
||||
return average_clusters(edited, getattr(self, "_active_clusters", []))
|
||||
return edited
|
||||
|
||||
def _emit_current(self) -> None:
|
||||
frame = self._player.current()
|
||||
if frame is None:
|
||||
return
|
||||
self._last_raw_frame = frame
|
||||
edited = Frame(seq=frame.seq, time=frame.time,
|
||||
values=self._apply_pipeline(frame.values))
|
||||
self._last = self._model.process(edited)
|
||||
self.frameUpdated.emit()
|
||||
self.stateChanged.emit()
|
||||
|
||||
def _reprocess_current(self) -> None:
|
||||
"""Re-run the model on the last raw frame"""
|
||||
frame = self._last_raw_frame
|
||||
if frame is None:
|
||||
return
|
||||
edited = Frame(seq=frame.seq, time=frame.time,
|
||||
values=self._apply_pipeline(frame.values))
|
||||
self._last = self._model.process(edited)
|
||||
self.frameUpdated.emit()
|
||||
self.stateChanged.emit()
|
||||
|
||||
# ---- live: stream start/stop ----
|
||||
@Slot(str, str)
|
||||
def startStream(self, port: str, family_code: str = "") -> None:
|
||||
from pygui.serialcomm.serial_port import SerialPort # transport open
|
||||
import serial as pyserial
|
||||
from pygui.backend.wafer.wafer_layouts import load_layout_for_wafer_id
|
||||
import time
|
||||
|
||||
if family_code:
|
||||
try:
|
||||
self._sensors = load_layout_for_wafer_id(family_code)
|
||||
self.sensorsChanged.emit()
|
||||
except Exception as e:
|
||||
log.warning("Could not load layout for %s: %s", family_code, e)
|
||||
|
||||
self._active_clusters = getattr(self._sensors, 'clusters', [])
|
||||
if not self._active_clusters:
|
||||
self._active_clusters = group_sensors_by_radius(self._sensors)
|
||||
|
||||
# The new binary protocol sends payload of (sensorCount * 2) bytes
|
||||
# Each sensor is a big-endian 16-bit value (Sign-Magnitude).
|
||||
expected_sensors = 80 if (family_code or "") == "X" else 244
|
||||
|
||||
def parse_binary_frame(payload: bytes, seq: int) -> Frame:
|
||||
values = []
|
||||
num_sensors = min(expected_sensors, len(payload) // 2)
|
||||
for i in range(num_sensors):
|
||||
high_byte = payload[i * 2]
|
||||
low_byte = payload[i * 2 + 1]
|
||||
val16 = (high_byte << 8) | low_byte
|
||||
|
||||
is_negative = (val16 & 0x8000) != 0
|
||||
integer_part = (val16 >> 7) & 0xFF
|
||||
fractional_bits = val16 & 0x7F
|
||||
fractional_part = fractional_bits / 128.0
|
||||
result = integer_part + fractional_part
|
||||
if is_negative:
|
||||
result = -result
|
||||
values.append(result)
|
||||
|
||||
return Frame(seq=seq, time=time.monotonic(), values=values)
|
||||
|
||||
# Clear out any old data from the prev sessions
|
||||
self._model.reset()
|
||||
self._last = None
|
||||
self._last_raw_frame = None
|
||||
|
||||
transport = SerialPort.open_port(port, timeout=1)
|
||||
# Send 'D2' command padded to 512 bytes to start the stream
|
||||
cmd = b"D2" + SerialPort.COMMAND_PAD.encode()
|
||||
transport.write(cmd)
|
||||
|
||||
def on_error(exc: Exception):
|
||||
log.error("Live stream error: %s", exc)
|
||||
|
||||
self._reader = StreamReader(
|
||||
transport, parse_binary_frame,
|
||||
on_frame=lambda f: self._liveFrame.emit(f),
|
||||
on_error=on_error,
|
||||
family_code=family_code or "A")
|
||||
self._reader.start()
|
||||
self._repaint_timer.start()
|
||||
|
||||
self.setMode("live")
|
||||
self.stateChanged.emit()
|
||||
|
||||
@Slot()
|
||||
def stopStream(self) -> None:
|
||||
self._repaint_timer.stop()
|
||||
if self._reader:
|
||||
transport = self._reader._transport
|
||||
self._reader.stop()
|
||||
self._reader = None
|
||||
|
||||
# Send 'D2S' command padded to 512 bytes to stop the stream
|
||||
if transport and transport.is_open:
|
||||
try:
|
||||
cmd = b"D2S" + (b"F" * 509)
|
||||
transport.write(cmd)
|
||||
transport.flush()
|
||||
except Exception as exc:
|
||||
log.error("Error sending stop command: %s", exc)
|
||||
|
||||
self.stateChanged.emit()
|
||||
self.stopRecording()
|
||||
|
||||
@Slot(object)
|
||||
def _on_live_frame(self, frame: Frame) -> None:
|
||||
# Main thread (queued). Record raw, process edited; mark dirty for repaint.
|
||||
self._last_raw_frame = frame
|
||||
edited = Frame(seq=frame.seq, time=frame.time,
|
||||
values=self._apply_pipeline(frame.values))
|
||||
self._last = self._model.process(edited)
|
||||
if self._recorder.is_recording:
|
||||
self._recorder.write(frame) # always record raw values, never edited
|
||||
self._dirty = True
|
||||
|
||||
def _flush_repaint(self) -> None:
|
||||
if not self._dirty:
|
||||
return
|
||||
self._dirty = False
|
||||
self.frameUpdated.emit()
|
||||
self.stateChanged.emit()
|
||||
|
||||
# ---- recording ----
|
||||
@Slot(str, str)
|
||||
def startRecording(self, path: str, serial: str = "") -> None:
|
||||
self._recorder.start(path, self._sensors, serial)
|
||||
self.recordingChanged.emit()
|
||||
|
||||
@Slot()
|
||||
def stopRecording(self) -> None:
|
||||
if self._recorder.is_recording:
|
||||
self._recorder.stop()
|
||||
self.recordingChanged.emit()
|
||||
|
||||
@Slot(int, float)
|
||||
def replaceSensor(self, index: int, value: float) -> None:
|
||||
"""Override sensor `index` to display `value` every frame."""
|
||||
self._sensor_editor.set_replacement(index, value)
|
||||
self._reprocess_current()
|
||||
|
||||
@Slot(int, float)
|
||||
def offsetSensor(self, index: int, delta: float) -> None:
|
||||
"""Shift sensor `index` by `delta` every frame."""
|
||||
self._sensor_editor.set_offset(index, delta)
|
||||
self._reprocess_current()
|
||||
|
||||
@Slot(int)
|
||||
def clearSensorEdit(self, index: int) -> None:
|
||||
"""Remove all overrides for sensor `index`."""
|
||||
self._sensor_editor.clear(index)
|
||||
self._reprocess_current()
|
||||
|
||||
@Slot()
|
||||
def clearSensorEdits(self) -> None:
|
||||
"""Remove all sensor overrides."""
|
||||
self._sensor_editor.clear()
|
||||
self._reprocess_current()
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# ===== Crypto Sub-package =====
|
||||
from pygui.backend.crypto.crypto_helper import * # noqa: F403
|
||||
@@ -0,0 +1,16 @@
|
||||
# ===== Data Sub-package =====
|
||||
from pygui.backend.data.csv_recorder import CsvRecorder
|
||||
from pygui.backend.data.csv_file_metadata import CSVFileMetadata
|
||||
from pygui.backend.data.data_records import read_data_records, is_official_csv, read_official_csv
|
||||
from pygui.backend.data.data_segment import DataSegment
|
||||
from pygui.backend.data.file_browser import FileBrowser
|
||||
from pygui.backend.data.local_settings import LocalSettings
|
||||
from pygui.backend.data.local_settings_model import LocalSettingsModel
|
||||
|
||||
__all__ = [
|
||||
"CsvRecorder", "CSVFileMetadata",
|
||||
"read_data_records", "is_official_csv", "read_official_csv",
|
||||
"DataSegment",
|
||||
"FileBrowser",
|
||||
"LocalSettings", "LocalSettingsModel",
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
# ===== Backend Data Constants =====
|
||||
|
||||
DEFAULT_DATA_DIR_NAME = "isc_data"
|
||||
@@ -25,8 +25,8 @@ class CSVFileMetadata:
|
||||
try:
|
||||
return datetime.strptime(self.date, date_format)
|
||||
except (ValueError, TypeError):
|
||||
# If format is wrong or date is None, return current time
|
||||
return datetime.now()
|
||||
# If format is wrong or date is None, return None
|
||||
return None
|
||||
|
||||
# ===== Formatting =====
|
||||
@staticmethod
|
||||
@@ -48,4 +48,5 @@ class CSVFileMetadata:
|
||||
|
||||
def string_date_format(self) -> str:
|
||||
"""Helper to ensure the date is always a string for JSON."""
|
||||
return self.format_date(self.get_date())
|
||||
dt = self.get_date()
|
||||
return self.format_date(dt) if dt else self.date
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Append live frames"""
|
||||
from __future__ import annotations
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional, TextIO
|
||||
|
||||
|
||||
from pygui.backend.models.frame import Frame
|
||||
from pygui.backend.wafer.zwafer_models import Sensor
|
||||
|
||||
class CsvRecorder:
|
||||
def __init__(self) -> None:
|
||||
self._file_handle: Optional[TextIO] = None
|
||||
self._oath: Optional[str] = None
|
||||
|
||||
|
||||
@property
|
||||
def is_recording(self) -> bool:
|
||||
return self._file_handle is not None
|
||||
|
||||
|
||||
def start(self, path: str, sensors:list[Sensor], serial: str = "") -> None:
|
||||
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
||||
file_handle = open(path, "w", encoding="utf-8", newline ="")
|
||||
file_handle.write(f"Wafer ID={serial}\n")
|
||||
file_handle.write(f"Acquisition Date={datetime.now():%m/%d/%Y}\n")
|
||||
file_handle.write("Label," + ",".join(s.label for s in sensors) + "\n")
|
||||
file_handle.write("X (mm)," + ",".join(str(s.x) for s in sensors) + "\n")
|
||||
file_handle.write("Y (mm)," + ",".join(str(s.y) for s in sensors) + "\n")
|
||||
file_handle.write("data\n")
|
||||
file_handle.flush()
|
||||
self._file_handle, self._path = file_handle, path
|
||||
|
||||
def write(self, frame: Frame) -> None:
|
||||
if self._file_handle is None:
|
||||
return
|
||||
row = [f"{frame.time:.3f}"] + [f"{v:.3f}" for v in frame.values]
|
||||
self._file_handle.write(",".join(row) + "\n")
|
||||
self._file_handle.flush()
|
||||
|
||||
def stop(self) -> Optional[str]:
|
||||
if self._file_handle is None:
|
||||
return None
|
||||
self._file_handle.close()
|
||||
path, self._file_handle, self._path = self._path, None, None
|
||||
return path
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Read the data section of a Z-wafer CSV into DataRecords, or the official headerless CSV."""
|
||||
from __future__ import annotations
|
||||
from pathlib import Path
|
||||
|
||||
from pygui.backend.wafer.zwafer_models import DataRecord, Sensor
|
||||
|
||||
def read_data_records(file_path: str) -> list[DataRecord]:
|
||||
records: list[DataRecord] = []
|
||||
in_data = False
|
||||
with Path(file_path).open("r", encoding="utf-8") as file_handle:
|
||||
for line in file_handle:
|
||||
line = line.rstrip().rstrip(",").strip()
|
||||
if not line:
|
||||
continue
|
||||
if not in_data:
|
||||
if line.split(",")[0].lower() == "data":
|
||||
in_data = True
|
||||
continue
|
||||
cols = [c.strip() for c in line.split(",") if c.strip() != ""]
|
||||
try:
|
||||
nums = [float(c) for c in cols]
|
||||
except ValueError:
|
||||
continue
|
||||
if not nums:
|
||||
continue
|
||||
records.append(DataRecord(time=nums[0], values=nums[1:]))
|
||||
return records
|
||||
|
||||
|
||||
def is_official_csv(file_path: str) -> bool:
|
||||
"""Return True if the file uses the headerless official format (sensor-name header row, no 'data' sentinel)."""
|
||||
with Path(file_path).open("r", encoding="utf-8") as f:
|
||||
first = f.readline().rstrip().rstrip(",")
|
||||
if not first:
|
||||
return False
|
||||
parts = [p.strip() for p in first.split(",") if p.strip()]
|
||||
# Official format: first cell looks like "SensorN" (starts with letter, not key=value)
|
||||
return bool(parts) and parts[0].lower().startswith("sensor") and "=" not in parts[0]
|
||||
|
||||
|
||||
def read_official_csv(file_path: str) -> tuple[list[Sensor], list[DataRecord]]:
|
||||
"""Parse the headerless official CSV: row 0 = sensor names, rows 1+ = values (no timestamp)."""
|
||||
from pygui.backend.wafer.wafer_layouts import load_layout_for_wafer_id
|
||||
|
||||
stem = Path(file_path).stem
|
||||
wafer_id = stem.split("-")[0] if "-" in stem else stem
|
||||
try:
|
||||
sensors = load_layout_for_wafer_id(wafer_id)
|
||||
except KeyError:
|
||||
return [], []
|
||||
|
||||
records: list[DataRecord] = []
|
||||
with Path(file_path).open("r", encoding="utf-8") as f:
|
||||
f.readline() # skip sensor-name header
|
||||
for i, line in enumerate(f):
|
||||
line = line.rstrip().rstrip(",")
|
||||
if not line:
|
||||
continue
|
||||
cols = [c.strip() for c in line.split(",") if c.strip()]
|
||||
try:
|
||||
values = [float(c) for c in cols]
|
||||
except ValueError:
|
||||
continue
|
||||
if values:
|
||||
records.append(DataRecord(time=float(i) * 0.5, values=values))
|
||||
return sensors, records
|
||||
@@ -8,8 +8,9 @@ from pathlib import Path
|
||||
from PySide6.QtCore import QObject, Property, QStandardPaths, Signal, Slot
|
||||
from PySide6.QtWidgets import QFileDialog, QMessageBox
|
||||
|
||||
from backend.csv_file_metadata import CSVFileMetadata
|
||||
from backend.zwafer_parser import ZWaferParser
|
||||
from pygui.backend.data.csv_file_metadata import CSVFileMetadata
|
||||
from pygui.backend.wafer.zwafer_parser import ZWaferParser
|
||||
from pygui.backend.data.constants import DEFAULT_DATA_DIR_NAME
|
||||
|
||||
|
||||
# ===== File Browser Model =====
|
||||
@@ -120,24 +121,49 @@ class FileBrowser(QObject):
|
||||
for csv_path in sorted(self._current_directory.glob("*.csv")):
|
||||
try:
|
||||
metadata = self._load_metadata(csv_path)
|
||||
wafer_type = metadata.get_wafer_type().upper() or (csv_path.stem[0].upper() if csv_path.stem else "")
|
||||
date_str = metadata.string_date_format()
|
||||
serial = metadata.wafer[1:] if len(metadata.wafer) > 1 else ""
|
||||
if not serial:
|
||||
stem = csv_path.stem
|
||||
serial = stem[1:] if len(stem) > 1 else ""
|
||||
if "-" in serial:
|
||||
serial = serial.split("-")[0]
|
||||
if "_" in serial:
|
||||
serial = serial.split("_")[0]
|
||||
time_str = date_str[11:] if len(date_str) > 10 else ""
|
||||
self._files.append(
|
||||
{
|
||||
"selected": selected_state.get(str(csv_path), False),
|
||||
"baseName": csv_path.stem,
|
||||
"wafer": metadata.wafer,
|
||||
"date": metadata.string_date_format(),
|
||||
"waferType": wafer_type,
|
||||
"serialNumber": serial,
|
||||
"date": date_str,
|
||||
"timeStr": time_str,
|
||||
"chamber": metadata.chamber,
|
||||
"notes": metadata.notes,
|
||||
"masterType": master_state.get(str(csv_path), "") or metadata.master_type,
|
||||
"fileName": str(csv_path),
|
||||
"highlight": metadata.get_wafer_type() in {"A", "B", "C"},
|
||||
"highlight": wafer_type in {"A", "B", "C"},
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
stem = csv_path.stem
|
||||
serial_fallback = stem[1:] if len(stem) > 1 else ""
|
||||
if "-" in serial_fallback:
|
||||
serial_fallback = serial_fallback.split("-")[0]
|
||||
if "_" in serial_fallback:
|
||||
serial_fallback = serial_fallback.split("_")[0]
|
||||
self._files.append(
|
||||
{
|
||||
"selected": selected_state.get(str(csv_path), False),
|
||||
"wafer": csv_path.stem,
|
||||
"baseName": stem,
|
||||
"wafer": stem,
|
||||
"waferType": stem[0].upper() if stem else "",
|
||||
"serialNumber": serial_fallback,
|
||||
"date": "",
|
||||
"timeStr": "",
|
||||
"chamber": "",
|
||||
"notes": "Unable to parse metadata",
|
||||
"masterType": master_state.get(str(csv_path), ""),
|
||||
@@ -168,25 +194,19 @@ class FileBrowser(QObject):
|
||||
# Fall back to CSV/header parsing if sidecar is malformed.
|
||||
pass
|
||||
|
||||
meta = self._metadata_from_filename(csv_path)
|
||||
parser_data, _ = self._parser.parse(str(csv_path))
|
||||
if parser_data is not None:
|
||||
wafer = parser_data.serial or ""
|
||||
date_text = ""
|
||||
if parser_data.serial:
|
||||
meta.wafer = parser_data.serial
|
||||
if parser_data.date != datetime.min:
|
||||
date_text = CSVFileMetadata.format_date(parser_data.date)
|
||||
return CSVFileMetadata(
|
||||
wafer=wafer,
|
||||
date=date_text,
|
||||
chamber="",
|
||||
notes="",
|
||||
filename=str(csv_path),
|
||||
)
|
||||
meta.date = CSVFileMetadata.format_date(parser_data.date)
|
||||
|
||||
return self._metadata_from_filename(csv_path)
|
||||
return meta
|
||||
|
||||
def _metadata_from_filename(self, csv_path: Path) -> CSVFileMetadata:
|
||||
match = re.match(
|
||||
r"^(?P<wafer>[^-]+)-(?P<date>\d{8})(?:_(?P<run>\d+))?$",
|
||||
r"^(?P<wafer>[^-]+)-(?P<date>\d{8})(?:_(?P<time>\d{6}))?$",
|
||||
csv_path.stem,
|
||||
)
|
||||
if not match:
|
||||
@@ -194,7 +214,9 @@ class FileBrowser(QObject):
|
||||
|
||||
date_text = ""
|
||||
try:
|
||||
parsed = datetime.strptime(match.group("date"), "%Y%m%d")
|
||||
date_part = match.group("date")
|
||||
time_part = match.group("time") or "000000"
|
||||
parsed = datetime.strptime(date_part + time_part, "%Y%m%d%H%M%S")
|
||||
date_text = CSVFileMetadata.format_date(parsed)
|
||||
except ValueError:
|
||||
date_text = ""
|
||||
@@ -211,7 +233,7 @@ class FileBrowser(QObject):
|
||||
QStandardPaths.DocumentsLocation
|
||||
)
|
||||
base_dir = Path(documents_dir) if documents_dir else (Path.home() / "Documents")
|
||||
return base_dir / "isc_data"
|
||||
return base_dir / DEFAULT_DATA_DIR_NAME
|
||||
|
||||
def _set_current_directory(self, directory: Path) -> None:
|
||||
normalized = Path(directory)
|
||||
@@ -236,7 +258,7 @@ class FileBrowser(QObject):
|
||||
def showInfo(self, message: str) -> None:
|
||||
self._show_info(message)
|
||||
|
||||
@Slot(str, result="QVariantMap")
|
||||
@Slot(str, result=dict)
|
||||
def parseCsvMetadata(self, file_path: str) -> dict:
|
||||
from pathlib import Path as _Path
|
||||
|
||||
@@ -6,7 +6,8 @@ from typing import Any
|
||||
|
||||
from PySide6.QtCore import QObject, Property, QDateTime, QStandardPaths, Signal, Slot
|
||||
|
||||
from backend.local_settings import LocalSettings
|
||||
from pygui.backend.data.local_settings import LocalSettings
|
||||
from pygui.backend.data.constants import DEFAULT_DATA_DIR_NAME
|
||||
|
||||
|
||||
MASTER_FAMILIES = ("A", "B", "C", "D", "E", "F", "P", "X", "Z")
|
||||
@@ -52,7 +53,7 @@ class LocalSettingsModel(QObject):
|
||||
QStandardPaths.DocumentsLocation
|
||||
)
|
||||
base_dir = Path(documents_dir) if documents_dir else (Path.home() / "Documents")
|
||||
return base_dir / "isc_data"
|
||||
return base_dir / DEFAULT_DATA_DIR_NAME
|
||||
|
||||
def _new_defaults(self) -> dict[str, Any]:
|
||||
return {
|
||||
@@ -221,7 +222,7 @@ class LocalSettingsModel(QObject):
|
||||
self.waferRetriesChanged.emit()
|
||||
self._recompute_derived()
|
||||
|
||||
@Property("QVariantMap", notify=mastersChanged)
|
||||
@Property(dict, notify=mastersChanged)
|
||||
def masters(self) -> dict[str, str]:
|
||||
return dict(self._masters)
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# ===== Models Sub-package =====
|
||||
from pygui.backend.models.frame import Frame
|
||||
from pygui.backend.models.frame_player import FramePlayer, frames_from_wafer_data
|
||||
from pygui.backend.models.frame_stats import Stats, compute_stats
|
||||
from pygui.backend.models.session_model import SessionModel, SessionUpdate
|
||||
from pygui.backend.models.data_model import TemperatureTableModel
|
||||
from pygui.backend.models.sensor_editor import SensorEditor
|
||||
from pygui.backend.models.threshold_classifier import ThresholdConfig, classify, resolve_bounds
|
||||
from pygui.backend.models.stability_detector import StabilityDetector
|
||||
|
||||
__all__ = [
|
||||
"Frame", "FramePlayer", "frames_from_wafer_data",
|
||||
"Stats", "compute_stats",
|
||||
"SessionModel", "SessionUpdate",
|
||||
"TemperatureTableModel",
|
||||
"SensorEditor",
|
||||
"ThresholdConfig", "classify", "resolve_bounds",
|
||||
"StabilityDetector",
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Frame:
|
||||
"""One sample across all sensors ata a point in time"""
|
||||
|
||||
seq: int # monotonically increasing
|
||||
time: float # seconds (relative or epoch)
|
||||
values: list[float] # one per sensor, in sensor-layout order
|
||||
@@ -0,0 +1,51 @@
|
||||
from __future__ import annotations
|
||||
from pygui.backend.models.frame import Frame
|
||||
from pygui.backend.wafer.zwafer_models import ZWaferData
|
||||
|
||||
def frames_from_wafer_data(data: ZWaferData, records) -> list[Frame]:
|
||||
"""Build Frames from parsed DataRecords (records = list[DataRecord].)"""
|
||||
return [Frame(seq=i, time=r.time, values=list(r.values)) for i, r in enumerate(records)]
|
||||
|
||||
class FramePlayer:
|
||||
def __init__(self) -> None:
|
||||
self._frames: list[Frame] =[]
|
||||
self._i =0
|
||||
|
||||
def load(self, frames: list[Frame]) -> "FramePlayer":
|
||||
self._frames = frames
|
||||
self._i = 0
|
||||
return self
|
||||
|
||||
@property
|
||||
def total(self) -> int:
|
||||
return len(self._frames)
|
||||
|
||||
@property
|
||||
def index(self) -> int:
|
||||
return self._i
|
||||
|
||||
@property
|
||||
def at_end(self) -> bool:
|
||||
return self._i >= len(self._frames) -1
|
||||
|
||||
def current(self) -> Frame | None:
|
||||
if not self._frames:
|
||||
return None
|
||||
return self._frames[self._i]
|
||||
|
||||
def _clamp(self, i:int):
|
||||
return max(0, min(i, len(self._frames) - 1)) if self._frames else 0
|
||||
|
||||
def step(self, delta: int) -> "FramePlayer":
|
||||
self._i = self._clamp(self._i + delta)
|
||||
return self
|
||||
|
||||
def seek(self, i: int) -> "FramePlayer":
|
||||
self._i = self._clamp(i)
|
||||
return self
|
||||
|
||||
def next_frame_ms(self) -> float:
|
||||
"""Wall-clock ms until the next frame based on recorded timestamps; 0 if at end."""
|
||||
if self._i + 1 >= len(self._frames):
|
||||
return 0.0
|
||||
return (self._frames[self._i + 1].time - self._frames[self._i].time) * 1000.0
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Per-frame descriptive statistics"""
|
||||
|
||||
from __future__ import annotations
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Stats:
|
||||
min: float; min_index: int
|
||||
max: float; max_index: int
|
||||
diff: float; avg: float
|
||||
sigma: float; three_sigma: float
|
||||
|
||||
|
||||
def compute_stats(values: list[float]) -> Stats:
|
||||
clean = [(i, v) for i, v in enumerate(values) if not math.isnan(v)]
|
||||
if not clean:
|
||||
return Stats(0.0, -1, 0.0, -1, 0.0, 0.0, 0.0, 0.0)
|
||||
|
||||
min_index, min_v = min(clean, key=lambda iv: iv[1])
|
||||
max_index, max_v = max(clean, key=lambda iv: iv[1])
|
||||
nums = [v for _, v in clean]
|
||||
avg = sum(nums) / len(nums)
|
||||
variance = sum((v - avg) ** 2 for v in nums) / len(nums)
|
||||
sigma = math.sqrt(variance)
|
||||
|
||||
return Stats(
|
||||
min=min_v,
|
||||
min_index=min_index,
|
||||
max=max_v,
|
||||
max_index=max_index,
|
||||
diff=max_v - min_v,
|
||||
avg=avg,
|
||||
sigma=sigma,
|
||||
three_sigma=3 * sigma,
|
||||
)
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Per-session sensor value overrides (replacement and/or offset)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class SensorEditor:
|
||||
def __init__(self) -> None:
|
||||
self._replacements: dict[int, float] = {}
|
||||
self._offsets: dict[int, float] = {}
|
||||
|
||||
|
||||
def set_replacement(self, index: int, value: float) -> None:
|
||||
"""Force sensor `index` to read `value` for every frame this session."""
|
||||
self._replacements[index] = value
|
||||
|
||||
def set_offset(self, index: int, value: float) -> None:
|
||||
"""Shift sensor `index` by `delta` (applied after any replacement)."""
|
||||
self._offsets[index] = value
|
||||
|
||||
def clear(self, index: int | None = None) -> None:
|
||||
"""Remove overrides. Pass an index to clear one sensor; omit to clear all."""
|
||||
if index is None:
|
||||
self._replacements.clear()
|
||||
self._offsets.clear()
|
||||
else:
|
||||
self._replacements.pop(index, None)
|
||||
self._offsets.pop(index, None)
|
||||
def has_overrides(self) -> bool:
|
||||
return bool(self._replacements or self._offsets)
|
||||
|
||||
def active_indices(self) -> list[int]:
|
||||
"""Return sorted sensor indices that have any override."""
|
||||
return sorted(set(self._replacements) | set(self._offsets))
|
||||
|
||||
|
||||
def apply(self, values: list[float]) -> list[float]:
|
||||
"""Return a new list wit all overrides applied (original unchanged)"""
|
||||
if not self.has_overrides():
|
||||
return list(values)
|
||||
result = list(values)
|
||||
for i in range(len(result)):
|
||||
v = result[i]
|
||||
if i in self._replacements:
|
||||
v = self._replacements[i]
|
||||
if i in self._offsets:
|
||||
v += self._offsets[i]
|
||||
result[i] = v
|
||||
return result
|
||||
@@ -0,0 +1,39 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pygui.backend.models.frame import Frame
|
||||
from pygui.backend.models.frame_stats import Stats, compute_stats
|
||||
from pygui.backend.models.stability_detector import StabilityDetector
|
||||
from pygui.backend.models.threshold_classifier import ThresholdConfig, classify, resolve_bounds
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SessionUpdate:
|
||||
seq: int
|
||||
values: list[float]
|
||||
bands: list[str]
|
||||
stats: Stats
|
||||
state: str
|
||||
target: float
|
||||
margin: float
|
||||
|
||||
class SessionModel:
|
||||
def __init__(self, thresholds: ThresholdConfig | None = None) -> None:
|
||||
self._config = thresholds or ThresholdConfig()
|
||||
self._stability = StabilityDetector()
|
||||
|
||||
def set_thresholds(self, config: ThresholdConfig) -> None:
|
||||
self._config = config
|
||||
|
||||
def reset(self) -> None:
|
||||
self._stability.reset()
|
||||
|
||||
def process(self, frame: Frame) -> SessionUpdate:
|
||||
stats = compute_stats(frame.values)
|
||||
target, margin = resolve_bounds(frame.values, self._config)
|
||||
bands = [classify(v, target, margin)for v in frame.values]
|
||||
# State always track the user's process set_point, even in auto-color mode.
|
||||
state = self._stability.update(stats.avg, frame.time, self._config.set_point)
|
||||
return SessionUpdate(
|
||||
seq=frame.seq, values=frame.values, bands=bands,
|
||||
stats=stats, state=state, target=target, margin=margin,
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Detect process state (Idle, Ramp, Set) from the running average temp"""
|
||||
from __future__ import annotations
|
||||
from typing import Optional
|
||||
|
||||
STATE_IDLE = "idle"
|
||||
STATE_RAMP = "ramp"
|
||||
STATE_SET = "set"
|
||||
|
||||
|
||||
class StabilityDetector:
|
||||
def __init__(self, idle_below: float = 50.0, tolerance: float = 1.0,
|
||||
settle_seconds: float = 10.0) -> None:
|
||||
self._idle_below = idle_below
|
||||
self._tolerance = tolerance
|
||||
self._settle_seconds = settle_seconds
|
||||
self._near_since: Optional[float] = None # When avg entered the +- tolerance band
|
||||
|
||||
|
||||
def reset(self) -> None:
|
||||
self._near_since = None
|
||||
|
||||
def update(self, avg: float, time: float, set_point: float) ->str:
|
||||
if avg < self._idle_below:
|
||||
self._near_since = None
|
||||
return STATE_IDLE
|
||||
if abs(avg - set_point) <= self._tolerance:
|
||||
if self._near_since is None:
|
||||
self._near_since = time
|
||||
if time - self._near_since >= self._settle_seconds:
|
||||
return STATE_SET
|
||||
return STATE_RAMP
|
||||
self._near_since = None
|
||||
return STATE_RAMP
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Classify sensor values into three bands around (target, margin)
|
||||
|
||||
Auto mode derives target=mean, margin=1
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
|
||||
BAND_IN = "in_range"
|
||||
BAND_HIGH = "high"
|
||||
BAND_LOW = "low"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ThresholdConfig:
|
||||
set_point: float = 149.0 # process target: used as band TARGET when auto=False
|
||||
margin: float = 1.0 # used as band MARGIN when auto=False
|
||||
auto: bool = True # auto=True: target=frame mean, margin=frame 1σ
|
||||
|
||||
def resolve_bounds(values: list[float], cfg: ThresholdConfig) -> tuple[float, float]:
|
||||
if not cfg.auto:
|
||||
return cfg.set_point, cfg.margin
|
||||
clean = [v for v in values if not math.isnan(v)]
|
||||
if not clean:
|
||||
return cfg.set_point, cfg.margin
|
||||
mean = sum(clean) / len(clean)
|
||||
variance = sum((v - mean) ** 2 for v in clean) / len(clean)
|
||||
return mean, math.sqrt(variance)
|
||||
|
||||
def classify(value: float, target: float, margin: float) -> str:
|
||||
if math.isnan(value):
|
||||
return BAND_IN
|
||||
if value > target + margin:
|
||||
return BAND_HIGH
|
||||
if value < target - margin:
|
||||
return BAND_LOW
|
||||
return BAND_IN
|
||||
|
||||
def classify_all(values: list[float], cfg: ThresholdConfig) -> list[str]:
|
||||
target, margin = resolve_bounds(values, cfg)
|
||||
return [classify(v, target, margin)for v in values ]
|
||||
@@ -0,0 +1,11 @@
|
||||
# ===== Visualization Sub-package =====
|
||||
from pygui.backend.visualization.wafer_map_item import WaferMapItem
|
||||
from pygui.backend.visualization.graph_view import GraphView
|
||||
from pygui.backend.visualization.rbf_heatmap import interpolate_field
|
||||
from pygui.backend.visualization.contour_models import ContourLine, ContourSegment
|
||||
|
||||
__all__ = [
|
||||
"WaferMapItem", "GraphView",
|
||||
"interpolate_field",
|
||||
"ContourLine", "ContourSegment",
|
||||
]
|
||||
@@ -2,7 +2,7 @@ from typing import List, Tuple, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from backend.contour_models import ContourLine, ContourSegment
|
||||
from pygui.backend.visualization.contour_models import ContourLine, ContourSegment
|
||||
|
||||
|
||||
# ===== Contour Generation =====
|
||||
@@ -0,0 +1,61 @@
|
||||
"""RBF (thin-plate spline) heatmap field.
|
||||
|
||||
Uses CuPy for GPU acceleration when available, falls back to NumPy + SciPy.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from scipy.interpolate import RBFInterpolator
|
||||
|
||||
try:
|
||||
import cupy as _cupy # type: ignore
|
||||
BACKEND = "cupy"
|
||||
except Exception:
|
||||
_cupy = None
|
||||
BACKEND = "numpy"
|
||||
|
||||
_KERNEL = "thin_plate_spline"
|
||||
_SMOOTHING = 0.0
|
||||
|
||||
|
||||
def interpolate_field(
|
||||
xs: np.ndarray,
|
||||
ys: np.ndarray,
|
||||
vs: np.ndarray,
|
||||
*,
|
||||
width: int,
|
||||
height: int,
|
||||
extent: tuple[float, float, float, float], # (xmin, xmax, ymin, ymax) in mm
|
||||
round_clip: bool = False,
|
||||
) -> np.ndarray:
|
||||
"""Return a (height, width) float64 array of interpolated values.
|
||||
|
||||
Args:
|
||||
xs, ys: sensor positions in mm (1-D arrays, length N)
|
||||
vs: sensor values (length N)
|
||||
width/height: output grid dimensions in pixels
|
||||
extent: (xmin, xmax, ymin, ymax) in the same mm space as xs/ys
|
||||
round_clip: if True, pixels outside the inscribed ellipse become NaN
|
||||
"""
|
||||
coords = np.column_stack([xs, ys])
|
||||
rbf = RBFInterpolator(coords, vs, kernel=_KERNEL, smoothing=_SMOOTHING)
|
||||
|
||||
xmin, xmax, ymin, ymax = extent
|
||||
gx = np.linspace(xmin, xmax, width)
|
||||
gy = np.linspace(ymin, ymax, height)
|
||||
grid_x, grid_y = np.meshgrid(gx, gy)
|
||||
flat = np.column_stack([grid_x.ravel(), grid_y.ravel()])
|
||||
|
||||
# RBFInterpolator always runs on CPU; CuPy only accelerates other ops if added later
|
||||
field = rbf(flat).reshape(height, width)
|
||||
|
||||
if round_clip:
|
||||
cx = (xmin + xmax) / 2
|
||||
cy = (ymin + ymax) / 2
|
||||
rx = (xmax - xmin) / 2
|
||||
ry = (ymax - ymin) / 2
|
||||
dist = ((grid_x - cx) / rx) ** 2 + ((grid_y - cy) / ry) ** 2
|
||||
field = np.where(dist <= 1.0, field, np.nan)
|
||||
|
||||
return field.astype(np.float64)
|
||||
|
||||
@@ -0,0 +1,511 @@
|
||||
"""QQuickPaintedItem wafer map — ported from the replay app's ReplayWidget.
|
||||
|
||||
Draws:
|
||||
• Radial ring template (concentric guides + crosshair axes + top notch)
|
||||
• RBF heatmap layer (blended under markers via `blend` 0→1)
|
||||
• Sensor marker circles colored by band (low/in_range/high)
|
||||
• Numbered labels (toggle via `showLabels`)
|
||||
|
||||
All sensor coordinates are center-origin mm (from wafer_layouts or a loaded CSV).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
from PySide6.QtCore import Property, QPoint, Signal, Slot, Qt
|
||||
from PySide6.QtGui import (
|
||||
QBrush, QColor, QFont, QImage, QPainter, QPen, QPolygon,
|
||||
)
|
||||
from PySide6.QtQml import QmlElement
|
||||
from PySide6.QtQuick import QQuickPaintedItem
|
||||
|
||||
from pygui.backend.visualization.rbf_heatmap import interpolate_field
|
||||
from pygui.backend.wafer.zwafer_models import Sensor
|
||||
|
||||
QML_IMPORT_NAME = "ISC.Wafer"
|
||||
QML_IMPORT_MAJOR_VERSION = 1
|
||||
|
||||
|
||||
@QmlElement
|
||||
class WaferMapItem(QQuickPaintedItem):
|
||||
"""Painted wafer map; driven by SessionController via QML property bindings."""
|
||||
|
||||
sensorsChanged = Signal()
|
||||
valuesChanged = Signal()
|
||||
bandsChanged = Signal()
|
||||
targetChanged = Signal()
|
||||
marginChanged = Signal()
|
||||
blendChanged = Signal()
|
||||
showLabelsChanged = Signal()
|
||||
colorsChanged = Signal()
|
||||
shapeChanged = Signal()
|
||||
sizeChanged = Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._sensors: list[Sensor] = []
|
||||
self._values: list[float] = []
|
||||
self._bands: list[str] = []
|
||||
self._target: float = 149.0
|
||||
self._margin: float = 1.0
|
||||
self._blend: float = 0.0
|
||||
self._show_labels: bool = True
|
||||
self._shape: str = "round"
|
||||
self._size: float = 300.0
|
||||
|
||||
# Dark-theme color defaults (match Theme.qml tokens)
|
||||
self._ring_color = QColor("#2A3441") # waferRingColor (toneBorder)
|
||||
self._axis_color = QColor("#3A4D5C") # waferAxisColor (softBorder)
|
||||
self._low_color = QColor("#5B9DF5") # sensorLow
|
||||
self._in_range_color = QColor("#22C55E") # sensorInRange
|
||||
self._high_color = QColor("#EF4444") # sensorHigh
|
||||
self._text_color = QColor("#CBD5E1") # bodyColor
|
||||
|
||||
# Internal draw state
|
||||
self._markers: dict[int, tuple[int, int]] = {} # sensor index → (px, py)
|
||||
self._marker_r: int = 4
|
||||
self._heatmap: QImage | None = None
|
||||
|
||||
self.widthChanged.connect(self._on_resize)
|
||||
self.heightChanged.connect(self._on_resize)
|
||||
|
||||
# ── Qt properties ────────────────────────────────────────────────────
|
||||
|
||||
@Property("QVariantList", notify=sensorsChanged)
|
||||
def sensors(self) -> list:
|
||||
return [{"label": s.label, "x": s.x, "y": s.y} for s in self._sensors]
|
||||
|
||||
@sensors.setter
|
||||
def sensors(self, val: list) -> None:
|
||||
self._sensors = [
|
||||
Sensor(
|
||||
label=d["label"],
|
||||
x=float(d["x"]),
|
||||
y=float(d["y"]),
|
||||
side=d.get("side", "right"),
|
||||
offset_x=float(d.get("offset_x", 0.0)),
|
||||
offset_y=float(d.get("offset_y", 0.0))
|
||||
)
|
||||
for d in (val or [])
|
||||
]
|
||||
self._rebuild()
|
||||
self.sensorsChanged.emit()
|
||||
|
||||
@Property("QVariantList", notify=valuesChanged)
|
||||
def values(self) -> list:
|
||||
return self._values
|
||||
|
||||
@values.setter
|
||||
def values(self, val: list) -> None:
|
||||
self._values = list(val or [])
|
||||
self._rebuild_heatmap()
|
||||
self.valuesChanged.emit()
|
||||
self.update()
|
||||
|
||||
@Property("QVariantList", notify=bandsChanged)
|
||||
def bands(self) -> list:
|
||||
return self._bands
|
||||
|
||||
@bands.setter
|
||||
def bands(self, val: list) -> None:
|
||||
self._bands = list(val or [])
|
||||
self.bandsChanged.emit()
|
||||
self.update()
|
||||
|
||||
@Property(float, notify=targetChanged)
|
||||
def target(self) -> float:
|
||||
return self._target
|
||||
|
||||
@target.setter
|
||||
def target(self, val: float) -> None:
|
||||
self._target = float(val)
|
||||
self._rebuild_heatmap()
|
||||
self.targetChanged.emit()
|
||||
self.update()
|
||||
|
||||
@Property(float, notify=marginChanged)
|
||||
def margin(self) -> float:
|
||||
return self._margin
|
||||
|
||||
@margin.setter
|
||||
def margin(self, val: float) -> None:
|
||||
self._margin = float(val)
|
||||
self._rebuild_heatmap()
|
||||
self.marginChanged.emit()
|
||||
self.update()
|
||||
|
||||
@Property(float, notify=blendChanged)
|
||||
def blend(self) -> float:
|
||||
return self._blend
|
||||
|
||||
@blend.setter
|
||||
def blend(self, val: float) -> None:
|
||||
self._blend = max(0.0, min(1.0, float(val)))
|
||||
if self._blend > 0 and self._heatmap is None:
|
||||
self._rebuild_heatmap()
|
||||
self.blendChanged.emit()
|
||||
self.update()
|
||||
|
||||
@Property(bool, notify=showLabelsChanged)
|
||||
def showLabels(self) -> bool:
|
||||
return self._show_labels
|
||||
|
||||
@showLabels.setter
|
||||
def showLabels(self, val: bool) -> None:
|
||||
self._show_labels = bool(val)
|
||||
self.showLabelsChanged.emit()
|
||||
self.update()
|
||||
|
||||
@Property(str, notify=shapeChanged)
|
||||
def shape(self) -> str:
|
||||
return self._shape
|
||||
|
||||
@shape.setter
|
||||
def shape(self, val: str) -> None:
|
||||
self._shape = str(val).lower()
|
||||
self._rebuild()
|
||||
self.shapeChanged.emit()
|
||||
|
||||
@Property(float, notify=sizeChanged)
|
||||
def size(self) -> float:
|
||||
return self._size
|
||||
|
||||
@size.setter
|
||||
def size(self, val: float) -> None:
|
||||
self._size = float(val)
|
||||
self._rebuild()
|
||||
self.sizeChanged.emit()
|
||||
|
||||
# Colour properties — QML can bind these to Theme tokens
|
||||
@Property(QColor, notify=colorsChanged)
|
||||
def ringColor(self) -> QColor: return self._ring_color
|
||||
@ringColor.setter
|
||||
def ringColor(self, c: QColor) -> None: self._ring_color = c; self.update()
|
||||
|
||||
@Property(QColor, notify=colorsChanged)
|
||||
def axisColor(self) -> QColor: return self._axis_color
|
||||
@axisColor.setter
|
||||
def axisColor(self, c: QColor) -> None: self._axis_color = c; self.update()
|
||||
|
||||
@Property(QColor, notify=colorsChanged)
|
||||
def lowColor(self) -> QColor: return self._low_color
|
||||
@lowColor.setter
|
||||
def lowColor(self, c: QColor) -> None: self._low_color = c; self.update()
|
||||
|
||||
@Property(QColor, notify=colorsChanged)
|
||||
def inRangeColor(self) -> QColor: return self._in_range_color
|
||||
@inRangeColor.setter
|
||||
def inRangeColor(self, c: QColor) -> None: self._in_range_color = c; self.update()
|
||||
|
||||
@Property(QColor, notify=colorsChanged)
|
||||
def highColor(self) -> QColor: return self._high_color
|
||||
@highColor.setter
|
||||
def highColor(self, c: QColor) -> None: self._high_color = c; self.update()
|
||||
|
||||
@Property(QColor, notify=colorsChanged)
|
||||
def textColor(self) -> QColor: return self._text_color
|
||||
@textColor.setter
|
||||
def textColor(self, c: QColor) -> None: self._text_color = c; self.update()
|
||||
|
||||
# ── slots ─────────────────────────────────────────────────────────────
|
||||
|
||||
@Slot(float, float, result=int)
|
||||
def which_marker(self, x: float, y: float) -> int:
|
||||
"""Return the sensor index nearest to (x, y) within marker radius, else -1."""
|
||||
r = max(self._marker_r, 6)
|
||||
for idx, (mx, my) in self._markers.items():
|
||||
if abs(mx - x) <= r and abs(my - y) <= r:
|
||||
return idx
|
||||
return -1
|
||||
|
||||
# ── internal ─────────────────────────────────────────────────────────
|
||||
|
||||
def _on_resize(self) -> None:
|
||||
self._rebuild()
|
||||
|
||||
def _rebuild(self) -> None:
|
||||
self._compute_markers()
|
||||
self._rebuild_heatmap()
|
||||
self.update()
|
||||
|
||||
def _draw_size(self) -> int:
|
||||
return max(1, int(min(self.width(), self.height())))
|
||||
|
||||
def _center(self) -> tuple[int, int]:
|
||||
return int(self.width() / 2), int(self.height() / 2)
|
||||
|
||||
def _wafer_radius_mm(self) -> float:
|
||||
"""Radius of the wafer bounding circle in mm (5% padding beyond outermost sensor)."""
|
||||
if not self._sensors:
|
||||
return 150.0
|
||||
r = max(math.hypot(s.x, s.y) for s in self._sensors)
|
||||
return r * 1.05
|
||||
|
||||
def _sensor_ring_radii_mm(self) -> list[float]:
|
||||
"""Distinct radial distances of sensor groups, sorted ascending, plus the outer boundary."""
|
||||
if not self._sensors:
|
||||
r = self._wafer_radius_mm()
|
||||
return [r * f for f in (0.25, 0.50, 0.75, 1.0)]
|
||||
# Cluster radii that are within 2 mm of each other into one ring; skip center point.
|
||||
radii = sorted(r for r in {math.hypot(s.x, s.y) for s in self._sensors} if r > 1.0)
|
||||
groups: list[float] = []
|
||||
for r in radii:
|
||||
if not groups or r - groups[-1] > 2.0:
|
||||
groups.append(r)
|
||||
else:
|
||||
groups[-1] = (groups[-1] + r) / 2 # merge close values
|
||||
# Always include the outer boundary ring so the wafer circle is drawn.
|
||||
outer = self._wafer_radius_mm()
|
||||
if not groups or outer - groups[-1] > 2.0:
|
||||
groups.append(outer)
|
||||
return groups
|
||||
|
||||
def _scale(self, ds: int, r_mm: float) -> float:
|
||||
"""Pixels per mm. The wafer radius maps to ds//2 - 24 px."""
|
||||
return (ds / 2 - 24) / r_mm
|
||||
|
||||
def _to_px(self, x_mm: float, y_mm: float, cx: int, cy: int, scale: float) -> tuple[int, int]:
|
||||
"""Center-origin mm → pixel (top-left origin). Y is flipped."""
|
||||
return cx + int(x_mm * scale), cy - int(y_mm * scale)
|
||||
|
||||
def _compute_markers(self) -> None:
|
||||
ds = self._draw_size()
|
||||
r_mm = self._wafer_radius_mm()
|
||||
sc = self._scale(ds, r_mm)
|
||||
cx, cy = self._center()
|
||||
self._marker_r = max(3, ds // 70)
|
||||
self._markers = {i: self._to_px(s.x, s.y, cx, cy, sc)
|
||||
for i, s in enumerate(self._sensors)}
|
||||
|
||||
def _rebuild_heatmap(self) -> None:
|
||||
if not self._sensors or not self._values or self._blend == 0.0:
|
||||
self._heatmap = None
|
||||
return
|
||||
ds = self._draw_size()
|
||||
r_mm = self._wafer_radius_mm()
|
||||
xs = np.array([s.x for s in self._sensors])
|
||||
ys = np.array([s.y for s in self._sensors])
|
||||
vs = np.array(self._values[:len(self._sensors)], dtype=float)
|
||||
if len(vs) < len(self._sensors):
|
||||
self._heatmap = None
|
||||
return
|
||||
try:
|
||||
field = interpolate_field(
|
||||
xs, ys, vs,
|
||||
width=ds, height=ds,
|
||||
extent=(-r_mm, r_mm, -r_mm, r_mm),
|
||||
round_clip=(self._shape == "round"),
|
||||
)
|
||||
except Exception:
|
||||
self._heatmap = None
|
||||
return
|
||||
self._heatmap = self._field_to_qimage(field, ds)
|
||||
|
||||
def _field_to_qimage(self, field: np.ndarray, ds: int) -> QImage:
|
||||
"""Apply a band-aware tri-color gradient → RGBA QImage."""
|
||||
lo_b = self._target - self._margin
|
||||
hi_b = self._target + self._margin
|
||||
span = hi_b - lo_b or 1.0
|
||||
# t: 0 = lo_b, 1 = hi_b (clipped)
|
||||
t = np.clip((field - lo_b) / span, 0.0, 1.0)
|
||||
|
||||
def c(q: QColor) -> np.ndarray:
|
||||
return np.array([q.redF(), q.greenF(), q.blueF()], dtype=np.float32)
|
||||
|
||||
lo_c = c(self._low_color)
|
||||
mid_c = c(self._in_range_color)
|
||||
hi_c = c(self._high_color)
|
||||
|
||||
t2 = t * 2 # 0→2 across full range
|
||||
|
||||
lower = t <= 0.5
|
||||
t_lo = np.clip(t2, 0.0, 1.0)[:, :, np.newaxis] # 0→1 in lower half
|
||||
t_hi = np.clip(t2 - 1.0, 0.0, 1.0)[:, :, np.newaxis] # 0→1 in upper half
|
||||
|
||||
rgb = np.where(
|
||||
lower[:, :, np.newaxis],
|
||||
lo_c * (1 - t_lo) + mid_c * t_lo,
|
||||
mid_c * (1 - t_hi) + hi_c * t_hi,
|
||||
)
|
||||
# Outside the wafer circle `field` is NaN → NaN propagates into rgb. Alpha
|
||||
# masks those pixels anyway, but zero them so the uint8 cast is well-defined.
|
||||
rgb = np.nan_to_num(rgb, nan=0.0)
|
||||
rgba = np.zeros((ds, ds, 4), dtype=np.uint8)
|
||||
rgba[:, :, :3] = (rgb * 255).astype(np.uint8)
|
||||
rgba[:, :, 3] = np.where(np.isfinite(field), 210, 0).astype(np.uint8)
|
||||
|
||||
return QImage(rgba.tobytes(), ds, ds, QImage.Format.Format_RGBA8888).copy()
|
||||
|
||||
# ── paint ─────────────────────────────────────────────────────────────
|
||||
|
||||
def paint(self, painter: QPainter) -> None:
|
||||
ds = self._draw_size()
|
||||
r_px = int(ds / 2 - 4)
|
||||
cx, cy = self._center()
|
||||
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
|
||||
self._paint_template(painter, cx, cy, r_px)
|
||||
|
||||
if self._heatmap and self._blend > 0.0:
|
||||
painter.setOpacity(self._blend)
|
||||
painter.drawImage(cx - self._heatmap.width() // 2,
|
||||
cy - self._heatmap.height() // 2, self._heatmap)
|
||||
painter.setOpacity(1.0)
|
||||
|
||||
self._paint_markers(painter)
|
||||
|
||||
def _paint_template(self, painter: QPainter, cx: int, cy: int, r_px: int) -> None:
|
||||
ds = self._draw_size()
|
||||
r_mm = self._wafer_radius_mm()
|
||||
sc = self._scale(ds, r_mm)
|
||||
|
||||
if self._shape == "square":
|
||||
# Draw square boundary (thick pen)
|
||||
border_pen = QPen(self._ring_color, 2, Qt.PenStyle.SolidLine)
|
||||
painter.setPen(border_pen)
|
||||
half_size_px = int(self._size / 2 * sc)
|
||||
painter.drawRect(cx - half_size_px, cy - half_size_px, 2 * half_size_px, 2 * half_size_px)
|
||||
|
||||
# Crosshair axes
|
||||
axis_pen = QPen(self._axis_color, 1, Qt.PenStyle.DashLine)
|
||||
painter.setPen(axis_pen)
|
||||
painter.drawLine(cx, cy - half_size_px, cx, cy + half_size_px)
|
||||
painter.drawLine(cx - half_size_px, cy, cx + half_size_px, cy)
|
||||
|
||||
# Draw concentric square guide lines at the distinct radii of sensor rings
|
||||
grid_pen = QPen(self._ring_color, 1, Qt.PenStyle.SolidLine)
|
||||
painter.setPen(grid_pen)
|
||||
for ring_r_mm in self._sensor_ring_radii_mm()[:-1]: # exclude the outermost border
|
||||
rr = max(1, int(ring_r_mm * sc))
|
||||
painter.drawRect(cx - rr, cy - rr, 2 * rr, 2 * rr)
|
||||
else:
|
||||
# Concentric rings
|
||||
ring_pen = QPen(self._ring_color, 1, Qt.PenStyle.SolidLine)
|
||||
painter.setPen(ring_pen)
|
||||
for ring_r_mm in self._sensor_ring_radii_mm():
|
||||
rr = max(1, int(ring_r_mm * sc))
|
||||
painter.drawEllipse(cx - rr, cy - rr, 2 * rr, 2 * rr)
|
||||
|
||||
# Crosshair axes
|
||||
axis_pen = QPen(self._axis_color, 1, Qt.PenStyle.DashLine)
|
||||
painter.setPen(axis_pen)
|
||||
painter.drawLine(cx, cy - r_px, cx, cy + r_px)
|
||||
painter.drawLine(cx - r_px, cy, cx + r_px, cy)
|
||||
|
||||
# Top notch triangle (wafer orientation marker)
|
||||
nw = max(6, ds // 25)
|
||||
nh = max(4, ds // 35)
|
||||
notch = QPolygon([
|
||||
QPoint(cx, cy - r_px),
|
||||
QPoint(cx - nw // 2, cy - r_px + nh),
|
||||
QPoint(cx + nw // 2, cy - r_px + nh),
|
||||
])
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.setBrush(QBrush(self._axis_color))
|
||||
painter.drawPolygon(notch)
|
||||
|
||||
def _paint_markers(self, painter: QPainter) -> None:
|
||||
r = self._marker_r
|
||||
|
||||
# Scale font size based on the number of sensors to prevent overlap on dense wafers
|
||||
num_sensors = len(self._sensors)
|
||||
font_scale = 1.0
|
||||
if num_sensors > 60:
|
||||
font_scale = 0.7 # reduce font size by 30% for dense wafers
|
||||
elif num_sensors > 40:
|
||||
font_scale = 0.85
|
||||
|
||||
id_font = QFont()
|
||||
id_font.setPointSize(max(4, int(r * font_scale)))
|
||||
id_font.setBold(True)
|
||||
|
||||
temp_font = QFont()
|
||||
temp_font.setPointSize(max(3, int((r - 1) * font_scale)))
|
||||
|
||||
band_color = {
|
||||
"in_range": self._in_range_color,
|
||||
"high": self._high_color,
|
||||
"low": self._low_color,
|
||||
}
|
||||
|
||||
for i, s in enumerate(self._sensors):
|
||||
if i not in self._markers:
|
||||
continue
|
||||
px, py = self._markers[i]
|
||||
color = band_color.get(
|
||||
self._bands[i] if i < len(self._bands) else "in_range",
|
||||
self._in_range_color,
|
||||
)
|
||||
# Filled circle with thin dark outline for contrast over heatmap
|
||||
painter.setPen(QPen(QColor(0, 0, 0, 100), 1))
|
||||
painter.setBrush(QBrush(color))
|
||||
painter.drawEllipse(px - r, py - r, 2 * r, 2 * r)
|
||||
|
||||
if self._show_labels:
|
||||
has_temp = i < len(self._values)
|
||||
|
||||
# Fetch text alignment side and offsets
|
||||
side = getattr(s, "side", "right").lower()
|
||||
ox = getattr(s, "offset_x", 0.0) * r
|
||||
oy = getattr(s, "offset_y", 0.0) * r
|
||||
|
||||
# Pre-compute metrics using current scaled fonts
|
||||
painter.setFont(id_font)
|
||||
id_fm = painter.fontMetrics()
|
||||
id_line_h = id_fm.height()
|
||||
id_ascent = id_fm.ascent()
|
||||
|
||||
painter.setFont(temp_font)
|
||||
temp_fm = painter.fontMetrics()
|
||||
temp_line_h = temp_fm.height()
|
||||
temp_ascent = temp_fm.ascent()
|
||||
|
||||
id_text = s.label
|
||||
temp_text = f"{self._values[i]:.2f}" if has_temp else ""
|
||||
|
||||
id_w = id_fm.horizontalAdvance(id_text)
|
||||
temp_w = temp_fm.horizontalAdvance(temp_text) if has_temp else 0
|
||||
text_w = max(id_w, temp_w)
|
||||
|
||||
# Height of the 1 or 2-line text block
|
||||
text_h = (id_line_h + temp_line_h) if has_temp else id_line_h
|
||||
|
||||
# Calculate box top-left (lx, ly) relative to dot center (px, py)
|
||||
gap = 3
|
||||
if side == "left":
|
||||
lx = px - r - gap - text_w - ox
|
||||
ly = py - text_h // 2 + oy
|
||||
elif side == "top":
|
||||
lx = px - text_w // 2 + ox
|
||||
ly = py - r - gap - text_h - oy
|
||||
elif side == "bottom":
|
||||
lx = px - text_w // 2 + ox
|
||||
ly = py + r + gap + oy
|
||||
else: # "right" or default
|
||||
lx = px + r + gap + ox
|
||||
ly = py - text_h // 2 + oy
|
||||
|
||||
# Draw Sensor ID (first line)
|
||||
painter.setFont(id_font)
|
||||
painter.setPen(QPen(self._text_color))
|
||||
y1 = ly + id_ascent
|
||||
if side in ("top", "bottom"):
|
||||
painter.drawText(lx + (text_w - id_w) // 2, y1, id_text)
|
||||
elif side == "left":
|
||||
painter.drawText(lx + (text_w - id_w), y1, id_text)
|
||||
else:
|
||||
painter.drawText(lx, y1, id_text)
|
||||
|
||||
# Draw Temperature (second line)
|
||||
if has_temp:
|
||||
painter.setFont(temp_font)
|
||||
painter.setPen(QPen(color))
|
||||
y2 = ly + id_line_h + temp_ascent
|
||||
if side in ("top", "bottom"):
|
||||
painter.drawText(lx + (text_w - temp_w) // 2, y2, temp_text)
|
||||
elif side == "left":
|
||||
painter.drawText(lx + (text_w - temp_w), y2, temp_text)
|
||||
else:
|
||||
painter.drawText(lx, y2, temp_text)
|
||||
@@ -0,0 +1,10 @@
|
||||
# ===== Wafer Sub-package =====
|
||||
from pygui.backend.wafer.zwafer_models import ZWaferData, Sensor, DataRecord
|
||||
from pygui.backend.wafer.zwafer_parser import ZWaferParser
|
||||
from pygui.backend.wafer.wafer_layouts import load_layout, available_families
|
||||
|
||||
__all__ = [
|
||||
"ZWaferData", "Sensor", "DataRecord",
|
||||
"ZWaferParser",
|
||||
"load_layout", "available_families",
|
||||
]
|
||||
@@ -0,0 +1,163 @@
|
||||
"""Load wafer sensor layouts from bundled YAML files.
|
||||
|
||||
YAML schema mirrors the replay app's wafer_desc.py format:
|
||||
X/Y — sensor positions in mm relative to x_origin/y_origin
|
||||
size — wafer diameter/edge in mm
|
||||
shape — "round" or "square"
|
||||
start_sn — first sensor number (usually 1)
|
||||
x_origin — "left" | "right" | "center"
|
||||
y_origin — "bottom" | "top" | "center"
|
||||
|
||||
Returned Sensor coords are center-origin mm (negative values toward the edge).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from pygui.backend.wafer.zwafer_models import Sensor
|
||||
|
||||
_LAYOUTS_DIR = Path(__file__).parent.parent.parent / "assets" / "layouts"
|
||||
|
||||
|
||||
class WaferLayout(list):
|
||||
"""Subclass of list that carries wafer shape and size metadata."""
|
||||
def __init__(self, sensors: list[Sensor], shape: str = "round", size: float = 300.0):
|
||||
super().__init__(sensors)
|
||||
self.shape = shape
|
||||
self.size = size
|
||||
|
||||
|
||||
def resolve_shape_and_size(sensors: list[Sensor], wafer_id: str = "") -> tuple[str, float]:
|
||||
"""Helper to determine the shape and size of a sensor layout.
|
||||
|
||||
Tries matching by wafer_id prefix, falling back to the number of sensors.
|
||||
"""
|
||||
if wafer_id:
|
||||
prefix = wafer_id[0].upper()
|
||||
for path in _LAYOUTS_DIR.glob("*.yaml"):
|
||||
try:
|
||||
data = _load_yaml(path)
|
||||
if prefix in data.get("wafers", []):
|
||||
return data.get("shape", "round"), float(data.get("size", 300.0))
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# Fallback to sensor count mapping
|
||||
n = len(sensors)
|
||||
if n == 80:
|
||||
return "square", 310.0
|
||||
elif n == 65:
|
||||
return "round", 300.0
|
||||
elif n == 48:
|
||||
return "round", 300.0
|
||||
elif n == 29:
|
||||
return "round", 200.0
|
||||
elif n == 22:
|
||||
return "round", 200.0
|
||||
|
||||
return "round", 300.0
|
||||
|
||||
|
||||
def _family_name(raw_name: str) -> str:
|
||||
return raw_name.replace("wafer", "").strip("_")
|
||||
|
||||
|
||||
def _load_yaml(path: Path) -> dict:
|
||||
with path.open(encoding="utf-8") as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
|
||||
def available_families() -> list[str]:
|
||||
return [_family_name(_load_yaml(p)["name"]) for p in _LAYOUTS_DIR.glob("*.yaml")]
|
||||
|
||||
|
||||
def load_layout(family: str) -> WaferLayout:
|
||||
for path in _LAYOUTS_DIR.glob("*.yaml"):
|
||||
data = _load_yaml(path)
|
||||
if _family_name(data["name"]) == family:
|
||||
return _to_sensors(data)
|
||||
raise KeyError(f"Unknown wafer family: {family!r}")
|
||||
|
||||
|
||||
def load_layout_for_wafer_id(wafer_id: str) -> WaferLayout:
|
||||
"""Match 'B00108' → bcdwafer by looking up the first char in each YAML's 'wafers' list."""
|
||||
prefix = wafer_id[0].upper() if wafer_id else ""
|
||||
for path in _LAYOUTS_DIR.glob("*.yaml"):
|
||||
data = _load_yaml(path)
|
||||
if prefix in data.get("wafers", []):
|
||||
return _to_sensors(data)
|
||||
raise KeyError(f"No layout found for wafer ID prefix {prefix!r}")
|
||||
|
||||
|
||||
def _to_sensors(data: dict) -> WaferLayout:
|
||||
xs: list[float] = data["X"]
|
||||
ys: list[float] = data["Y"]
|
||||
size: float = float(data["size"])
|
||||
shape: str = data.get("shape", "round")
|
||||
start_sn: int = data.get("start_sn", 1)
|
||||
reverse_x: bool = data.get("reverse_x", False)
|
||||
reverse_y: bool = data.get("reverse_y", False)
|
||||
x_orig: str = data.get("x_origin", "left")
|
||||
y_orig: str = data.get("y_origin", "bottom")
|
||||
label_exceptions = data.get("label_exceptions", {}) or {}
|
||||
|
||||
x_shift = {"left": size / 2, "right": -(size / 2), "center": 0.0}.get(x_orig, 0.0)
|
||||
y_shift = {"bottom": size / 2, "top": -(size / 2), "center": 0.0}.get(y_orig, 0.0)
|
||||
|
||||
sensors: list[Sensor] = []
|
||||
for i, (x_mm, y_mm) in enumerate(zip(xs, ys)):
|
||||
if reverse_x:
|
||||
x_mm = -x_mm
|
||||
if reverse_y:
|
||||
y_mm = -y_mm
|
||||
|
||||
x_coord = x_mm - x_shift
|
||||
y_coord = y_mm - y_shift
|
||||
|
||||
# Determine label position defaults based on quadrants
|
||||
sn = start_sn + i
|
||||
side = "right"
|
||||
offset_x = 0.0
|
||||
offset_y = 0.0
|
||||
|
||||
# Check explicit label exception from layout config
|
||||
if sn in label_exceptions:
|
||||
exc = label_exceptions[sn]
|
||||
if exc:
|
||||
side_key = list(exc.keys())[0]
|
||||
val = exc[side_key]
|
||||
side = side_key
|
||||
if isinstance(val, list) and len(val) >= 2:
|
||||
offset_x = float(val[0])
|
||||
offset_y = float(val[1])
|
||||
else:
|
||||
# Smart default quadrant positioning to keep labels pointing inward
|
||||
if x_coord > 30.0 and y_coord > 30.0:
|
||||
side = "left"
|
||||
offset_y = -0.5
|
||||
elif x_coord < -30.0 and y_coord > 30.0:
|
||||
side = "right"
|
||||
offset_y = -0.5
|
||||
elif x_coord < -30.0 and y_coord < -30.0:
|
||||
side = "right"
|
||||
offset_y = 0.5
|
||||
elif x_coord > 30.0 and y_coord < -30.0:
|
||||
side = "left"
|
||||
offset_y = 0.5
|
||||
elif x_coord > 15.0:
|
||||
side = "left"
|
||||
elif x_coord < -15.0:
|
||||
side = "right"
|
||||
|
||||
sensors.append(Sensor(
|
||||
label=str(sn),
|
||||
x=x_coord,
|
||||
y=y_coord,
|
||||
side=side,
|
||||
offset_x=offset_x,
|
||||
offset_y=offset_y,
|
||||
))
|
||||
return WaferLayout(sensors, shape=shape, size=size)
|
||||
|
||||
@@ -9,6 +9,9 @@ class Sensor:
|
||||
label: str
|
||||
x: float
|
||||
y: float
|
||||
side: str = "right"
|
||||
offset_x: float = 0.0
|
||||
offset_y: float = 0.0
|
||||
|
||||
|
||||
# ===== Data Row =====
|
||||
@@ -2,7 +2,7 @@ from pathlib import Path
|
||||
from typing import Tuple, Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from backend.zwafer_models import ZWaferData, Sensor
|
||||
from pygui.backend.wafer.zwafer_models import ZWaferData, Sensor
|
||||
|
||||
|
||||
# ===== Z-Wafer CSV Parser =====
|
||||
@@ -32,7 +32,14 @@ class ZWaferParser:
|
||||
|
||||
# ===== Header Parsing =====
|
||||
def _process_header(self, file_obj) -> ZWaferData:
|
||||
"""Parse header and sensor layout from open file object."""
|
||||
"""Parse header and sensor layout from open file object.
|
||||
|
||||
Handles two CSV row orderings:
|
||||
1. Label/X/Y rows BEFORE 'data' (fixture test format)
|
||||
2. 'data' row BEFORE Label/X/Y (some real CSV files)
|
||||
The C# parser handles both by collecting sensor layout rows
|
||||
regardless of whether 'data' has been seen.
|
||||
"""
|
||||
wafer_data = ZWaferData()
|
||||
labels: Optional[list] = None
|
||||
x_coords: Optional[list] = None
|
||||
@@ -47,26 +54,25 @@ class ZWaferParser:
|
||||
parts = [p.strip() for p in line.split(",")]
|
||||
first_part = parts[0].lower()
|
||||
|
||||
# Collect sensor layout rows (Label, X (mm), Y (mm))
|
||||
# regardless of whether 'data' has been seen.
|
||||
# This matches the C# parser behavior.
|
||||
if first_part == "label":
|
||||
labels = parts[1:]
|
||||
elif first_part == "x (mm)":
|
||||
x_coords = parts[1:]
|
||||
elif first_part == "y (mm)":
|
||||
y_coords = parts[1:]
|
||||
# Parse metadata (key=value pairs) — only when not a sensor layout row
|
||||
elif first_part != "data":
|
||||
self._parse_header_line(wafer_data, parts)
|
||||
|
||||
# Detect end of header section
|
||||
if first_part == "data":
|
||||
wafer_data.csv_headers = labels or []
|
||||
self._build_sensor_layout(wafer_data, labels, x_coords, y_coords)
|
||||
return wafer_data
|
||||
|
||||
# Detect switch from metadata to sensor layout
|
||||
# Parse metadata (key=value pairs)
|
||||
if not self._parse_header_line(wafer_data, parts):
|
||||
# If not metadata, it's part of sensor layout
|
||||
label = parts[0].lower()
|
||||
values = parts[1:]
|
||||
|
||||
if label == "label":
|
||||
labels = values
|
||||
elif label == "x (mm)":
|
||||
x_coords = values
|
||||
elif label == "y (mm)":
|
||||
y_coords = values
|
||||
|
||||
return wafer_data # Incomplete file — return partial data
|
||||
|
||||
# ===== Metadata Parsing =====
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Serial port communication layer for the temperature-sensing wafer."""
|
||||
|
||||
from serialcomm.device_service import DeviceService
|
||||
from serialcomm.serial_port import SerialPort, WaferInfo
|
||||
from pygui.serialcomm.device_service import DeviceService
|
||||
from pygui.serialcomm.serial_port import SerialPort, WaferInfo
|
||||
|
||||
__all__ = ["DeviceService", "SerialPort", "WaferInfo"]
|
||||
@@ -11,8 +11,8 @@ from typing import Optional
|
||||
|
||||
import serial.tools.list_ports
|
||||
|
||||
from backend.local_settings import LocalSettings
|
||||
from serialcomm.serial_port import SerialPort, WaferInfo
|
||||
from pygui.backend.data.local_settings import LocalSettings
|
||||
from pygui.serialcomm.serial_port import SerialPort, WaferInfo
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -37,7 +37,58 @@ class DeviceService:
|
||||
|
||||
def enumerate_ports(self) -> list[str]:
|
||||
"""Return list of available serial port names."""
|
||||
return [p.device for p in serial.tools.list_ports.comports()]
|
||||
hardware_ports = [p.device for p in serial.tools.list_ports.comports()]
|
||||
virtual_ports = []
|
||||
|
||||
# On macOS/Linux, also detect virtual loopback serial ports (PTYs) opened by socat
|
||||
import platform
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
if platform.system() in ("Darwin", "Linux"):
|
||||
try:
|
||||
# Find PTYs opened by socat
|
||||
result = subprocess.run(
|
||||
["lsof", "-c", "socat"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False
|
||||
)
|
||||
if result.returncode == 0:
|
||||
for line in result.stdout.splitlines():
|
||||
parts = line.strip().split()
|
||||
if len(parts) >= 9:
|
||||
fd = parts[3]
|
||||
# Exclude standard FDs (0, 1, 2) which are typically the controlling terminal
|
||||
if fd.startswith(('0', '1', '2')):
|
||||
continue
|
||||
name = parts[-1]
|
||||
match = re.search(r'(/dev/(?:ttys\d+|pts/\d+))\b', name)
|
||||
if match:
|
||||
port = match.group(1)
|
||||
if port not in virtual_ports:
|
||||
virtual_ports.append(port)
|
||||
except Exception as e:
|
||||
log.debug("Error listing virtual ports via lsof: %s", e)
|
||||
|
||||
# Prioritize physical USB serial interfaces first -> generic hardware -> virtual ports
|
||||
# Virtual ports (PTYs detected via lsof) should NOT appear in hardware_ports,
|
||||
# so we filter them out explicitly.
|
||||
virtual_set = set(virtual_ports)
|
||||
ordered_ports = []
|
||||
for p in hardware_ports:
|
||||
if "usbserial" in p.lower() or "usb" in p.lower() or "ttyusb" in p.lower():
|
||||
if p not in ordered_ports:
|
||||
ordered_ports.append(p)
|
||||
for p in hardware_ports:
|
||||
if p not in ordered_ports and p not in virtual_set:
|
||||
ordered_ports.append(p)
|
||||
for p in virtual_ports:
|
||||
if p not in ordered_ports:
|
||||
ordered_ports.append(p)
|
||||
|
||||
return ordered_ports
|
||||
|
||||
|
||||
def detect_wafer(self, port: str) -> Optional[WaferInfo]:
|
||||
"""Try to detect a wafer on the given port.
|
||||
@@ -46,15 +46,16 @@ class SerialPort:
|
||||
self._port_name = port_name
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Context manager for scoped port access
|
||||
# Port opening (shared between scoped and long-lived use)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@contextmanager
|
||||
def _open(self, timeout: float | None = None) -> Iterator[pyserial.Serial]:
|
||||
"""Open the serial port, yield it, then close on exit."""
|
||||
port = pyserial.Serial(
|
||||
self._port_name,
|
||||
self.BAUDRATE,
|
||||
@staticmethod
|
||||
def open_port(port_name: str, timeout: float | None = None) -> pyserial.Serial:
|
||||
"""Open a serial port with baud-rate fallback.
|
||||
|
||||
Tries SerialPort.BAUDRATE first, falls back to 115200 on failure.
|
||||
The caller owns the lifecycle of the returned Serial object."""
|
||||
kwargs = dict(
|
||||
parity=pyserial.PARITY_NONE,
|
||||
bytesize=8,
|
||||
stopbits=pyserial.STOPBITS_ONE,
|
||||
@@ -63,7 +64,22 @@ class SerialPort:
|
||||
dsrdtr=False,
|
||||
)
|
||||
if timeout is not None:
|
||||
port.timeout = timeout
|
||||
kwargs["timeout"] = timeout
|
||||
|
||||
try:
|
||||
return pyserial.Serial(port_name, SerialPort.BAUDRATE, **kwargs)
|
||||
except Exception as exc:
|
||||
log.warning("Baud rate %d failed on %s, falling back to 115200: %s", SerialPort.BAUDRATE, port_name, exc)
|
||||
return pyserial.Serial(port_name, 115200, **kwargs)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Context manager for scoped port access
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@contextmanager
|
||||
def _open(self, timeout: float | None = None) -> Iterator[pyserial.Serial]:
|
||||
"""Open the serial port, yield it, then close on exit."""
|
||||
port = self.open_port(self._port_name, timeout=timeout)
|
||||
try:
|
||||
yield port
|
||||
finally:
|
||||
@@ -0,0 +1,259 @@
|
||||
"""Continuous serial frame reader (Live-mode source)"""
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from typing import Callable
|
||||
|
||||
from pygui.backend.models.frame import Frame
|
||||
from pygui.serialcomm.data_parser import _convert_hex_to_temp
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
ParseFrame = Callable[[bytes, int], Frame]
|
||||
|
||||
class StreamReader:
|
||||
|
||||
def __init__(self, transport, parse_frame: ParseFrame,
|
||||
on_frame: Callable[[Frame], None],
|
||||
on_error: Callable[[Exception], None] | None = None,
|
||||
family_code: str = "A") -> None:
|
||||
self._transport = transport
|
||||
self._parse = parse_frame
|
||||
self._on_frame = on_frame
|
||||
self._on_error = on_error or (lambda e: None)
|
||||
self._family_code = family_code or "A"
|
||||
self._thread: threading.Thread | None = None
|
||||
self._stop = threading.Event()
|
||||
self.error_count = 0
|
||||
|
||||
def start(self) -> None:
|
||||
self._stop.clear()
|
||||
self._thread = threading.Thread(target=self._run, daemon = True)
|
||||
self._thread.start()
|
||||
|
||||
def _read_chunk(self, min_bytes: int = 1) -> bytes:
|
||||
"""Read a chunk of available bytes from the transport.
|
||||
|
||||
Reads up to 256 bytes in one shot instead of byte-at-a-time,
|
||||
falling back to single-byte reads with a yield when nothing is
|
||||
immediately available."""
|
||||
chunk = self._transport.read(max(min_bytes, 256))
|
||||
if chunk:
|
||||
return chunk
|
||||
# Nothing available right now – yield briefly to avoid busy-spin.
|
||||
b = self._transport.read(1)
|
||||
if not b:
|
||||
time.sleep(0.005)
|
||||
return b
|
||||
|
||||
def _accumulate(self, needed: int) -> bytearray:
|
||||
"""Accumulate *needed* bytes using buffered reads."""
|
||||
buf = bytearray()
|
||||
while len(buf) < needed and not self._stop.is_set():
|
||||
chunk = self._transport.read(needed - len(buf))
|
||||
if not chunk:
|
||||
time.sleep(0.005)
|
||||
continue
|
||||
buf.extend(chunk)
|
||||
return buf
|
||||
|
||||
def _run(self) -> None:
|
||||
try:
|
||||
# Peek at the first few bytes to determine protocol.
|
||||
peek_buf = bytearray()
|
||||
while not self._stop.is_set() and len(peek_buf) < 32:
|
||||
b = self._transport.read(1)
|
||||
if not b:
|
||||
continue
|
||||
peek_buf.extend(b)
|
||||
if b[0] == 10: # newline
|
||||
break
|
||||
|
||||
if self._stop.is_set() or not peek_buf:
|
||||
return
|
||||
|
||||
raw = bytes(peek_buf)
|
||||
|
||||
# Binary sync bytes are the strongest signal.
|
||||
if raw.startswith(b"\xaa\x88"):
|
||||
self._run_binary(raw)
|
||||
return
|
||||
|
||||
# Text-lines (CSV) requires BOTH a newline AND commas in the
|
||||
# pre-newline content. This avoids misclassifying a binary
|
||||
# payload that happens to contain 0x2C as text.
|
||||
has_newline = b"\n" in raw or b"\r" in raw
|
||||
line_before_nl = raw.split(b"\n")[0].split(b"\r")[0]
|
||||
if has_newline and b"," in line_before_nl:
|
||||
self._run_text_lines(raw)
|
||||
return
|
||||
|
||||
# Default: ASCII hex dump stream.
|
||||
self._run_ascii(raw)
|
||||
finally:
|
||||
try:
|
||||
self._transport.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _run_binary(self, initial_bytes: bytes) -> None:
|
||||
log.info("StreamReader: starting binary stream parsing")
|
||||
buf = bytearray(initial_bytes)
|
||||
|
||||
while not self._stop.is_set():
|
||||
# Find sync marker in buffer.
|
||||
idx = 0
|
||||
synced = False
|
||||
while idx < len(buf) - 1:
|
||||
if buf[idx] == 0xAA and buf[idx + 1] == 0x88:
|
||||
synced = True
|
||||
break
|
||||
idx += 1
|
||||
|
||||
if synced:
|
||||
buf = buf[idx:]
|
||||
# Accumulate header (5 bytes after sync).
|
||||
while len(buf) < 7 and not self._stop.is_set():
|
||||
chunk = self._read_chunk()
|
||||
buf.extend(chunk)
|
||||
|
||||
if self._stop.is_set():
|
||||
return
|
||||
|
||||
payload_len = int.from_bytes(buf[5:7], byteorder='little')
|
||||
total_packet_len = 2 + 5 + payload_len + 2
|
||||
|
||||
# Accumulate the rest of the packet in one buffered read.
|
||||
remaining = total_packet_len - len(buf)
|
||||
if remaining > 0:
|
||||
extra = self._accumulate(remaining)
|
||||
buf.extend(extra)
|
||||
|
||||
if self._stop.is_set():
|
||||
return
|
||||
|
||||
header = buf[2:7]
|
||||
seq = int.from_bytes(header[1:3], byteorder='little')
|
||||
payload = buf[7:7 + payload_len]
|
||||
|
||||
try:
|
||||
frame = self._parse(payload, seq)
|
||||
self._on_frame(frame)
|
||||
except Exception as exc:
|
||||
self.error_count += 1
|
||||
if self.error_count <= 10:
|
||||
log.warning("Parse error on binary payload: %s", exc)
|
||||
self._on_error(exc)
|
||||
|
||||
buf = buf[total_packet_len:]
|
||||
else:
|
||||
# Resync: keep only trailing 0xAA if present, then read more.
|
||||
if len(buf) > 0 and buf[-1] == 0xAA:
|
||||
buf = bytearray([0xAA])
|
||||
else:
|
||||
buf.clear()
|
||||
|
||||
chunk = self._read_chunk()
|
||||
buf.extend(chunk)
|
||||
|
||||
def _run_ascii(self, initial_bytes: bytes) -> None:
|
||||
log.info("StreamReader: starting ASCII hex dump stream parsing")
|
||||
|
||||
valid_words = 80 if self._family_code == "X" else 244
|
||||
block_words = 256
|
||||
word_char_len = 4
|
||||
chars_needed = block_words * word_char_len # total hex chars per block
|
||||
|
||||
hex_chars = bytearray()
|
||||
if initial_bytes:
|
||||
for b in initial_bytes:
|
||||
if chr(b) in "0123456789ABCDEFabcdef":
|
||||
hex_chars.append(b)
|
||||
elif b == 0xAA:
|
||||
hex_chars.append(b)
|
||||
|
||||
seq = 0
|
||||
while not self._stop.is_set():
|
||||
# Accumulate a full block of hex characters using buffered reads.
|
||||
while len(hex_chars) < chars_needed and not self._stop.is_set():
|
||||
chunk = self._read_chunk(min_bytes=chars_needed - len(hex_chars))
|
||||
for byte in chunk:
|
||||
if byte == 0xAA:
|
||||
# Sync marker appeared – hand off to binary parser.
|
||||
hex_chars.append(byte)
|
||||
break
|
||||
c = chr(byte)
|
||||
if c in "0123456789ABCDEFabcdef":
|
||||
hex_chars.append(c)
|
||||
else:
|
||||
continue
|
||||
break # broke out of inner loop due to 0xAA
|
||||
|
||||
# Check for early termination (0xAA sync marker anywhere in buffer).
|
||||
aa_idx = hex_chars.find(bytes([0xAA]))
|
||||
if aa_idx >= 0:
|
||||
log.info("StreamReader: Detected 0xAA at position %d, switching to binary mode.", aa_idx)
|
||||
self._run_binary(hex_chars[aa_idx:])
|
||||
return
|
||||
|
||||
# Parse words from the accumulated hex characters.
|
||||
word_buffer = []
|
||||
for i in range(0, min(len(hex_chars), chars_needed), word_char_len):
|
||||
word_str = hex_chars[i:i + word_char_len]
|
||||
if len(word_str) < word_char_len:
|
||||
break
|
||||
word_buffer.append(bytes(word_str))
|
||||
|
||||
if not word_buffer:
|
||||
time.sleep(0.01)
|
||||
continue
|
||||
|
||||
hex_chars = hex_chars[len(word_buffer) * word_char_len:]
|
||||
|
||||
valid_hex_words = word_buffer[:valid_words]
|
||||
values = []
|
||||
for hex_val in valid_hex_words:
|
||||
try:
|
||||
swapped = hex_val[2:4] + hex_val[0:2]
|
||||
t = _convert_hex_to_temp(swapped, self._family_code)
|
||||
values.append(t)
|
||||
except Exception:
|
||||
values.append(0.0)
|
||||
|
||||
try:
|
||||
frame = Frame(seq=seq, time=time.monotonic(), values=values)
|
||||
self._on_frame(frame)
|
||||
seq += 1
|
||||
except Exception as exc:
|
||||
log.error("Error emitting ASCII frame: %s", exc)
|
||||
|
||||
def _run_text_lines(self, initial_bytes: bytes) -> None:
|
||||
log.info("StreamReader: starting line-by-line ASCII text parsing")
|
||||
buf = bytearray(initial_bytes)
|
||||
seq = 0
|
||||
|
||||
while not self._stop.is_set():
|
||||
# Drain complete lines from the buffer.
|
||||
while b"\n" in buf:
|
||||
line_bytes, _, buf = buf.partition(b"\n")
|
||||
line = line_bytes.decode('utf-8', errors='ignore').strip()
|
||||
if line:
|
||||
try:
|
||||
frame = self._parse(line, seq)
|
||||
if frame:
|
||||
self._on_frame(frame)
|
||||
seq += 1
|
||||
except Exception as exc:
|
||||
self.error_count += 1
|
||||
self._on_error(exc)
|
||||
|
||||
# Read more data using buffered reads.
|
||||
chunk = self._read_chunk()
|
||||
buf.extend(chunk)
|
||||
|
||||
def stop(self) -> None:
|
||||
self._stop.set()
|
||||
if self._thread is not None and self._thread.is_alive():
|
||||
self._thread.join(timeout = 2.0)
|
||||
self._thread = None
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
Wafer ID=A12
|
||||
Acquisition Date=03/26/2025
|
||||
Label,1,2,3
|
||||
X (mm),0,10,-10
|
||||
Y (mm),10,-10,-10
|
||||
data
|
||||
0.0,149.0,148.5,150.6
|
||||
0.5,149.2,148.4,150.7
|
||||
1.0,149.1,148.6,150.5
|
||||
|
@@ -0,0 +1,52 @@
|
||||
"""Tests for src/pygui/backend/cluster_average.py."""
|
||||
import math
|
||||
import pytest
|
||||
from pygui.backend.cluster_average import average_clusters, group_sensors_by_radius
|
||||
from pygui.backend.wafer.zwafer_models import Sensor
|
||||
|
||||
|
||||
def test_members_take_cluster_mean():
|
||||
vals = [10.0, 20.0, 100.0, 0.0]
|
||||
clusters = [[0, 1], [2, 3]]
|
||||
assert average_clusters(vals, clusters) == [15.0, 15.0, 50.0, 50.0]
|
||||
|
||||
|
||||
def test_unclustered_sensors_unchanged():
|
||||
vals = [10.0, 20.0, 30.0]
|
||||
assert average_clusters(vals, [[0, 1]]) == [15.0, 15.0, 30.0]
|
||||
|
||||
|
||||
def test_empty_clusters_is_identity():
|
||||
assert average_clusters([1.0, 2.0], []) == [1.0, 2.0]
|
||||
|
||||
|
||||
def test_nan_handling_in_averaging():
|
||||
vals = [10.0, float('nan'), 100.0, float('nan')]
|
||||
clusters = [[0, 1], [2, 3]]
|
||||
res = average_clusters(vals, clusters)
|
||||
assert res[0] == 10.0
|
||||
assert res[1] == 10.0
|
||||
assert res[2] == 100.0
|
||||
assert res[3] == 100.0
|
||||
|
||||
|
||||
def test_all_nan_cluster():
|
||||
vals = [float('nan'), float('nan'), 5.0]
|
||||
clusters = [[0, 1]]
|
||||
res = average_clusters(vals, clusters)
|
||||
assert math.isnan(res[0])
|
||||
assert math.isnan(res[1])
|
||||
assert res[2] == 5.0
|
||||
|
||||
|
||||
def test_group_by_radius():
|
||||
sensors = [
|
||||
Sensor(label="1", x=0.0, y=0.0), # center (r=0)
|
||||
Sensor(label="2", x=10.0, y=0.0), # r=10
|
||||
Sensor(label="3", x=0.0, y=10.5), # r=10.5 (within 2mm)
|
||||
Sensor(label="4", x=20.0, y=0.0), # r=20
|
||||
]
|
||||
# Center should be excluded or not grouped; other sensors grouped
|
||||
clusters = group_sensors_by_radius(sensors, tolerance=2.0)
|
||||
# Expected: 2 and 3 are in the same cluster because 10.5 - 10.0 <= 2.0.
|
||||
assert [1, 2] in clusters
|
||||
@@ -0,0 +1,40 @@
|
||||
from pygui.backend.data.csv_recorder import CsvRecorder
|
||||
from pygui.backend.data.data_records import read_data_records
|
||||
from pygui.backend.models.frame import Frame
|
||||
from pygui.backend.wafer.zwafer_models import Sensor
|
||||
from pygui.backend.wafer.zwafer_parser import ZWaferParser
|
||||
|
||||
|
||||
def test_recording_roundtrips_through_parser(tmp_path):
|
||||
sensors = [Sensor("1", 0.0, 1.0), Sensor("2", 1.0, 0.0)]
|
||||
path = tmp_path / "rec.csv"
|
||||
|
||||
rec = CsvRecorder()
|
||||
rec.start(str(path), sensors, serial="A12")
|
||||
rec.write(Frame(seq=0, time=0.0, values=[149.0, 148.0]))
|
||||
rec.write(Frame(seq=1, time=0.5, values=[149.5, 148.5]))
|
||||
rec.stop()
|
||||
|
||||
assert path.exists()
|
||||
data, _ = ZWaferParser().parse(str(path))
|
||||
assert data is not None
|
||||
assert[s.label for s in data.sensors] == ["1", "2"]
|
||||
assert len(data.csv_headers) == 2
|
||||
|
||||
|
||||
def test_recorded_data_rows_are_readable(tmp_path):
|
||||
sensors = [Sensor("1", 0.0, 1.0), Sensor("2", 1.0, 0.0)]
|
||||
path = tmp_path / "rec.csv"
|
||||
|
||||
rec = CsvRecorder()
|
||||
rec.start(str(path), sensors, serial="A12")
|
||||
rec.write(Frame(seq=0, time=0.0, values=[149.0, 148.0]))
|
||||
rec.write(Frame(seq=1, time=0.5, values=[149.5, 148.5]))
|
||||
rec.stop()
|
||||
|
||||
records = read_data_records(str(path))
|
||||
assert len(records) == 2
|
||||
assert records[0].time == 0.0
|
||||
assert records[0].values == [149.0, 148.0]
|
||||
assert records[1].time == 0.5
|
||||
assert records[1].values == [149.5, 148.5]
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Tests for serialcomm/data_parser.py binary parsing pipeline."""
|
||||
|
||||
import pytest
|
||||
from serialcomm.data_parser import (
|
||||
from pygui.serialcomm.data_parser import (
|
||||
csv_column_count,
|
||||
parse_binary_data,
|
||||
convert_to_temperatures,
|
||||
@@ -12,7 +12,6 @@ from serialcomm.data_parser import (
|
||||
MAXDUT_X,
|
||||
)
|
||||
|
||||
|
||||
# ── csv_column_count ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -203,6 +202,7 @@ class TestConvertToTemperatures:
|
||||
def test_p_family_single_block(self):
|
||||
data = _make_p_block(1, value=0x0100)
|
||||
hex_data = parse_binary_data(data, "P")
|
||||
assert hex_data is not None
|
||||
result = convert_to_temperatures(hex_data, "P")
|
||||
assert len(result) == 1
|
||||
assert all(isinstance(v, str) for v in result[0])
|
||||
@@ -270,9 +270,9 @@ class TestSaveToCsv:
|
||||
result = save_to_csv(data, family, f"{family}00001", str(tmp_path))
|
||||
assert result is not None, f"save_to_csv returned None for {family}"
|
||||
headers = open(result).readline().strip().split(",")
|
||||
assert len(headers) == expected_cols, (
|
||||
f"{family}: expected {expected_cols} headers, got {len(headers)}"
|
||||
)
|
||||
assert (
|
||||
len(headers) == expected_cols
|
||||
), f"{family}: expected {expected_cols} headers, got {len(headers)}"
|
||||
assert headers[0] == "Sensor1"
|
||||
assert headers[-1] == f"Sensor{expected_cols}"
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Tests for src/pygui/backend/data_records.py."""
|
||||
from pygui.backend.data.data_records import read_data_records
|
||||
|
||||
|
||||
def test_reads_rows_after_data_marker(tmp_path):
|
||||
p = tmp_path / "s.csv"
|
||||
p.write_text(
|
||||
"Wafer ID=A12\n"
|
||||
"Label,1,2\nX (mm),0,10\nY (mm),10,-10\n"
|
||||
"data\n"
|
||||
"0.0,149.0,148.5\n"
|
||||
"0.5,149.2,148.4\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
recs = read_data_records(str(p))
|
||||
assert len(recs) == 2
|
||||
assert recs[0].time == 0.0
|
||||
assert recs[0].values == [149.0, 148.5]
|
||||
assert recs[1].values == [149.2, 148.4]
|
||||
@@ -0,0 +1,30 @@
|
||||
from pygui.backend.models.frame import Frame
|
||||
from pygui.backend.models.frame_player import FramePlayer
|
||||
|
||||
def frames(n):
|
||||
return [Frame(seq=i, time=float(i), values=[149.0 + i])for i in range(n)]
|
||||
|
||||
def test_loads_and_reports_total():
|
||||
p = FramePlayer(); p.load(frames(3))
|
||||
assert p.total == 3 and p.index == 0
|
||||
current = p.current()
|
||||
assert current is not None
|
||||
assert current.values == [149.0]
|
||||
|
||||
def test_step_forward_and_back_clamps():
|
||||
p = FramePlayer(); p.load(frames(3))
|
||||
assert p.step(1).index == 1
|
||||
assert p.step(1).index == 2
|
||||
assert p.step(1).index == 2
|
||||
assert p.step(-5).index == 0
|
||||
|
||||
def test_seek():
|
||||
p = FramePlayer(); p.load(frames(5))
|
||||
assert p.seek(3).index == 3
|
||||
assert p.seek(99).index == 4
|
||||
|
||||
def test_at_end():
|
||||
p = FramePlayer(); p.load(frames(2))
|
||||
assert not p.at_end
|
||||
p.seek(1)
|
||||
assert p.at_end
|
||||
@@ -0,0 +1,22 @@
|
||||
import math
|
||||
import pytest
|
||||
from pygui.backend.models.frame_stats import compute_stats, Stats
|
||||
|
||||
|
||||
def test_basic_stat():
|
||||
s = compute_stats([148.0, 150.0, 149.0])
|
||||
assert s.min == 148.0 and s.min_index == 0
|
||||
assert s.max == 150.0 and s.max_index == 1
|
||||
assert s.diff == pytest.approx(2.0)
|
||||
assert s.avg == pytest.approx(149.0)
|
||||
assert s.sigma == pytest.approx(math.sqrt(2 / 3))
|
||||
assert s.three_sigma == pytest.approx(3 * math.sqrt(2 / 3))
|
||||
|
||||
|
||||
def test_empty_values_returns_zeros():
|
||||
s = compute_stats([])
|
||||
assert s == Stats(0.0, -1, 0.0, -1, 0.0, 0.0, 0.0, 0.0)
|
||||
|
||||
|
||||
def test_ignores_nan():
|
||||
s = compute_stats([149.0, float("nan"), 151.0])
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Tests for src/pygui/backend/rbf_heatmap.py."""
|
||||
import numpy as np
|
||||
from pygui.backend.visualization.rbf_heatmap import interpolate_field, BACKEND
|
||||
|
||||
|
||||
def test_field_shape_and_interpolates_at_sites():
|
||||
xs = np.array([0.0, 10.0, -10.0, 0.0])
|
||||
ys = np.array([10.0, -10.0, -10.0, 0.0])
|
||||
vs = np.array([149.0, 150.0, 148.0, 149.5])
|
||||
field = interpolate_field(xs, ys, vs, width=64, height=64,
|
||||
extent=(-15, 15, -15, 15))
|
||||
assert field.shape == (64, 64)
|
||||
assert np.isfinite(field).all()
|
||||
assert field.min() <= 150.0 and field.max() >= 148.0
|
||||
|
||||
|
||||
def test_round_clip_produces_nans_outside_circle():
|
||||
xs = np.array([0.0, 5.0, -5.0])
|
||||
ys = np.array([5.0, -5.0, 0.0])
|
||||
vs = np.array([149.0, 150.0, 148.0])
|
||||
field = interpolate_field(xs, ys, vs, width=64, height=64,
|
||||
extent=(-15, 15, -15, 15), round_clip=True)
|
||||
assert np.isnan(field).any()
|
||||
# center pixel must be finite
|
||||
assert np.isfinite(field[32, 32])
|
||||
|
||||
|
||||
def test_backend_reported():
|
||||
assert BACKEND in ("cupy", "numpy")
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Tests for src/pygui/backend/sensor_editor.py."""
|
||||
import pytest
|
||||
from pygui.backend.models.sensor_editor import SensorEditor
|
||||
|
||||
|
||||
def test_replace_overrides_raw_value():
|
||||
ed = SensorEditor()
|
||||
ed.set_replacement(2, 150.0) # index 2 forced to 150.0 regardless of raw
|
||||
assert ed.apply([149.0, 149.0, 99.0]) == [149.0, 149.0, 150.0]
|
||||
|
||||
|
||||
def test_offset_shifts_raw_value():
|
||||
ed = SensorEditor()
|
||||
ed.set_offset(0, +0.5)
|
||||
assert ed.apply([149.0, 149.0]) == [149.5, 149.0]
|
||||
|
||||
|
||||
def test_replace_and_offset_apply_in_order():
|
||||
"""Replacement wins over raw, then offset is added on top."""
|
||||
ed = SensorEditor()
|
||||
ed.set_replacement(2, 150.0)
|
||||
ed.set_offset(2, +1.0) # sensor 2: replace 99→150, then +1 → 151
|
||||
ed.set_offset(0, +0.5)
|
||||
out = ed.apply([149.0, 149.0, 99.0, 149.0])
|
||||
assert out == [149.5, 149.0, 151.0, 149.0]
|
||||
|
||||
|
||||
def test_clear_single_index_restores_only_that_sensor():
|
||||
ed = SensorEditor()
|
||||
ed.set_replacement(0, 0.0)
|
||||
ed.set_replacement(1, 0.0)
|
||||
ed.clear(0) # clear only index 0
|
||||
assert ed.apply([149.0, 149.0]) == [149.0, 0.0]
|
||||
|
||||
|
||||
def test_clear_restores_all():
|
||||
ed = SensorEditor()
|
||||
ed.set_offset(0, 5.0)
|
||||
ed.set_replacement(1, 200.0)
|
||||
ed.clear()
|
||||
assert ed.apply([149.0, 149.0]) == [149.0, 149.0]
|
||||
|
||||
|
||||
def test_no_overrides_returns_copy():
|
||||
ed = SensorEditor()
|
||||
original = [149.0, 149.0]
|
||||
result = ed.apply(original)
|
||||
assert result == original
|
||||
assert result is not original
|
||||
@@ -0,0 +1,28 @@
|
||||
from pygui.backend.models.frame import Frame
|
||||
from pygui.backend.models.threshold_classifier import ThresholdConfig, BAND_HIGH, BAND_IN, BAND_LOW
|
||||
from pygui.backend.models.session_model import SessionModel, SessionUpdate
|
||||
|
||||
|
||||
|
||||
def test_manual_banding_and_stats():
|
||||
model = SessionModel(ThresholdConfig(set_point=149.0, margin=1.0, auto=False))
|
||||
update = model.process(Frame(seq=7, time=1.0, values=[151.0, 149.0, 147.0]))
|
||||
assert update.seq == 7
|
||||
assert update.bands == [BAND_HIGH, BAND_IN, BAND_LOW]
|
||||
assert update.stats.avg == 149.0
|
||||
assert (update.target, update.margin) == (149.0, 1.0)
|
||||
assert update.state in ("idle", "ramp", "set")
|
||||
|
||||
def test_auto_banding_uses_mean_and_sigma():
|
||||
model = SessionModel(ThresholdConfig(auto=True))
|
||||
# Tight cluster around 149 -> small σ -> the 200.0 outlier reads HIGH
|
||||
update = model.process(Frame(seq=0, time=0.0, values=[149.0, 149.0, 149.0, 200.0]))
|
||||
assert update.bands[3] == BAND_HIGH
|
||||
assert update.target == update.stats.avg
|
||||
|
||||
def test_set_thresholds_reband_on_next_frame():
|
||||
m = SessionModel(ThresholdConfig(set_point=149.0, margin=1.0, auto=False))
|
||||
m.set_thresholds(ThresholdConfig(set_point=149.0, margin=5.0, auto=False))
|
||||
upd = m.process(Frame(seq=0, time=0.0, values=[151.0]))
|
||||
assert upd.bands == [BAND_IN]
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
from pygui.backend.models.stability_detector import (
|
||||
StabilityDetector, STATE_IDLE, STATE_RAMP, STATE_SET,
|
||||
)
|
||||
|
||||
|
||||
SET_POINT = 149.0
|
||||
|
||||
def make():
|
||||
return StabilityDetector(idle_below = 50.0, tolerance=1.0, settle_seconds=2.0)
|
||||
|
||||
def test_idle_when_cold():
|
||||
d = make()
|
||||
assert d.update(avg=25.0, time=0.0, set_point=SET_POINT) == STATE_IDLE
|
||||
|
||||
def test_ramp_while_far_from_setpoint():
|
||||
d = make()
|
||||
d.update(avg=100.0, time=0.0, set_point=SET_POINT)
|
||||
|
||||
def test_ramp_until_settle_time_elapses():
|
||||
d = make()
|
||||
assert d.update(avg=149.2, time=0.0, set_point=SET_POINT) == STATE_RAMP
|
||||
assert d.update(avg=148.9, time=0.0, set_point=SET_POINT) == STATE_RAMP
|
||||
|
||||
def test_set_after_holding_near_setpoint():
|
||||
d = make()
|
||||
d.update(avg=149.2, time=0.0, set_point=SET_POINT)
|
||||
d.update(avg=148.9, time=1.0, set_point=SET_POINT)
|
||||
assert d.update(avg=149.0, time=2.5, set_point=SET_POINT) == STATE_SET
|
||||
|
||||
|
||||
def test_back_to_ramp_on_disturbance():
|
||||
d = make()
|
||||
for t in (0.0, 1.0, 2.5):
|
||||
d.update(149.0, t, set_point=SET_POINT)
|
||||
assert d.update(avg=160.0, time=3.0, set_point=SET_POINT) == STATE_RAMP
|
||||
@@ -0,0 +1,47 @@
|
||||
import time
|
||||
from pygui.backend.models.frame import Frame
|
||||
from pygui.serialcomm.stream_reader import StreamReader
|
||||
|
||||
class FakeTransport:
|
||||
"""Yield canned data then blocks (returns b'')."""
|
||||
def __init__(self, lines):
|
||||
self._data = b"".join(lines)
|
||||
self._pos = 0
|
||||
self.closed = False
|
||||
def read(self, n=1):
|
||||
time.sleep(0.001)
|
||||
if self._pos >= len(self._data):
|
||||
return b""
|
||||
res = self._data[self._pos:self._pos+n]
|
||||
self._pos += n
|
||||
return res
|
||||
def readline(self):
|
||||
time.sleep(0.001)
|
||||
if self._pos >= len(self._data):
|
||||
return b""
|
||||
idx = self._data.find(b"\n", self._pos)
|
||||
if idx == -1:
|
||||
res = self._data[self._pos:]
|
||||
self._pos = len(self._data)
|
||||
return res
|
||||
res = self._data[self._pos:idx+1]
|
||||
self._pos = idx + 1
|
||||
return res
|
||||
def close(self): self.closed = True
|
||||
|
||||
def parse_line(raw: str, seq: int) -> Frame:
|
||||
parts = [float(x) for x in raw.split(",")]
|
||||
return Frame(seq = seq, time = parts[0], values = parts[1:])
|
||||
|
||||
def test_reads_frames_and_counts_errors():
|
||||
got, errors = [], []
|
||||
transport = FakeTransport([b"0.0,149,148\n", b"garbage\n", b"0.5,150,149\n"])
|
||||
r = StreamReader(transport, parse_line,
|
||||
on_frame = got.append, on_error = lambda e: errors.append(e))
|
||||
|
||||
r.start()
|
||||
time.sleep(0.1)
|
||||
r.stop()
|
||||
assert [f.values for f in got] == [[149.0, 148.0], [150.0, 149.0]]
|
||||
assert r.error_count == 1
|
||||
assert transport.closed
|
||||
@@ -0,0 +1,29 @@
|
||||
import math
|
||||
|
||||
from pygui.backend.models.threshold_classifier import (
|
||||
ThresholdConfig, classify, classify_all, resolve_bounds,
|
||||
BAND_IN, BAND_HIGH, BAND_LOW
|
||||
)
|
||||
|
||||
|
||||
def test_classify_about_target_margin():
|
||||
assert classify(149.0, target=149.0, margin=1.0) ==BAND_IN # exactly
|
||||
assert classify(149.9, target=149.0, margin=1.0) ==BAND_IN # within
|
||||
assert classify(150.5, target=149.0, margin=1.0) ==BAND_HIGH # 1.5 above
|
||||
assert classify(147.5, target=149.0, margin=1.0) ==BAND_LOW # 1.5 below
|
||||
|
||||
|
||||
def test_manual_bounds_use_set_point_and_margin():
|
||||
cfg = ThresholdConfig(set_point=149.0, margin=1.0, auto=False)
|
||||
assert resolve_bounds([200.0, 0.0], cfg) == (149.0, 1.0)
|
||||
|
||||
def test_auto_bounds_use_mean_and_sigma():
|
||||
cfg = ThresholdConfig(auto=True)
|
||||
target, margin = resolve_bounds([148.0, 150.0, 149.0], cfg)
|
||||
assert target == 149.0
|
||||
assert margin == math.sqrt(2 / 3)
|
||||
|
||||
def test_classify_all_manual():
|
||||
cfg = ThresholdConfig(set_point=149.0, margin=1.0, auto=False)
|
||||
assert classify_all([149.0, 151.0, 147.0], cfg) == [BAND_IN, BAND_HIGH, BAND_LOW]
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
"""Tests for src/pygui/backend/wafer_layouts.py."""
|
||||
import pytest
|
||||
from pygui.backend.wafer.wafer_layouts import load_layout, available_families
|
||||
from pygui.backend.wafer.zwafer_models import Sensor
|
||||
|
||||
|
||||
def test_lists_bundled_families():
|
||||
fams = available_families()
|
||||
assert "aep" in fams and "x" in fams
|
||||
|
||||
|
||||
def test_loads_sensors_in_mm():
|
||||
sensors = load_layout("aep")
|
||||
assert sensors and all(isinstance(s, Sensor) for s in sensors)
|
||||
assert any(s.x < 0 for s in sensors)
|
||||
assert max(abs(s.x) for s in sensors) < 200.0
|
||||
|
||||
|
||||
def test_unknown_family_raises():
|
||||
with pytest.raises(KeyError):
|
||||
load_layout("nope")
|
||||
@@ -1,6 +1,19 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.11"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.14' and sys_platform == 'darwin'",
|
||||
"python_full_version < '3.14' and sys_platform == 'darwin'",
|
||||
"python_full_version >= '3.14' and sys_platform == 'win32'",
|
||||
"python_full_version < '3.14' and sys_platform == 'win32'",
|
||||
"python_full_version >= '3.14' and sys_platform == 'linux'",
|
||||
"python_full_version < '3.14' and sys_platform == 'linux'",
|
||||
]
|
||||
supported-markers = [
|
||||
"sys_platform == 'darwin'",
|
||||
"sys_platform == 'win32'",
|
||||
"sys_platform == 'linux'",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
@@ -11,6 +24,146 @@ 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 = "contourpy"
|
||||
version = "1.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cycler"
|
||||
version = "0.12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fonttools"
|
||||
version = "4.63.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/84/69/c97f2c18e0db87d2c7b15da1974dace76ae938f1cfa22e2727a648b7ed43/fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0", size = 3597189, upload-time = "2026-05-14T12:04:30.958Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/75/2b/a7f1545bdf5da69c4bda0cea2a5781f0ad2a6623e0277267672db43c5fe6/fonttools-4.63.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b8ae05d9eacf6081414d759c0a352769ac28ce31280d6bb8e77b03f9e3c449f", size = 2881793, upload-time = "2026-05-14T12:02:56.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/50/965308c703f085f225db2886813b27e015b8b3438c350b22dd65b52c2a2c/fonttools-4.63.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79cdc9f567aec74a72918fd060283911406750cbc9fd28c1316023deb6ce31a9", size = 2428130, upload-time = "2026-05-14T12:02:58.891Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/38/6937fbd7f2dc3a6b48725851bc2c15ec949b9af14d9bbcb5fe83cdf9bdf9/fonttools-4.63.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c14b4fd138c4bafcca294765c547914e1aa431ae1ca94ab99d8db08c958bd3b", size = 5111952, upload-time = "2026-05-14T12:03:01.263Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/43/a81f20050a3115b57d62c8e781446949512eac36690dc384ccea65ff4cc1/fonttools-4.63.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76ac49f929aecaf82d83250b8347e099d7aecba0f4726c1d9b6df3b8bb5fe18", size = 5082308, upload-time = "2026-05-14T12:03:03.211Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/00/cdd9d4944ca6ae280d01e69cc37bde3bf663630b837a6fc6d2cd65d80e0e/fonttools-4.63.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dcf076a4474fe0d7367e5bbf5b052c7284fa1feca729c04176ce513521afd8a0", size = 5087932, upload-time = "2026-05-14T12:03:05.147Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/f1/0aa0dbea778c75adbef223c42019fd47d22262b905974d62d829545d485f/fonttools-4.63.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7dd683fef0663e9f0f45cf541d788d24caa3ec9db50796b588e1757d8b3bc007", size = 5213271, upload-time = "2026-05-14T12:03:07.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/99/253e4056e1f0e67b9390125a154b73b5eb73ad521bece95c004858fdeec2/fonttools-4.63.0-cp311-cp311-win32.whl", hash = "sha256:afefc1ed0a59785a7fb06ea7e1678e849c193e1e387db783579bc7b3056fcfcb", size = 2304473, upload-time = "2026-05-14T12:03:09.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/60/defa5e69641db890a63be281f41345f4c33b157824eaf0b9fad3e08b0dcb/fonttools-4.63.0-cp311-cp311-win_amd64.whl", hash = "sha256:063e08bd17bd5a90127a14123de0d6a952dbc847695fd98b63c043d58057f90c", size = 2356389, upload-time = "2026-05-14T12:03:11.53Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/ef/b3c6b9b5be2f82416d73fe2ed2e96e2793cd80e7510bd6a17ca79cdd88ec/fonttools-4.63.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02", size = 2881131, upload-time = "2026-05-14T12:03:13.386Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/a0/c815bea63117fa63e4e1c01f8a1110d2112fa003f838e6467094ec2432ce/fonttools-4.63.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0", size = 2426704, upload-time = "2026-05-14T12:03:15.801Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/04/0b91d8e916e92ad1fac9e4624760baf0fd5ff2ead614c2f68fb21373f03f/fonttools-4.63.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af", size = 5044298, upload-time = "2026-05-14T12:03:18.085Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/c7/2342da9830e3e9d4870305ca5d2091d2a83284f2953079b7bdd3b5e029d8/fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8", size = 4999800, upload-time = "2026-05-14T12:03:20.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/6d/67fe16c48d7ce050979b33f47e0d28a318f02da030602e944c34f7a16ef3/fonttools-4.63.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b", size = 4982666, upload-time = "2026-05-14T12:03:22.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/00/3bbab338c07c71fa56269953845e92c951a61457bbbb0f1022551ea266d9/fonttools-4.63.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78", size = 5133598, upload-time = "2026-05-14T12:03:25.168Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/f2/aa27c7f98db5b064883dadcc5283947e81e034de42e22a33675878d98b54/fonttools-4.63.0-cp312-cp312-win32.whl", hash = "sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263", size = 2292575, upload-time = "2026-05-14T12:03:27.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/36/cccb9bc2a6ab63d1b2980374f0dca72ce95ae267c9b4cfe77455bb70d0d4/fonttools-4.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272", size = 2343211, upload-time = "2026-05-14T12:03:30.057Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/8d/d8fec3dcde2963f8c908fb315e5ff2cd0ac34f82394bbbf73a2aa5145ce3/fonttools-4.63.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd", size = 2876062, upload-time = "2026-05-14T12:03:32.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/71/d935dc54e4ff121bfdd11e08702db63a7e6f25af21d8a3d7b7212df53641/fonttools-4.63.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59", size = 2424594, upload-time = "2026-05-14T12:03:34.86Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/40/e76320afa1df918e146155ef239b1719ee266092e96f5423bfd075affba1/fonttools-4.63.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d", size = 5024840, upload-time = "2026-05-14T12:03:36.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/36/0b805d8c485f872f65a509cbe3b58a5d0d17bee855333b54a150c79d3061/fonttools-4.63.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68", size = 4975801, upload-time = "2026-05-14T12:03:38.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/26/2cee03d0aa083ab022da5c07aff9ed3f689da1defb81ad6917c9627896da/fonttools-4.63.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be", size = 4965009, upload-time = "2026-05-14T12:03:41.494Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/48/cc4b66d9058c0d0982c833fad10127c4b0e9324606aafa41382295ca4102/fonttools-4.63.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27", size = 5105892, upload-time = "2026-05-14T12:03:43.525Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/1f/a98a30a814b9ddef3a2e706025f90b9e0bc94890e6cb15254bc86547d11a/fonttools-4.63.0-cp313-cp313-win32.whl", hash = "sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380", size = 2291313, upload-time = "2026-05-14T12:03:45.594Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/46/5177b01f3b4abfdd4409f31cca4ab279c9343a26efbe9ec78c97fc612e02/fonttools-4.63.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b", size = 2342299, upload-time = "2026-05-14T12:03:47.414Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/d2/23d25e3f247b328be58d04a4c9f894178a0d1eda7d42867cfb388adaf416/fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745", size = 2875338, upload-time = "2026-05-14T12:03:50.052Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/58/7dfa0c761cb3b2964e2a84c4dc986c926a87de0cb9fb60d5b28ded3f2914/fonttools-4.63.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03", size = 2422661, upload-time = "2026-05-14T12:03:52.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/87/64cfa18a7a1621d17b7f4502b2b0ed8a135a90c3db51ea590ee99043e76b/fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49", size = 5010526, upload-time = "2026-05-14T12:03:54.647Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/e1/a8933a72c45a87177fbde2696e0d0755c8c9062f8c077a961c6215fa27b1/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b", size = 4923946, upload-time = "2026-05-14T12:03:56.984Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/60/872e6e233b8c5e8b41413796ff18b7fe479661bd40147e071b450dfad7a1/fonttools-4.63.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6", size = 4962489, upload-time = "2026-05-14T12:03:59.443Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/c4/83c24f2ec38b90cfda84bf4b1a1f49df80e84a1db4e7ac6e0d41bf23bc39/fonttools-4.63.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4", size = 5071870, upload-time = "2026-05-14T12:04:02.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/40/3ae22b60ff1d41ce0bd044b31238cdc72cef99f28b976f1e128ebd618c9b/fonttools-4.63.0-cp314-cp314-win32.whl", hash = "sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616", size = 2295026, upload-time = "2026-05-14T12:04:04.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/d4/98078064ccc76b45cb0f6c002452011e93c4bd26f6850344f0951cc1fe89/fonttools-4.63.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5", size = 2347454, upload-time = "2026-05-14T12:04:06.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/4e/652d1580c5f4e39f7d103b0c793e4773129ad633dce4addd0cf4dfebde02/fonttools-4.63.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001", size = 2958152, upload-time = "2026-05-14T12:04:08.706Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/55/ad864c9a9b219f552eb46b32cd7906c466e5a578ba0c3abfcc0fe7413eb6/fonttools-4.63.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e", size = 2460809, upload-time = "2026-05-14T12:04:10.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/2b/0aa8db70f18cf52e49b4ed5ecec68547f981160bf5ded3b5aed6faa0a6f9/fonttools-4.63.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096", size = 5148649, upload-time = "2026-05-14T12:04:12.747Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/63/18e4369c25043096f1048e0c9915951adc4f842bd81c6b18155824d6fa99/fonttools-4.63.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f", size = 4932147, upload-time = "2026-05-14T12:04:14.806Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/3f/67f3eac2ffd8a98446c5022f8ed3864eac878a5ff7af8df4c8286dba16cc/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40", size = 5027237, upload-time = "2026-05-14T12:04:17.675Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/ba/4e6214cb38a7b04779e97bb7636de9a5c7f20af7018d03dee0b64c08510a/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196", size = 5053933, upload-time = "2026-05-14T12:04:20.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/3b/214dcc19ee31d3d38fb5ad2755c11ef0514e5dc300bbaf41c0b69f393799/fonttools-4.63.0-cp314-cp314t-win32.whl", hash = "sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8", size = 2359326, upload-time = "2026-05-14T12:04:24.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/1e/3ff1a9b523058c2eeb6a9d50f5574e2a738200d0d94107d5bc4105e8da3f/fonttools-4.63.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419", size = 2425829, upload-time = "2026-05-14T12:04:26.829Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
@@ -20,6 +173,255 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kiwisolver"
|
||||
version = "1.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/dd/a495a9c104be1c476f0386e714252caf2b7eca883915422a64c50b88c6f5/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c", size = 122798, upload-time = "2026-03-09T13:12:58.963Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/60/37b4047a2af0cf5ef6d8b4b26e91829ae6fc6a2d1f74524bcb0e7cd28a32/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb", size = 66216, upload-time = "2026-03-09T13:13:00.155Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/aa/510dc933d87767584abfe03efa445889996c70c2990f6f87c3ebaa0a18c5/kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac", size = 63911, upload-time = "2026-03-09T13:13:01.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/46/bddc13df6c2a40741e0cc7865bb1c9ed4796b6760bd04ce5fae3928ef917/kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27", size = 1438209, upload-time = "2026-03-09T13:13:03.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/d6/76621246f5165e5372f02f5e6f3f48ea336a8f9e96e43997d45b240ed8cd/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398", size = 1248888, upload-time = "2026-03-09T13:13:05.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/c1/31559ec6fb39a5b48035ce29bb63ade628f321785f38c384dee3e2c08bc1/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db", size = 1266304, upload-time = "2026-03-09T13:13:06.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/ef/1cb8276f2d29cc6a41e0a042f27946ca347d3a4a75acf85d0a16aa6dcc82/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc", size = 1319650, upload-time = "2026-03-09T13:13:08.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/e4/5ba3cecd7ce6236ae4a80f67e5d5531287337d0e1f076ca87a5abe4cd5d0/kiwisolver-1.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679", size = 970949, upload-time = "2026-03-09T13:13:10.299Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/69/dc61f7ae9a2f071f26004ced87f078235b5507ab6e5acd78f40365655034/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309", size = 2199125, upload-time = "2026-03-09T13:13:11.841Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/7b/abbe0f1b5afa85f8d084b73e90e5f801c0939eba16ac2e49af7c61a6c28d/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2", size = 2293783, upload-time = "2026-03-09T13:13:14.399Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/80/5908ae149d96d81580d604c7f8aefd0e98f4fd728cf172f477e9f2a81744/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c", size = 1960726, upload-time = "2026-03-09T13:13:16.047Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/08/a78cb776f8c085b7143142ce479859cfec086bd09ee638a317040b6ef420/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08", size = 2464738, upload-time = "2026-03-09T13:13:17.897Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/e1/65584da5356ed6cb12c63791a10b208860ac40a83de165cb6a6751a686e3/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4", size = 2270718, upload-time = "2026-03-09T13:13:19.421Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/6c/28f17390b62b8f2f520e2915095b3c94d88681ecf0041e75389d9667f202/kiwisolver-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b", size = 73480, upload-time = "2026-03-09T13:13:20.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/0e/2ee5debc4f77a625778fec5501ff3e8036fe361b7ee28ae402a485bb9694/kiwisolver-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac", size = 64930, upload-time = "2026-03-09T13:13:21.997Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/eb/5fcbbbf9a0e2c3a35effb88831a483345326bbc3a030a3b5b69aee647f84/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232", size = 59532, upload-time = "2026-03-09T13:15:47.047Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/9b/e17104555bb4db148fd52327feea1e96be4b88e8e008b029002c281a21ab/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a", size = 57420, upload-time = "2026-03-09T13:15:48.199Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/44/2b5b95b7aa39fb2d8d9d956e0f3d5d45aef2ae1d942d4c3ffac2f9cfed1a/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737", size = 79892, upload-time = "2026-03-09T13:15:49.694Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/7d/7157f9bba6b455cfb4632ed411e199fc8b8977642c2b12082e1bd9e6d173/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16", size = 77603, upload-time = "2026-03-09T13:15:50.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matplotlib"
|
||||
version = "3.11.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "contourpy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "cycler", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "fonttools", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "kiwisolver", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "numpy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "pillow", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "pyparsing", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/24/080c99d223d158d3a8902769269ab6da5b50f7a0e6e072513907e02b7a6c/matplotlib-3.11.0.tar.gz", hash = "sha256:68c0c7be01b30dcca3638934f7f591df73401235cbdbf0d1ab1c71e7db7f8b57", size = 33251176, upload-time = "2026-06-12T02:29:15.508Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/a2/78f662f1b18968531f67d3fcde1b7ea8496920bacd4f16ddb5b79d112e46/matplotlib-3.11.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f857524b442f0f36e641868ce2171aafa88cb0bc0644f4e1d8a5df9b32649fef", size = 9436261, upload-time = "2026-06-12T02:27:34.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/92/044f1de43901310202f4c79acf4f141be53b2ca8d8380e2fcefb3d523a75/matplotlib-3.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:57baa92fdc82948ed716eae6d2579d4d6f40965cd8d2f416755b4a72580a3233", size = 9264669, upload-time = "2026-06-12T02:27:37.413Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/f4/f0b4f9ba7ec14a7af8151f3ad71ecfe3561e6ba38cfab1db3681ba4ca112/matplotlib-3.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:630eee0e67d35cce2019a0e670719f4816e3b86aff0fa72729f6c69786fceb45", size = 10021076, upload-time = "2026-06-12T02:27:39.926Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/33/4d679c6dcd594a156542080ac907ddccf7b09ca11655c4b28eca8e9ee5da/matplotlib-3.11.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5106c444d0bf966eee2853548c03772af4ab7199118e086c62fbac8ccb07c055", size = 10828999, upload-time = "2026-06-12T02:27:42.433Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/74/0a3683802037d8cd013144d77c247219b47f2aabace6fdde74faa12bacf7/matplotlib-3.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d7aea652b58e686444079be3376ef546bffa1eee9b9bb9c472b9fcf6cf410d3", size = 10913103, upload-time = "2026-06-12T02:27:44.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/9f/970fcbf381e82ec66fdf5da8ea76e2e9240f61a24011ce9fd1d42c37ac2d/matplotlib-3.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:70a5b3e9a5dab708c0f039709ae7c68d5b4d254e291ef76492cdba230c8bb5e4", size = 9310945, upload-time = "2026-06-12T02:27:46.867Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/4e/6e7cfed23611265ded53806852343b5c59339e506e84c474a9b5afc3b249/matplotlib-3.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:3d68266213e73823ac3be90615bab0cf31f88851e114cdb1dd25dacf3b01e1a7", size = 8999304, upload-time = "2026-06-12T02:27:48.798Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/17/f5276b496c61477a6c4fc5e7401f4bfe1c2e5ef7c6cd67896f2ade3809cb/matplotlib-3.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:06b5872e9cf11adc8f589ded3ce11bc3e1061ad498259664fabc1f6615beb918", size = 9449976, upload-time = "2026-06-12T02:27:50.989Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/34/bdd77418adb2178a1d59f044bd67bfebb115896e91b840b8a197eb3f4f4e/matplotlib-3.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0515d495124be3124340e59f164d901ed4484e2246a5b74cfa483cac3b80bd97", size = 9279307, upload-time = "2026-06-12T02:27:53.247Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/95/7f522393c88313336b20d70fc849555757b2e5febc22b83b3a3f0fd4bce9/matplotlib-3.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be5f93a1d21981bfb802ded0d77a0caa92d4342a47d45754fac77e314a506344", size = 10031353, upload-time = "2026-06-12T02:27:55.215Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/ce/8f25a0e3186aefd61913e7467d1b999465bcd0d0c03ac695c1b26ca559b7/matplotlib-3.11.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41635d7909d19e52e924a521dde6d8f670b0f53ab1d0e8c331fa831554f681d1", size = 10839232, upload-time = "2026-06-12T02:27:57.746Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/c2/db15da2bbdf9e3ca66df7db8e2c33a1dfed67be24a24d2c878efaaff01d6/matplotlib-3.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:94f5000f67ca9faa300863ea17f8bce9175cb67b88bec4bc7780502d53dd7c9e", size = 10923899, upload-time = "2026-06-12T02:28:00.223Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/2f/a58a4443a4d052a4ea77557478336aefc26c7981f6408d37adba763aa758/matplotlib-3.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ac6f1ef39f3d0f9e2463303013094992cdbe0f85f43bc54155bc472b2042768e", size = 9329528, upload-time = "2026-06-12T02:28:02.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/0f/4b669589d47733b97ab9df4b58d6fc1e68acb5ea42a928dc7cbdd6bf5871/matplotlib-3.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:9dd11fb612ce7bc60b1de5b4fc87ff959d22317b5de42aabf392f66f97af22eb", size = 9003413, upload-time = "2026-06-12T02:28:04.49Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/41/aa47f156b061d14c98b906f76c428507397708ec63ff94f410ae1752b426/matplotlib-3.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce3b839b34ae1f430b4616893a2945a2999debaa7e94e7e29a2a8bbf286f7b5", size = 9450532, upload-time = "2026-06-12T02:28:06.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/4f/5a9eb0375e81413953febf8af7b012a6b6357f53438a15c4f5ad86c6bbb5/matplotlib-3.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:373db8f91214e8ccaf35ac833cc1dd59dd961e148bbd55dd027141591dde1313", size = 9279760, upload-time = "2026-06-12T02:28:09.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/c0/1117d53077e3ac3152503a84e9cf7a5c239576805ee71276e80c2aaa7471/matplotlib-3.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be152b7570324dc8d01574cc9474dd2d803237acf528bcbb5b211fa347461a09", size = 10031623, upload-time = "2026-06-12T02:28:11.26Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/7e/e937138daffad65b71bf831a377809dcbc830fb4f31a31e067dc1faa2575/matplotlib-3.11.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:126f256df600652d7e4b394cf3164ff75210a00038f287c95a012a6f58d0e83f", size = 10839372, upload-time = "2026-06-12T02:28:14.102Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/c2/438ecc197ffb8023b6b9922915542f2172f5fd45b76703b0b4fc47322243/matplotlib-3.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:03acfeddf87b0dddb11b081ef7740ad445a3ca8bcb6b8e3011b08f2cf802b75c", size = 10924099, upload-time = "2026-06-12T02:28:16.383Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/2e/395883da416f378b3ed2c9f3e843ac477eae1ce731b671b79adaa6f0bacd/matplotlib-3.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:ab3722f04f3ff34c23b5012c5873d2894174e06c3822fcdac3610965a5ac7d06", size = 9329727, upload-time = "2026-06-12T02:28:18.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/82/2c388956abf8bf392dfb5b8917c502f1082df6a941b781ab8c8e5ba2474b/matplotlib-3.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:c945824670fb8915b4ac879e5e61f3c58e0913022f70a0de4c082b17372f8771", size = 9003506, upload-time = "2026-06-12T02:28:20.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/c1/34454baa44da7975ada82e9aea37105ec47059514dc967d3be14426ba8dc/matplotlib-3.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3489c3dc487669b4a980bc3068f87856de7a1564248d3f6c629efb2a58b03f24", size = 9499838, upload-time = "2026-06-12T02:28:22.713Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/c3/98fe79a398cf232219f090163a7fa7e6766e9f2e0ad26df54d6f8934d8ee/matplotlib-3.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6a98f5476ce784a50ce09998f4ae1e6a9f25043cef8a480c98949902eda74620", size = 9332298, upload-time = "2026-06-12T02:28:24.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/e4/b4b7c33151e74e5c802f3cde1ba807ebfc38401e329b44e215a5888dd76d/matplotlib-3.11.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:565af866fd63e4bd3f987d580afe27c44c2552a3b3305f4ecbb85133601ea6f3", size = 10045491, upload-time = "2026-06-12T02:28:27.141Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/28/394548efd68354110c1a1be11fe6b6e559e06d1a23da35908a0e316c55a9/matplotlib-3.11.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e6b3e64dea5062c570f04358e2711859f3531b459f29516274fbad889079e4f3", size = 10857059, upload-time = "2026-06-12T02:28:29.222Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/44/e7922e6e2a4d63bdfbc9dc4a53e3850ab438d46cf42e6779bb15ec92c948/matplotlib-3.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:942b37c5db1899610bd1543ce8e13e4ecff9a4633e7f63bb6aa9205d2644ebd1", size = 10939576, upload-time = "2026-06-12T02:28:31.66Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/be/b1ca96003a441d619b727fee21d671fdff7a5ce2f1bb797b2521aa2f679a/matplotlib-3.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:c08e649a6313e1291e713623b97a38e5bb4aa580b2a100a94a3309bc6b9c8eb3", size = 9379519, upload-time = "2026-06-12T02:28:33.888Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/72/4bf3b91821c34596dd6a7bdac5836d94f744144c8208939ef49d8ec43f7e/matplotlib-3.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2746cd2c113742ff6ce37a864c5ac5fd7aa644568f445e66166e457ac78e40e0", size = 9055456, upload-time = "2026-06-12T02:28:35.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/52/a94102ac99eb78e2fe9b826674f9ef9ee23327110ea6ab4776c1b4eb6209/matplotlib-3.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3338e3e3de128cf50d0d2fb92a122815daf9c755bd882a474343c05f8fd7ec79", size = 9452137, upload-time = "2026-06-12T02:28:37.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/03/b8cdb625a21f710dfa11bbca1f48fb4057d2c0286975f8b415bf80942c99/matplotlib-3.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:25c2e5455efd8d99f41fb79871a31feb7d301569642e332ec58d72cfe9282bc3", size = 9281514, upload-time = "2026-06-12T02:28:40.028Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/2d/4e1240ea82ee197dfb3851e71f71c87eeeb975f1753b56a0588e4e80739a/matplotlib-3.11.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9695457a467ff86d23f35037a43deb6f1134dd6d3e2ac8ce1e2087cff09ffb9", size = 10843005, upload-time = "2026-06-12T02:28:42.39Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/dc/6377ecfaa5fef79430f74a1a16638b4e2aa30d4692bae2c19f9d76fe3b01/matplotlib-3.11.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19c16c61dea63b3582918503e6b294193961261d9daa806d4ae2151f1ad05430", size = 11127459, upload-time = "2026-06-12T02:28:44.483Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/41/795c405aa7560443a3b01309424cde4a1113b85c90b8a63417444a749617/matplotlib-3.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2d72ea8b7924f3cb955e61518d21e43b3df1e6c8a793b480a0c1214f185d30ba", size = 10925160, upload-time = "2026-06-12T02:28:46.564Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/f7/3a9e6389a7cfaeff76c56e40c2dabcb13110e21e82f837228c834ebe748c/matplotlib-3.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:1c02da0a629dfa9debf52725ea06866b74c1fb70a895bae05e4493d34074f9f2", size = 9485186, upload-time = "2026-06-12T02:28:49.344Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/c0/396478ee7cf2091d182db8b4a8695f6a37f1ddb978989cf9dbb84cd5c123/matplotlib-3.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aa55d73b3117d4b07f959cd9eb6f69b375d8df3414139c479388e551aa5d999d", size = 9160349, upload-time = "2026-06-12T02:28:51.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/6f/1c3bd51bb2b34eaacdcf3c3d859dbb357f952fc8020c617dc118ad7c9e38/matplotlib-3.11.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a9d8c6e7cd2f0ddf11d8d92e520dd1d9d2abb0cf6ac8831e338666c81e905847", size = 9500921, upload-time = "2026-06-12T02:28:53.443Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/0d/4d861d0121840cb1a3fd4a10deb211efd6fccd481ed23e553f31f4f4da4a/matplotlib-3.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:be050fcf32f729eda99f7f75a80bf67612ce16ab9ac1c23a387dcaede95cb70e", size = 9332190, upload-time = "2026-06-12T02:28:55.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/cb/22f6bc35711a0b5639a784e74e653e77c86210bd4304449dd399a482f74e/matplotlib-3.11.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfabef0230d0697aa0d717385194dd41162e00207a68bf4abf94c2bf4c27dca0", size = 10854181, upload-time = "2026-06-12T02:28:57.856Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/7e/9a9eaca731a2939589da520f0ebe8fd8753d0f51fca98c7d20af6dbe261a/matplotlib-3.11.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1644db30e759199443493ac5e5caec24fdb775a8f6123021f85ba47c4133c3cb", size = 11137715, upload-time = "2026-06-12T02:29:00.555Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/f9/9b030b6088354acb0296871bb624b25befc1c42509d3c6cd17420c83a5b8/matplotlib-3.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:15b0d160079cb10699a0e98b5989c70677b2df7cacdc62af67c30f2facec46d9", size = 10939427, upload-time = "2026-06-12T02:29:02.527Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/94/6b273eaee4ee250863567d100865da61a5c1527fa67f527b7ed22e0dd29c/matplotlib-3.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:446307e6b04b57b1f1239e228a1ec2af0d589a1008cebc3dfa3f5441d095cfb6", size = 9535809, upload-time = "2026-06-12T02:29:04.994Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/95/1d36bddf2b7e2692c1540e78a6e5bc88bc1496b137e3e35a611f91b65ac3/matplotlib-3.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:652fb5696271d4c50f196d22a5ff4f8e4444c74f847423570d7dc0aa2bbd0159", size = 9209226, upload-time = "2026-06-12T02:29:07.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/c2/f5da6cd37ed6871f5c9b3c0507ddb69f14d6c36fac4541e4e0c60cb8cdfc/matplotlib-3.11.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:81ae77077a1e16d37a5b61096ccb07c8d90a99b518fa8256b8f21578932f2f62", size = 9434094, upload-time = "2026-06-12T02:29:09.135Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/07/56f66906e0f87a0c6d0d0acbd34dbc9432b1931d8f26ef618bd6f92932a9/matplotlib-3.11.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ddef37840695f5eef65f9f070fe2d2f510f584c2156203f9f622a5b0584efffd", size = 9262183, upload-time = "2026-06-12T02:29:11.283Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/d8/c4ecab06b7ea36a570c4f3bd2d48d1799fd5d9174470e45c2194199431e7/matplotlib-3.11.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf662e5ac5707658cb931e19972c4bd99f7b4f8b7bf79d3c821d239fa6b71e64", size = 10015653, upload-time = "2026-06-12T02:29:13.251Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.2"
|
||||
@@ -29,6 +431,153 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pandas"
|
||||
version = "3.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/42/16/b5c76b838fd9bf6ce84d3a53346b8874ec05c5f0040d75ef2c320100cd2a/pandas-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:455f6f8139d4282188f526868dbc3c828470e88a3d9d59a891bd46a455f21b98", size = 10338495, upload-time = "2026-05-11T18:52:11.558Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/b0/a4ffc4ae74d2d822200dcc46898987d8eb6032d1e2b219cae39da6f5cbcc/pandas-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4e15135e2ee5df1063313e2425ceef8ac0f4ae775893815b0923651b806a5639", size = 9938250, upload-time = "2026-05-11T18:52:17.005Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/b2/3323601a52caee42c019e370090ca4544b241437240ca04f786cce82b0cf/pandas-3.0.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:05f1f1752b8533ea03f7f39a9c15b1a058d067bb48f4748948e7a8691e0510f2", size = 10770558, upload-time = "2026-05-11T18:52:19.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/f1/bbecd2f867b97abebe0f9b53d750f862251b40337e061b36676ded3d920f/pandas-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a1e45c80cceb3b4a21bc5939d52e8cbd8d9b7305309219d59e9754d9ce09e27", size = 11274611, upload-time = "2026-05-11T18:52:22.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/4f/eafabf2d5fae5adf143b4d18d3706c5efdc368a7c4eb1ee8a3eddabbd0f6/pandas-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:14da8316da4d0c5a77618425996bfb1248ca87fc2c1486e6fde4652bd18b5824", size = 11784670, upload-time = "2026-05-11T18:52:25.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/44/1eb20389301b57b19cc099a1c2f662501f72f08a65f912d05822613c1532/pandas-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a55066a0505dae0ba2b50a46637db34b46f9094c65c5d4800794ef6335010938", size = 12353708, upload-time = "2026-05-11T18:52:28.139Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/62/c321f13b5ba1819fc8dca456c7fce578da2dcfecff1abbf0eaddf8406c0f/pandas-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6674ab18ad8c57802867264b00e15e7bb904700cdd9046e3b2fa1fce237439ea", size = 9907609, upload-time = "2026-05-11T18:52:30.982Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/85/1b7f563ebc6357c27233a02a96b589bcce1fa9c6eb89fb4f0e56421d277e/pandas-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:5cc09a68b3120e0f54870dede8287a7bb1fa463907e4fcec1ea77cab6179bf7a", size = 9165596, upload-time = "2026-05-11T18:52:33.334Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/f1/392f8c5bfc16f66a0d2d41561c01627c228fe7ed2a0d056ef11315042570/pandas-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fed2ff7fd9779120e388e285fc029bd5cf9490cdd2e4166a9ee22c0e49a9ab09", size = 10357846, upload-time = "2026-05-11T18:52:36.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/3d/b16412745651e855f357e5e66930248688378853a6e2698a214e331fba1f/pandas-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b168fc218fd80a6cbdbdbc1a97ddc7889ed057d7eb45f50d866ceab5f39904c4", size = 9899550, upload-time = "2026-05-11T18:52:38.976Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/a8/fa2535168fffcedf67f4f6de28d2dd903a747ca7c8ea6989451aaeb3a92f/pandas-3.0.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c", size = 10412965, upload-time = "2026-05-11T18:52:41.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/b6/09b01cdbc15224e2850365192d17b7bdebb8bdbd8780ed221fcdf0d9a515/pandas-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dc0b3fd2169c9157deed50b4d519553a3655c8c6a96027136d654592be973a9", size = 10894600, upload-time = "2026-05-11T18:52:45.02Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/a4/2eb28f2fccb4ced4a2c79ab2a5dee9ade1ebf44922ebad6fea158c9f95d4/pandas-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e65d5407dc0b394f509699650e4a2ec01c0514f21850f453fa60f3be79a5dbf", size = 11422824, upload-time = "2026-05-11T18:52:48.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/45/830bb57f533a4604b355e07edcb8ea18cf88b5f94e5fca92f27052d7c597/pandas-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8894dc474d648fe7b6ff0ca9b0bd73950d19952bc1a6534540762c5d79d305c", size = 11950889, upload-time = "2026-05-11T18:52:50.905Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/c5/fc1b368f303087d20e8c9bf3d6ceb186263cfac0ade735cd938538bea839/pandas-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c7be265b62cef88e253a941e4698604973736dcfe242fdb5198f0f7bc473cdcc", size = 9755463, upload-time = "2026-05-11T18:52:53.386Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/bd/fda8f9705b1b09c6ebe14bfc0fa0e4ec8584d54ea673628f157ff55131af/pandas-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:557409bc4178e70ee8d9ddb494798e51ebf6ea59330f6be22c51bab2a7db6c49", size = 9066158, upload-time = "2026-05-11T18:52:56.038Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa", size = 10331071, upload-time = "2026-05-11T18:52:58.838Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7", size = 9875690, upload-time = "2026-05-11T18:53:01.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8", size = 10381634, upload-time = "2026-05-11T18:53:04.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a", size = 10891243, upload-time = "2026-05-11T18:53:07.643Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb", size = 11388659, upload-time = "2026-05-11T18:53:10.634Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2", size = 11942880, upload-time = "2026-05-11T18:53:13.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44", size = 9757091, upload-time = "2026-05-11T18:53:16.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e", size = 9057282, upload-time = "2026-05-11T18:53:18.768Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d", size = 10767016, upload-time = "2026-05-11T18:53:21.227Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066", size = 10420210, upload-time = "2026-05-11T18:53:23.982Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd", size = 10336126, upload-time = "2026-05-11T18:53:26.731Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085", size = 10728051, upload-time = "2026-05-11T18:53:29.125Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796, upload-time = "2026-05-11T18:53:32.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741, upload-time = "2026-05-11T18:53:34.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958, upload-time = "2026-05-11T18:53:37.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/54/effdcc3c0ff7a08037889200e148ebe94c16c4f653be078c7b3675955df1/pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac", size = 10336065, upload-time = "2026-05-11T18:53:41.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f", size = 9926101, upload-time = "2026-05-11T18:53:43.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/e9/e35cf11c8a136e757b956f5f0efdcaa50aecde85ea055f1898dfc68262f3/pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb", size = 10457553, upload-time = "2026-05-11T18:53:46.394Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a", size = 10914065, upload-time = "2026-05-11T18:53:49.134Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/c2/1ef644445fcd72e3627bceec77e3560636f87ddce4ed841afe76b83b5bf9/pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360", size = 11459188, upload-time = "2026-05-11T18:53:52.527Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/49/4d8d4f42cbc9c4adc7a1870f269c02cbd6cd40d059622c06fb298addcbad/pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76", size = 11982966, upload-time = "2026-05-11T18:53:55.043Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5", size = 9876755, upload-time = "2026-05-11T18:53:58.067Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/af/33c469653b0ba03b50c3a98192d4c07f0c75c66b263ceb097fce0ee97d31/pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977", size = 9198658, upload-time = "2026-05-11T18:54:00.733Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/fa/b8c257bd76b8bd060c3a9151c1fca05e9b9c5e3af5d0f549c0356f6d143d/pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04", size = 10787242, upload-time = "2026-05-11T18:54:03.564Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/eb/f19206ffb0bf1919002969aa448b4702c6594845156a6f8050674855aac3/pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6", size = 10436369, upload-time = "2026-05-11T18:54:06.311Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/24/c7c39fb4fe22b71a0c2d78bf0c585c600092d85f94f086d2b3b2f6ca27e2/pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c", size = 10358306, upload-time = "2026-05-11T18:54:09.085Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/ec/dd2a9eb7fa1204df88c0864164e35b228ac581062ac612ba0a67fd812e4c/pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028", size = 10758394, upload-time = "2026-05-11T18:54:11.956Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/6e/00c61ea8e85b4f6d8d35e11852a1a4998fc7fafc91c6a602d1cc9c972d64/pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d", size = 11375717, upload-time = "2026-05-11T18:54:14.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/89/8fc1c268969fac43688d65fd92e67df24bd128d53cb4d2eee534cd307399/pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a", size = 11828897, upload-time = "2026-05-11T18:54:17.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/3b/e7d20dea247a3e6dc0bd8a6953854afbedc03951def4e7371e05e7263e25/pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1", size = 10900855, upload-time = "2026-05-11T18:54:19.72Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464, upload-time = "2026-05-11T18:54:22.754Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "12.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
@@ -50,37 +599,303 @@ wheels = [
|
||||
[[package]]
|
||||
name = "pygui"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "matplotlib", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "numpy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "pandas", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "pyqtgraph", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "pyserial", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "pyside6", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "scipy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
dev = [
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "pytest", marker = "extra == 'dev'" }]
|
||||
requires-dist = [
|
||||
{ name = "matplotlib", specifier = ">=3.7.0" },
|
||||
{ name = "numpy", specifier = ">=1.24.0" },
|
||||
{ name = "pandas", specifier = ">=2.0.0" },
|
||||
{ name = "pyqtgraph", specifier = ">=0.13.0" },
|
||||
{ name = "pyserial", specifier = ">=3.5" },
|
||||
{ name = "pyside6", specifier = ">=6.6.0" },
|
||||
{ name = "pytest", marker = "extra == 'dev'" },
|
||||
{ name = "pyyaml", specifier = ">=6.0.3" },
|
||||
{ name = "scipy", specifier = ">=1.17.1" },
|
||||
]
|
||||
provides-extras = ["dev"]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "pytest", specifier = ">=9.0.3" }]
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "3.3.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyqtgraph"
|
||||
version = "0.14.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "numpy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/32/36/4c242f81fdcbfa4fb62a5645f6af79191f4097a0577bd5460c24f19cc4ef/pyqtgraph-0.14.0-py3-none-any.whl", hash = "sha256:7abb7c3e17362add64f8711b474dffac5e7b0e9245abdf992e9a44119b7aa4f5", size = 1924755, upload-time = "2025-11-16T19:43:22.251Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyserial"
|
||||
version = "3.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/ae3f0a63f41e4d2f6cb66a5b57197850f919f59e558159a4dd3a818f5082/pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", size = 159125, upload-time = "2020-11-23T03:59:15.045Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585, upload-time = "2020-11-23T03:59:13.41Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyside6"
|
||||
version = "6.11.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyside6-addons", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "pyside6-essentials", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "shiboken6", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/da/a6/27ba5947ed48918f7b74b7c43a1e280aac069e36f25adeb4c9adfac835c4/pyside6-6.11.1-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:537682c3b7530817203e667c1f5a2f00486b37bf52c52eeab438544c7a0917f6", size = 571921, upload-time = "2026-05-13T09:47:36.402Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/de/af89d71410c83b10654d86ff9aff2a4f87c30163658f1cc145242e222526/pyside6-6.11.1-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b1fc521ba2bb5109425ab8add06bddbdd524abcad06cfa012cc39a22a189feb2", size = 572102, upload-time = "2026-05-13T09:47:38.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/0e/d583bd3f7bf5046a4497b36f3902cfb64aa29554489a5a25c18e6b4ac0ac/pyside6-6.11.1-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:75f0005c3eb95c07cfb65522ec50d0815ac007a96482c21dc3cb4b4c04895d84", size = 572098, upload-time = "2026-05-13T09:47:39.44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/f2/d9d8ce1373dabb37e5919f63cd18446556079631d3f2eea3ada03c29f6b8/pyside6-6.11.1-cp310-abi3-win_amd64.whl", hash = "sha256:0968877ab1fb4ef3587a284da6fe05e8647ada56a6a3750b6395188e01f4aba6", size = 578377, upload-time = "2026-05-13T09:47:40.76Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/02/a6057d8bd2bdb1940820fff2d627fdf4013148c9c57adf69fa40d3452ac3/pyside6-6.11.1-cp310-abi3-win_arm64.whl", hash = "sha256:acee467cb5f256cc47ebb9d815a054c1d8416da380c191b247a76d164aa3f805", size = 561765, upload-time = "2026-05-13T09:47:41.9Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyside6-addons"
|
||||
version = "6.11.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyside6-essentials", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "shiboken6", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/6b/8bc94aff48b63f788f2d84e5467c12362d68906ba742c0942f46cb04c879/pyside6_addons-6.11.1-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:54733c77f789bef5f03c6aff4ad3bec8b2eff021f0cfcbc53d5e6c250ded24f9", size = 331714589, upload-time = "2026-05-13T09:39:12.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/62/fb1428a523b2a4541e232aab50d9e789e6b4526f37fd9593452a7ea5b6b3/pyside6_addons-6.11.1-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8e6c65fbd73a512d6f72cda8d8277444a85a34dc99dd1dae9c21d35b8671bb1f", size = 175063224, upload-time = "2026-05-13T09:39:34.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/9b/2ccd52f66db55c06de65d0501170a1935d04d64d0a230c0d892284a02ce3/pyside6_addons-6.11.1-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:bf1c6c4e954e5eba3d2a7c661ad4b9689e8f09c7f4a16bdf29713371d11af993", size = 170553429, upload-time = "2026-05-13T09:39:54.424Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/bd/8adc4d350b3b363f3dfc8fccdcf5bfed25f7e36c2fff30c64e106f4f1572/pyside6_addons-6.11.1-cp310-abi3-win_amd64.whl", hash = "sha256:0d13c4dfd671b050a48e4f8d8ddc724b7248f9c0437e7fc47fdf316278572923", size = 168816308, upload-time = "2026-05-13T09:40:13.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/b7/9a840d97f0f0f04e372a87e205dd30ee285b4e3b021b188459a917c9dc76/pyside6_addons-6.11.1-cp310-abi3-win_arm64.whl", hash = "sha256:3494f480dee92f415be2f2d989c0b3f4755ac332b28045cbf4ba0f5c5a22ba37", size = 35759347, upload-time = "2026-05-13T09:40:21.199Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyside6-essentials"
|
||||
version = "6.11.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "shiboken6", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/da/10d9197e7370eb4fed8df5fc547b7548dec88e5c5949e2d450db4ae96feb/pyside6_essentials-6.11.1-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:228de53c2bc26b07e5021fbe3614fc44ca08e4dab9999af08c2b389d2c239957", size = 110352945, upload-time = "2026-05-13T09:43:08.006Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/49/0e1237c4400bec7e335d2c4eeb49bc40d9fd88a9ac44ca9083ce1abdc308/pyside6_essentials-6.11.1-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:e3ef7027b41e4e55fadb56e3b3257dc8ee92154b639fe67fc4c8e05e9d976c60", size = 79908535, upload-time = "2026-05-13T09:43:24.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/c5/da4c5f23c6540ac5211a1f60177c8dee84b1bf40f2719479587ab8c60731/pyside6_essentials-6.11.1-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:a039b6da68a3a4b9d243217b2b98d475eed3f617159ef6be925badab53c11b0d", size = 78960051, upload-time = "2026-05-13T09:43:35.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/0e/b663ecc96ca57b5c91b83b6615d6b174380b0faf30338125c26e053d6aa7/pyside6_essentials-6.11.1-cp310-abi3-win_amd64.whl", hash = "sha256:63311bd48e32c584599ab04b9ef7c324082374cd2c9fa533f978fb893bb47e40", size = 77549267, upload-time = "2026-05-13T09:43:44.92Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/12/eb6723faf5cb7fa581145da1c15f40d641b96e080f0491af2f1859fdeedb/pyside6_essentials-6.11.1-cp310-abi3-win_arm64.whl", hash = "sha256:11253ea52aabecefe9febddbbe78b43a824129e3af1cec98431028fba7fa954f", size = 57964512, upload-time = "2026-05-13T09:43:52.968Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
{ name = "iniconfig", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "pluggy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
{ name = "pygments", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.9.0.post0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "six", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
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.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scipy"
|
||||
version = "1.17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shiboken6"
|
||||
version = "6.11.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/17/f3/f2b63df0251e7cd3172ea28e32ede52739de9566bcefcd0178681538ac81/shiboken6-6.11.1-cp310-abi3-macosx_13_0_universal2.whl", hash = "sha256:1a16867f103ef1c662a5f09dfed03273a9f81688b174555162c58e83650a3f02", size = 476874, upload-time = "2026-05-13T09:47:01.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/9b/e0355d8897b5c150770f1d95718aad17d432fcc9c035c04f3f58427d4693/shiboken6-6.11.1-cp310-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9a8bccfafc8805254cabcfa1edfaf55cd52889f4998c91ad0d9a4433fb1bcdbe", size = 272222, upload-time = "2026-05-13T09:47:02.653Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/d5/dd4f1defed400be03340f2ede34b61f846776650b4e7ed9ebaf4c71979a2/shiboken6-6.11.1-cp310-abi3-manylinux_2_39_aarch64.whl", hash = "sha256:1bd2f4314414df2d122d9f646e03b731bc6d6b5f77a5f53f99a4fe4e97d84e6f", size = 270350, upload-time = "2026-05-13T09:47:04.02Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/b5/3f6fb2ee65b534193fb4ef713dd619dc31dadff5d12c16979a7699ad58be/shiboken6-6.11.1-cp310-abi3-win_amd64.whl", hash = "sha256:c2c6863aa80ec18c0f82cea3417837b279cdc60024ac17123461dc9042577df7", size = 1223647, upload-time = "2026-05-13T09:47:05.924Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/d1/f15ca0e1666faae02c945f48e745ea35f8fcd8243b176109b4e2c4251f47/shiboken6-6.11.1-cp310-abi3-win_arm64.whl", hash = "sha256:7c8d9af17db4495d4fa5b1c393f218311c4855546b9dfa6a0bd21bcd66b55e9d", size = 1784170, upload-time = "2026-05-13T09:47:07.617Z" },
|
||||
]
|
||||
|
||||
[[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 = "tzdata"
|
||||
version = "2026.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user