Restructure into src/ layout under pygui package

Move all application source under src/pygui/ and rewire imports,
build config, and QML module path to match.
- Relocate backend/, serialcomm/, and the ISC QML module into
  src/pygui/; convert main.py into pygui/__main__.py with a main()
  entry point (run via `python -m pygui` or the new `isc` script)
- Rewrite absolute imports: backend.* -> pygui.backend.*,
  serialcomm.* -> pygui.serialcomm.* (source + tests)
- Move app icons (isc.ico/icns) into packaging/
- Update README and ISC.qmlproject to the new paths
This commit is contained in:
jack
2026-06-03 11:41:45 -07:00
parent af170666e8
commit 9779baa468
34 changed files with 130 additions and 36 deletions
+131
View File
@@ -0,0 +1,131 @@
from pathlib import Path
from typing import Tuple, Optional
from datetime import datetime, timedelta
from pygui.backend.zwafer_models import ZWaferData, Sensor
# ===== Z-Wafer CSV Parser =====
class ZWaferParser:
"""Parses Z-wafer CSV files (header + sensor layout + data rows)."""
# ===== Public Parse API =====
def parse(self, file_path: str) -> Tuple[Optional[ZWaferData], Optional[Path]]:
"""
Parse a Z-wafer file.
Returns:
(ZWaferData, Path) on success
(None, None) on error (e.g., file not found)
"""
try:
path = Path(file_path)
if not path.exists():
raise FileNotFoundError(f"File not found: {file_path}")
with path.open("r", encoding="utf-8") as f:
return self._process_header(f), path
except (ValueError, KeyError):
raise
except Exception:
return None, None
# ===== Header Parsing =====
def _process_header(self, file_obj) -> ZWaferData:
"""Parse header and sensor layout from open file object."""
wafer_data = ZWaferData()
labels: Optional[list] = None
x_coords: Optional[list] = None
y_coords: Optional[list] = None
for line in file_obj:
# Strip trailing comma + whitespace
line = line.rstrip().rstrip(",").strip()
if not line or line.replace(",", "").strip() == "":
continue
parts = [p.strip() for p in line.split(",")]
first_part = parts[0].lower()
# 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 =====
def _parse_header_line(self, wafer_data: ZWaferData, parts: list) -> bool:
"""Parse key=value pairs from header line. Returns True if handled."""
non_empty_parts = [p for p in parts if p]
if not non_empty_parts:
return False
found_kv = False
for part in non_empty_parts:
eq_idx = part.find("=")
if eq_idx < 0:
continue
key = part[:eq_idx].strip()
value = part[eq_idx + 1 :].strip('=" ')
found_kv = True
wafer_data.header[key] = value
# Extract special fields
if key.lower() == "acquisition date":
try:
from datetime import datetime as dt
wafer_data.date = dt.strptime(value, "%m/%d/%Y")
except ValueError:
wafer_data.date = datetime.min # Fallback on parse error
elif key.lower() == "wafer id":
wafer_data.serial = value
if found_kv and wafer_data.date == datetime.min:
wafer_data.date = datetime.min
return found_kv
# ===== Sensor Layout Parsing =====
def _build_sensor_layout(
self,
wafer_data: ZWaferData,
labels: Optional[list],
x_coords: Optional[list],
y_coords: Optional[list],
) -> None:
"""Build sensor list from layout arrays."""
if not all([labels, x_coords, y_coords]):
raise ValueError("Sensor layout section is incomplete or missing.")
if len(labels) != len(x_coords) or len(labels) != len(y_coords):
raise ValueError(
f"Mismatched sensor columns: labels={len(labels)}, "
f"x={len(x_coords)}, y={len(y_coords)}"
)
for i in range(len(labels)):
try:
wafer_data.sensors.append(
Sensor(label=labels[i], x=float(x_coords[i]), y=float(y_coords[i]))
)
except ValueError as e:
raise ValueError(f"Invalid coordinate at index {i}: {e}")