diff --git a/.gitignore b/.gitignore index 0dffe18..b4c8015 100644 --- a/.gitignore +++ b/.gitignore @@ -222,4 +222,8 @@ video/demos/* # UMI specific ignores datasets/* checkpoints/* -demos/* \ No newline at end of file +demos/* + + +workspace/* +*.png diff --git a/AGENTS.md b/AGENTS.md index a30160a..4c12a32 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -144,6 +144,9 @@ Outcome: - Make artifact creation deterministic where possible. - Make errors notebook-friendly and actionable. - Fail fast on required dependencies. If a feature depends on a package that is required by this repo, import it normally and let missing dependencies fail at import time rather than adding deferred runtime guards. Reserve lazy imports or fallback guards for truly optional integrations only. +- Do not wrap calls that rely on required dependencies in local `ModuleNotFoundError` handling. Call them directly and let the required dependency failure surface naturally. +- In `src/opai/presentation/facade.py`, prefer comprehensive public-function implementations over underscore-prefixed helper members. Keep the facade logic visible in the public functions rather than hiding it behind private abstractions unless the file would otherwise become unworkable. +- Prefer comprehensive workflow implementations over tiny underscore-prefixed helpers across the repo, especially in application modules. Do not extract routine 2-5 line steps into private functions unless they carry real domain meaning, are reused meaningfully, or materially reduce complexity. - Do not import types from the `typing` module. Python 3.10+ native annotations are the repo standard. - Prefer built-in generics such as `list[str]`, `dict[str, object]`, `tuple[int, ...]`, and unions like `Path | None`. - When a protocol-style annotation is needed, import it from `collections.abc` instead of `typing`. diff --git a/Makefile b/Makefile index b6ce79f..50fedd3 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,9 @@ run-isort: run-formatter: uv run --extra dev ruff format src tests +launch-jupyterlab: + uv run jupyter lab --notebook-dir=workspace --ip=0.0.0.0 --no-browser + clean: rm -rf .pytest_cache .ruff_cache .venv build dist src/*.egg-info find . -type d -name "__pycache__" -prune -exec rm -rf {} + diff --git a/docs/calibration-workflow.md b/docs/calibration-workflow.md new file mode 100644 index 0000000..f9bcb8c --- /dev/null +++ b/docs/calibration-workflow.md @@ -0,0 +1,237 @@ +# Calibration Workflow With `opai` + +This guide documents the notebook-facing camera calibration workflow in `opai`. +The intended flow is: + +1. Start an `opai` session. +2. Generate a ChArUco board and print it. +3. Record a calibration video outside `opai`. +4. Run calibration from the recorded video. +5. Optionally verify the saved intrinsics. + +The notebook API lives at the package root, so the standard workflow should use `opai.*` directly. + +## What `opai` Provides + +`opai` currently supports these notebook-facing calibration calls: + +```python +ctx = opai.init(name) +board = opai.generate_charuco_board(...) +opai.plot_video_frames(video_path, frame_sample_step=...) +result = opai.calibrate_with_video(...) +verification = opai.verify_calibrated_parameters(...) +``` + +`opai` does not currently record camera video for you. The video capture step must be done with your normal camera app, CLI tool, or another script, then passed back into `opai` by file path. + +## 1. Start A Calibration Session + +Every notebook workflow starts by creating the active context: + +```python +import opai + +ctx = opai.init("camera-calibration") +ctx.session_directory +``` + +This creates or resumes the session directory at: + +```text +.opai_sessions/camera-calibration/ +``` + +All calibration artifacts are written under that directory. If you call `opai.init(...)` again with the same name, `opai` resumes that existing session directory and reuses its manifest. + +## 2. Generate A ChArUco Board + +Generate the board from the notebook and keep the returned config object. That avoids retyping parameters later. + +```python +board = opai.generate_charuco_board( + dictionary="DICT_5X5_100", + squares_x=11, + squares_y=8, + square_length=0.03, + marker_length=0.022, + image_width_px=2000, + image_height_px=1400, + margin_size_px=20, +) + +board.image_path +board.config_path +board.config +``` + +This writes: + +- `charuco_board.png` +- `charuco_config.json` + +Both files are saved into the active session directory. + +### Parameter Notes + +- `dictionary` must be a valid OpenCV ArUco dictionary name such as `DICT_5X5_100`. +- `square_length` and `marker_length` must be positive. +- `marker_length` must be smaller than `square_length`. +- `square_length` and `marker_length` should use a real-world unit consistently. Meters are a reasonable default. + +### Printing Guidance + +- Print the generated board without rescaling it after export. +- Use a flat, rigid backing if possible. +- Measure the printed square size if print scaling is a concern. +- Use the same board for the full calibration run. + +## 3. Record The Calibration Video + +Record the calibration video outside `opai`, then save it somewhere accessible from the notebook, for example: + +```python +video_path = "/path/to/calibration.mp4" +``` + +Recommended capture guidance: + +- Use the same camera, lens, focus mode, and image resolution you plan to use later. +- Move the board through the center, edges, and corners of the image. +- Capture different distances and tilt angles. +- Avoid heavy motion blur and severe occlusion. +- Keep enough frames where the board is fully visible and sharp. + +`opai.calibrate_with_video(...)` samples frames from the saved video, so one continuous handheld calibration clip is enough. + +## 4. Preview Sampled Frames + +Before calibrating, it can help to inspect the frames that `opai` will sample: + +```python +opai.plot_video_frames( + video_path=video_path, + frame_sample_step=15, +) +``` + +Use this to check whether the chosen sampling step keeps enough diverse board poses. `frame_sample_step` must be greater than `0`. + +## 5. Run Calibration From The Video + +The safest pattern is to reuse the values from `board.config` so the calibration inputs stay consistent with the generated board: + +```python +result = opai.calibrate_with_video( + video_path=video_path, + frame_sample_step=15, + row_count=board.config.squares_y, + col_count=board.config.squares_x, + square_length=board.config.square_length, + marker_length=board.config.marker_length, + dictionary=board.config.dictionary, + plot_result=True, +) + +result +``` + +Important mapping: + +- `row_count` corresponds to `squares_y` +- `col_count` corresponds to `squares_x` + +This workflow: + +- samples frames from the video +- detects ChArUco corners +- calibrates a fisheye camera model +- writes `calibration.json` into the session directory + +If `plot_result=True`, `opai` also plots the detected ChArUco corners on the frames that were kept for calibration. + +## 6. Optional Verification + +After calibration, you can verify the saved intrinsics against a calibration video: + +```python +verification = opai.verify_calibrated_parameters( + video_path=video_path, + n_check_imgs=10, + charuco_config_json="charuco_config.json", + intrinsics_json="calibration.json", + plot_result=True, +) + +verification +``` + +For verification, relative JSON paths are resolved against the active session directory first. This makes the saved session artifacts convenient to reuse directly from notebook cells. + +Verification writes: + +- `calibration_verification.json` + +## Session Artifacts + +After a typical calibration workflow, the session directory will contain files like: + +```text +.opai_sessions/camera-calibration/ +├── session.json +├── charuco_board.png +├── charuco_config.json +├── calibration.json +└── calibration_verification.json +``` + +`calibration_verification.json` is only created if you run verification. + +## Complete Notebook Example + +```python +import opai + +ctx = opai.init("camera-calibration") + +board = opai.generate_charuco_board( + dictionary="DICT_5X5_100", + squares_x=11, + squares_y=8, + square_length=0.03, + marker_length=0.022, +) + +video_path = "/path/to/calibration.mp4" + +opai.plot_video_frames(video_path=video_path, frame_sample_step=15) + +result = opai.calibrate_with_video( + video_path=video_path, + frame_sample_step=15, + row_count=board.config.squares_y, + col_count=board.config.squares_x, + square_length=board.config.square_length, + marker_length=board.config.marker_length, + dictionary=board.config.dictionary, + plot_result=True, +) + +verification = opai.verify_calibrated_parameters( + video_path=video_path, + n_check_imgs=10, + charuco_config_json="charuco_config.json", + intrinsics_json="calibration.json", + plot_result=True, +) +``` + +## Common Failure Cases + +- Calling calibration functions before `opai.init(...)`. +- Passing board dimensions that do not match the generated board. +- Using a different dictionary, `square_length`, or `marker_length` than the printed board. +- Sampling a video that contains too few clear ChArUco detections. +- Using verification video frames whose image size does not match the saved intrinsics. + +If you already have frames in memory instead of a video file, you can call `opai.calibrate(...)` directly with a sequence of `numpy.ndarray` frames, but the board parameters still need to match the printed ChArUco board. diff --git a/pyproject.toml b/pyproject.toml index a7ec117..d9b92fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,9 @@ authors = [ requires-python = ">=3.10" dependencies = [ "jupyterlab>=4.5.6", - "opencv-contrib-python>=4.13.0.92", + "matplotlib>=3.10.7", + "numpy>=2.2.6", + "opencv-contrib-python-headless>=4.13.0.92", "rich>=14.1.0", ] diff --git a/src/opai/__init__.py b/src/opai/__init__.py index 0caa38d..139a4ee 100644 --- a/src/opai/__init__.py +++ b/src/opai/__init__.py @@ -3,10 +3,14 @@ add_mapping, browse_session, calibrate, + calibrate_with_video, + generate_charuco_board, get_context, init, list_sessions, main, + plot_video_frames, + verify_calibrated_parameters, ) __all__ = [ @@ -14,8 +18,12 @@ "add_mapping", "browse_session", "calibrate", + "calibrate_with_video", + "generate_charuco_board", "get_context", "init", "list_sessions", "main", + "plot_video_frames", + "verify_calibrated_parameters", ] diff --git a/src/opai/application/__init__.py b/src/opai/application/__init__.py index 6bc7c4b..4348f3e 100644 --- a/src/opai/application/__init__.py +++ b/src/opai/application/__init__.py @@ -1,4 +1,8 @@ -from opai.application.calibration import calibrate +from opai.application.calibration import ( + calibrate, + generate_charuco_board, + verify_calibrated_parameters, +) from opai.application.session import ( add_demos, add_mapping, @@ -6,4 +10,12 @@ list_sessions, ) -__all__ = ["add_demos", "add_mapping", "browse_session", "calibrate", "list_sessions"] +__all__ = [ + "add_demos", + "add_mapping", + "browse_session", + "calibrate", + "generate_charuco_board", + "list_sessions", + "verify_calibrated_parameters", +] diff --git a/src/opai/application/calibration.py b/src/opai/application/calibration.py index c10e807..ac096a4 100644 --- a/src/opai/application/calibration.py +++ b/src/opai/application/calibration.py @@ -1,14 +1,37 @@ from __future__ import annotations +import json from collections.abc import Sequence +from pathlib import Path import cv2 import numpy as np -from opai.core.exceptions import OPAIValidationError, OPAIWorkflowError -from opai.domain.calibration import CalibrationIntrinsics, CalibrationResult +from opai.core.exceptions import ( + OPAIDependencyError, + OPAIValidationError, + OPAIWorkflowError, +) +from opai.domain.calibration import ( + CalibrationIntrinsics, + CalibrationResult, + CalibrationVerificationFrame, + CalibrationVerificationResult, + CharucoBoardArtifacts, + CharucoBoardConfig, + validate_charuco_board_config, +) from opai.domain.context import Context -from opai.infrastructure.persistence import write_calibration_result +from opai.domain.plot import plot_frames +from opai.infrastructure.persistence import ( + write_calibration_result, + write_calibration_verification_result, + write_charuco_board_config, + write_charuco_board_image, +) +from opai.infrastructure.video import ( + sample_video_frames as sample_video_frames_from_path, +) def calibrate( @@ -19,6 +42,9 @@ def calibrate( square_length: float, marker_length: float, dictionary: str, + plot_result: bool = False, + plot_nrows: int | None = None, + plot_ncols: int | None = None, ) -> CalibrationResult: _validate_inputs( frames=frames, @@ -28,37 +54,45 @@ def calibrate( marker_length=marker_length, ) - aruco_dictionary = _resolve_dictionary(dictionary) + dictionary_id = getattr(cv2.aruco, dictionary, None) + if dictionary_id is None: + raise OPAIValidationError(f"Unsupported ArUco dictionary: {dictionary}") + aruco_dictionary = cv2.aruco.getPredefinedDictionary(dictionary_id) board = cv2.aruco.CharucoBoard( - (col_count, row_count), - square_length, - marker_length, - aruco_dictionary, + (col_count, row_count), square_length, marker_length, aruco_dictionary ) + charuco_detector = cv2.aruco.CharucoDetector(board) - image_height, image_width = _get_frame_size(frames) + image_height, image_width = (int(value) for value in frames[0].shape[:2]) image_size = (image_width, image_height) all_charuco_corners: list[np.ndarray] = [] all_charuco_ids: list[np.ndarray] = [] + detected_corner_frames: list[np.ndarray] = [] for frame in frames: - grayscale = _to_grayscale(frame) - corners, ids, _ = cv2.aruco.detectMarkers(grayscale, aruco_dictionary) - if ids is None or len(ids) == 0: + grayscale = ( + frame if frame.ndim == 2 else cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + ) + charuco_corners, charuco_ids, _, _ = charuco_detector.detectBoard(grayscale) + + if charuco_ids is None or charuco_corners is None: continue - _, charuco_corners, charuco_ids = cv2.aruco.interpolateCornersCharuco( - markerCorners=corners, - markerIds=ids, - image=grayscale, - board=board, - ) - if charuco_ids is None or charuco_corners is None or len(charuco_ids) < 4: + if int(charuco_ids.size) < 4: continue all_charuco_corners.append(charuco_corners) all_charuco_ids.append(charuco_ids) + plotted_frame = frame.copy() + if plotted_frame.ndim == 2: + plotted_frame = cv2.cvtColor(plotted_frame, cv2.COLOR_GRAY2BGR) + cv2.aruco.drawDetectedCornersCharuco( + image=plotted_frame, + charucoCorners=charuco_corners, + charucoIds=charuco_ids, + ) + detected_corner_frames.append(plotted_frame) if not all_charuco_corners: raise OPAIWorkflowError( @@ -66,13 +100,32 @@ def calibrate( "Verify the board parameters and frame content." ) - ret, camera_matrix, dist_coeffs, rvecs, tvecs = cv2.aruco.calibrateCameraCharuco( - charucoCorners=all_charuco_corners, - charucoIds=all_charuco_ids, + if plot_result: + try: + plot_frames( + detected_corner_frames, + nrows=plot_nrows, + ncols=plot_ncols, + frames_are_bgr=True, + ) + except ModuleNotFoundError as exc: + raise OPAIDependencyError( + "Calibration plotting requires the 'matplotlib' package. Install " + "project dependencies before calling opai.calibrate(...) or " + "opai.calibrate_with_video(...)." + ) from exc + except ValueError as exc: + raise OPAIValidationError(str(exc)) from exc + + object_points, image_points = _build_fisheye_calibration_points( board=board, - imageSize=image_size, - cameraMatrix=None, - distCoeffs=None, + charuco_corners=all_charuco_corners, + charuco_ids=all_charuco_ids, + ) + ret, camera_matrix, dist_coeffs, rvecs, tvecs = _calibrate_fisheye( + object_points=object_points, + image_points=image_points, + image_size=image_size, ) mse_reproj_error = _compute_mse_reprojection_error( @@ -100,6 +153,482 @@ def calibrate( return result +def generate_charuco_board( + ctx: Context, + config: CharucoBoardConfig, +) -> CharucoBoardArtifacts: + validate_charuco_board_config(config) + normalized_config = CharucoBoardConfig( + dictionary=config.dictionary.strip(), + squares_x=config.squares_x, + squares_y=config.squares_y, + square_length=config.square_length, + marker_length=config.marker_length, + image_width_px=config.image_width_px, + image_height_px=config.image_height_px, + margin_size_px=config.margin_size_px, + ) + + dictionary_id = getattr(cv2.aruco, normalized_config.dictionary, None) + if dictionary_id is None: + raise OPAIValidationError( + f"Unsupported ArUco dictionary: {normalized_config.dictionary}" + ) + aruco_dictionary = cv2.aruco.getPredefinedDictionary(dictionary_id) + board = cv2.aruco.CharucoBoard( + (normalized_config.squares_x, normalized_config.squares_y), + normalized_config.square_length, + normalized_config.marker_length, + aruco_dictionary, + ) + board_image = board.generateImage( + (normalized_config.image_width_px, normalized_config.image_height_px), + marginSize=normalized_config.margin_size_px, + ) + + image_path = write_charuco_board_image(ctx.session_directory, board_image) + config_path = write_charuco_board_config( + ctx.session_directory, + normalized_config, + board_image_path=image_path.name, + ) + return CharucoBoardArtifacts( + image_path=image_path, + config_path=config_path, + config=normalized_config, + ) + + +def verify_calibrated_parameters( + ctx: Context, + video_path: str | Path, + n_check_imgs: int, + charuco_config_json: str | Path | dict[str, object], + intrinsics_json: str | Path | dict[str, object], + plot_result: bool = False, + plot_nrows: int | None = None, + plot_ncols: int | None = None, +) -> CalibrationVerificationResult: + if not isinstance(n_check_imgs, int) or isinstance(n_check_imgs, bool): + raise OPAIValidationError( + "n_check_imgs must be an integer greater than 0.", + details={"n_check_imgs": str(n_check_imgs)}, + ) + if n_check_imgs <= 0: + raise OPAIValidationError( + "n_check_imgs must be greater than 0.", + details={"n_check_imgs": n_check_imgs}, + ) + + all_frames = sample_video_frames(video_path=video_path, frame_sample_step=10) + check_image_count = min(n_check_imgs, len(all_frames)) + sampled_frame_indices = tuple( + int(index) + for index in np.linspace( + 0, + len(all_frames) - 1, + num=check_image_count, + dtype=int, + ) + ) + sampled_frames = tuple(all_frames[index] for index in sampled_frame_indices) + + charuco_payload = _load_json_payload( + ctx=ctx, + payload_or_path=charuco_config_json, + payload_name="charuco_config_json", + ) + intrinsics_payload = _load_json_payload( + ctx=ctx, + payload_or_path=intrinsics_json, + payload_name="intrinsics_json", + ) + + charuco_config = _build_charuco_board_config_from_payload(charuco_payload) + validate_charuco_board_config(charuco_config) + camera_matrix, dist_coeffs, intrinsics_image_size = ( + _build_fisheye_parameters_from_payload(intrinsics_payload) + ) + + image_height, image_width = (int(value) for value in sampled_frames[0].shape[:2]) + for frame in sampled_frames[1:]: + if frame.shape[:2] != (image_height, image_width): + raise OPAIWorkflowError( + "Intrinsics verification failed: sampled frames have inconsistent image dimensions." + ) + + if ( + intrinsics_image_size is not None + and ( + image_width, + image_height, + ) + != intrinsics_image_size + ): + raise OPAIValidationError( + "Calibration intrinsics image size does not match the verification video frames.", + details={ + "intrinsics_image_width": intrinsics_image_size[0], + "intrinsics_image_height": intrinsics_image_size[1], + "video_image_width": image_width, + "video_image_height": image_height, + }, + ) + + dictionary_id = getattr(cv2.aruco, charuco_config.dictionary, None) + if dictionary_id is None: + raise OPAIValidationError( + f"Unsupported ArUco dictionary: {charuco_config.dictionary}" + ) + + aruco_dictionary = cv2.aruco.getPredefinedDictionary(dictionary_id) + board = cv2.aruco.CharucoBoard( + (charuco_config.squares_x, charuco_config.squares_y), + charuco_config.square_length, + charuco_config.marker_length, + aruco_dictionary, + ) + charuco_detector = cv2.aruco.CharucoDetector(board) + + board_points = np.asarray(board.getChessboardCorners(), dtype=np.float64) + frame_results: list[CalibrationVerificationFrame] = [] + verification_frames: list[np.ndarray] = [] + total_squared_error_sum = 0.0 + total_corner_count = 0 + + for sampled_frame_index, frame in zip(sampled_frame_indices, sampled_frames): + grayscale = ( + frame if frame.ndim == 2 else cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + ) + charuco_corners, charuco_ids, _, _ = charuco_detector.detectBoard(grayscale) + if charuco_ids is None or charuco_corners is None: + continue + + observed_ids = np.asarray(charuco_ids, dtype=np.int32).reshape(-1) + if observed_ids.size < 4: + continue + + if np.any(observed_ids < 0) or np.any(observed_ids >= board_points.shape[0]): + raise OPAIWorkflowError( + "Intrinsics verification failed: detected ChArUco ids are outside the configured board range." + ) + + object_points = board_points[observed_ids].reshape(-1, 1, 3) + image_points = np.asarray(charuco_corners, dtype=np.float64).reshape(-1, 1, 2) + + pose_found, rvec, tvec = cv2.solvePnP( + object_points, + image_points, + camera_matrix, + dist_coeffs, + ) + if not pose_found: + continue + + reprojected_points, _ = cv2.fisheye.projectPoints( + object_points, + rvec, + tvec, + camera_matrix, + dist_coeffs, + ) + detected_points = image_points.reshape(-1, 2) + projected_points = reprojected_points.reshape(-1, 2) + deltas = projected_points - detected_points + squared_error_sum = float(np.sum(deltas * deltas)) + corner_count = int(deltas.shape[0]) + total_squared_error_sum += squared_error_sum + total_corner_count += corner_count + verification_frames.append( + _draw_calibration_verification_overlay( + frame=frame, + detected_points=detected_points, + reprojected_points=projected_points, + ) + ) + frame_results.append( + CalibrationVerificationFrame( + sampled_frame_index=int(sampled_frame_index), + detected_corner_count=corner_count, + mse_reproj_error=squared_error_sum / corner_count, + ) + ) + + if not frame_results or total_corner_count == 0: + raise OPAIWorkflowError( + "Intrinsics verification failed: no sampled frames produced a valid ChArUco pose." + ) + + if plot_result: + try: + plot_frames( + verification_frames, + nrows=plot_nrows, + ncols=plot_ncols, + frames_are_bgr=True, + ) + except ValueError as exc: + raise OPAIValidationError(str(exc)) from exc + + result = CalibrationVerificationResult( + requested_check_image_count=int(n_check_imgs), + sampled_image_count=int(check_image_count), + verified_image_count=len(frame_results), + skipped_image_count=int(check_image_count - len(frame_results)), + total_detected_corner_count=total_corner_count, + mse_reproj_error=total_squared_error_sum / total_corner_count, + frame_results=tuple(frame_results), + ) + write_calibration_verification_result(ctx.session_directory, result) + return result + + +def _draw_calibration_verification_overlay( + frame: np.ndarray, + detected_points: np.ndarray, + reprojected_points: np.ndarray, +) -> np.ndarray: + overlay = np.ascontiguousarray(frame.copy()) + if overlay.ndim == 2: + overlay = cv2.cvtColor(overlay, cv2.COLOR_GRAY2BGR) + + for detected_point, reprojected_point in zip(detected_points, reprojected_points): + detected_pixel = tuple(int(round(value)) for value in detected_point) + reprojected_pixel = tuple(int(round(value)) for value in reprojected_point) + cv2.arrowedLine( + overlay, + detected_pixel, + reprojected_pixel, + (0, 255, 255), + 1, + tipLength=0.2, + ) + cv2.circle( + overlay, + detected_pixel, + 5, + (0, 255, 0), + 2, + ) + cv2.circle( + overlay, + reprojected_pixel, + 3, + (0, 0, 255), + -1, + ) + return overlay + + +def sample_video_frames( + video_path: str | Path, + frame_sample_step: int, +) -> tuple[np.ndarray, ...]: + path = Path(video_path).expanduser() + if not path.exists(): + raise OPAIValidationError( + f"Calibration video does not exist: {path}", + details={"path": str(path)}, + ) + if not path.is_file(): + raise OPAIValidationError( + f"Calibration video path must point to a file: {path}", + details={"path": str(path)}, + ) + if frame_sample_step <= 0: + raise OPAIValidationError( + "frame_sample_step must be greater than 0.", + details={"frame_sample_step": frame_sample_step}, + ) + + frames = sample_video_frames_from_path(path, frame_sample_step) + if not frames: + raise OPAIWorkflowError( + "Calibration failed: video sampling produced no frames.", + details={ + "path": str(path), + "frame_sample_step": frame_sample_step, + }, + ) + return frames + + +def _load_json_payload( + ctx: Context, + payload_or_path: str | Path | dict[str, object], + payload_name: str, +) -> dict[str, object]: + if isinstance(payload_or_path, dict): + return payload_or_path + + if not isinstance(payload_or_path, (str, Path)): + raise OPAIValidationError( + f"{payload_name} must be a dict payload or a JSON file path.", + details={"payload_name": payload_name}, + ) + + raw_path = Path(payload_or_path).expanduser() + candidate_paths = ( + (raw_path,) + if raw_path.is_absolute() + else (ctx.session_directory / raw_path, raw_path) + ) + + resolved_path: Path | None = None + for candidate_path in candidate_paths: + if candidate_path.exists(): + resolved_path = candidate_path + break + + if resolved_path is None: + raise OPAIValidationError( + f"{payload_name} JSON file was not found.", + details={ + "payload_name": payload_name, + "path": str(raw_path), + }, + ) + + try: + payload = json.loads(resolved_path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise OPAIValidationError( + f"{payload_name} must be valid JSON.", + details={ + "payload_name": payload_name, + "path": str(resolved_path), + }, + ) from exc + if not isinstance(payload, dict): + raise OPAIValidationError( + f"{payload_name} must decode to a JSON object.", + details={ + "payload_name": payload_name, + "path": str(resolved_path), + }, + ) + return payload + + +def _build_charuco_board_config_from_payload( + payload: dict[str, object], +) -> CharucoBoardConfig: + dictionary = payload.get("dictionary") + if not isinstance(dictionary, str): + raise OPAIValidationError( + "charuco_config_json is missing a valid dictionary field.", + details={"field": "dictionary"}, + ) + + try: + return CharucoBoardConfig( + dictionary=dictionary.strip(), + squares_x=int(payload["squares_x"]), + squares_y=int(payload["squares_y"]), + square_length=float(payload["square_length"]), + marker_length=float(payload["marker_length"]), + image_width_px=int(payload["image_width_px"]), + image_height_px=int(payload["image_height_px"]), + margin_size_px=int(payload["margin_size_px"]), + ) + except KeyError as exc: + raise OPAIValidationError( + "charuco_config_json is missing required ChArUco fields.", + details={"field": str(exc)}, + ) from exc + except (TypeError, ValueError) as exc: + raise OPAIValidationError( + "charuco_config_json contains invalid ChArUco field values.", + details={"error": str(exc)}, + ) from exc + + +def _build_fisheye_parameters_from_payload( + payload: dict[str, object], +) -> tuple[np.ndarray, np.ndarray, tuple[int, int] | None]: + intrinsics_payload = payload.get("intrinsics") + if not isinstance(intrinsics_payload, dict): + raise OPAIValidationError( + "intrinsics_json must include an intrinsics object.", + details={"field": "intrinsics"}, + ) + + try: + focal_length = float(intrinsics_payload["focal_length"]) + aspect_ratio = float(intrinsics_payload["aspect_ratio"]) + principal_pt_x = float(intrinsics_payload["principal_pt_x"]) + principal_pt_y = float(intrinsics_payload["principal_pt_y"]) + skew = float(intrinsics_payload["skew"]) + radial_distortion_1 = float(intrinsics_payload["radial_distortion_1"]) + radial_distortion_2 = float(intrinsics_payload["radial_distortion_2"]) + radial_distortion_3 = float(intrinsics_payload["radial_distortion_3"]) + radial_distortion_4 = float(intrinsics_payload["radial_distortion_4"]) + except KeyError as exc: + raise OPAIValidationError( + "intrinsics_json is missing required intrinsic fields.", + details={"field": str(exc)}, + ) from exc + except (TypeError, ValueError) as exc: + raise OPAIValidationError( + "intrinsics_json contains invalid intrinsic field values.", + details={"error": str(exc)}, + ) from exc + + values_to_check = [ + focal_length, + aspect_ratio, + principal_pt_x, + principal_pt_y, + skew, + radial_distortion_1, + radial_distortion_2, + radial_distortion_3, + radial_distortion_4, + ] + if not all(np.isfinite(value) for value in values_to_check): + raise OPAIValidationError("intrinsics_json contains non-finite numeric values.") + if focal_length <= 0.0: + raise OPAIValidationError("intrinsics focal_length must be positive.") + if aspect_ratio <= 0.0: + raise OPAIValidationError("intrinsics aspect_ratio must be positive.") + + focal_length_y = focal_length / aspect_ratio + camera_matrix = np.array( + [ + [focal_length, skew, principal_pt_x], + [0.0, focal_length_y, principal_pt_y], + [0.0, 0.0, 1.0], + ], + dtype=np.float64, + ) + dist_coeffs = np.array( + [ + [radial_distortion_1], + [radial_distortion_2], + [radial_distortion_3], + [radial_distortion_4], + ], + dtype=np.float64, + ) + + if "image_width" not in payload or "image_height" not in payload: + return camera_matrix, dist_coeffs, None + + try: + image_width = int(payload["image_width"]) + image_height = int(payload["image_height"]) + except (TypeError, ValueError) as exc: + raise OPAIValidationError( + "intrinsics_json image_width and image_height must be integers.", + details={"error": str(exc)}, + ) from exc + if image_width <= 0 or image_height <= 0: + raise OPAIValidationError( + "intrinsics_json image_width and image_height must be positive." + ) + return camera_matrix, dist_coeffs, (image_width, image_height) + + def _validate_inputs( frames: Sequence[np.ndarray], row_count: int, @@ -126,22 +655,67 @@ def _validate_inputs( ) -def _resolve_dictionary(name: str) -> cv2.aruco.Dictionary: - dictionary_id = getattr(cv2.aruco, name, None) - if dictionary_id is None: - raise OPAIValidationError(f"Unsupported ArUco dictionary: {name}") - return cv2.aruco.getPredefinedDictionary(dictionary_id) +def _build_fisheye_calibration_points( + board: cv2.aruco.CharucoBoard, + charuco_corners: Sequence[np.ndarray], + charuco_ids: Sequence[np.ndarray], +) -> tuple[tuple[np.ndarray, ...], tuple[np.ndarray, ...]]: + if len(charuco_corners) != len(charuco_ids): + raise OPAIWorkflowError( + "Calibration failed: inconsistent ChArUco observation lengths." + ) + chessboard_corners = np.asarray(board.getChessboardCorners(), dtype=np.float64) + object_points: list[np.ndarray] = [] + image_points: list[np.ndarray] = [] -def _get_frame_size(frames: Sequence[np.ndarray]) -> tuple[int, int]: - height, width = frames[0].shape[:2] - return int(height), int(width) + for observed_corners, observed_ids in zip(charuco_corners, charuco_ids): + object_points.append( + chessboard_corners[observed_ids.flatten()].reshape(-1, 1, 3) + ) + image_points.append( + np.asarray(observed_corners, dtype=np.float64).reshape(-1, 1, 2) + ) + if not object_points: + raise OPAIWorkflowError( + "Calibration failed: no ChArUco observations available for fisheye calibration." + ) -def _to_grayscale(frame: np.ndarray) -> np.ndarray: - if frame.ndim == 2: - return frame - return cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + return tuple(object_points), tuple(image_points) + + +def _calibrate_fisheye( + object_points: Sequence[np.ndarray], + image_points: Sequence[np.ndarray], + image_size: tuple[int, int], +) -> tuple[ + float, np.ndarray, np.ndarray, tuple[np.ndarray, ...], tuple[np.ndarray, ...] +]: + ret, camera_matrix, dist_coeffs, rvecs, tvecs = cv2.fisheye.calibrate( + object_points, + image_points, + image_size, + np.eye(3, dtype=np.float64), + np.zeros((4, 1), dtype=np.float64), + flags=( + cv2.fisheye.CALIB_RECOMPUTE_EXTRINSIC + | cv2.fisheye.CALIB_CHECK_COND + | cv2.fisheye.CALIB_FIX_SKEW + ), + criteria=( + cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, + 100, + 1e-6, + ), + ) + return ( + float(ret), + np.asarray(camera_matrix, dtype=np.float64), + np.asarray(dist_coeffs, dtype=np.float64), + tuple(rvecs), + tuple(tvecs), + ) def _compute_mse_reprojection_error( @@ -169,12 +743,12 @@ def _compute_mse_reprojection_error( tvecs, ): object_points = chessboard_corners[observed_ids.flatten()].reshape(-1, 1, 3) - projected_corners, _ = cv2.projectPoints( - objectPoints=object_points, - rvec=rvec, - tvec=tvec, - cameraMatrix=camera_matrix, - distCoeffs=dist_coeffs, + projected_corners, _ = cv2.fisheye.projectPoints( + object_points, + rvec, + tvec, + camera_matrix, + dist_coeffs, ) deltas = projected_corners.reshape(-1, 2) - observed_corners.reshape(-1, 2) squared_error_sum += float(np.sum(deltas * deltas)) diff --git a/src/opai/application/session.py b/src/opai/application/session.py index 62fbcd0..66ef4b5 100644 --- a/src/opai/application/session.py +++ b/src/opai/application/session.py @@ -1,25 +1,60 @@ from __future__ import annotations from collections.abc import Iterable +from dataclasses import dataclass from pathlib import Path from opai.core.exceptions import OPAIValidationError from opai.domain.context import Context from opai.domain.session import DemoAsset, SessionManifest from opai.infrastructure.context_store import ( + SESSION_MANIFEST_FILENAME, + SESSION_ROOT_DIRNAME, + get_active_context, get_session_directory, list_session_names, load_manifest_for_context, persist_manifest_for_context, + session_root, ) from opai.infrastructure.persistence import ( build_file_tree, copy_demo_assets, copy_mapping_asset, list_relative_paths, + load_session_manifest, ) +@dataclass(frozen=True) +class SessionSummary: + name: str + demo_count: int + has_mapping: bool + file_count: int + is_active: bool + + +@dataclass(frozen=True) +class SessionCatalog: + root_dirname: str + root_path: Path + sessions: tuple[SessionSummary, ...] + + +@dataclass(frozen=True) +class SessionBrowseView: + root_dirname: str + root_path: Path + session_name: str + session_path: Path + demo_count: int + has_mapping: bool + file_count: int + file_paths: tuple[str, ...] + tree_payload: dict[str, dict] + + def add_demos(ctx: Context, video_paths: Iterable[str | Path]) -> tuple[DemoAsset, ...]: source_paths = _normalize_video_paths(video_paths, label="demo") manifest = load_manifest_for_context(ctx) @@ -51,10 +86,29 @@ def add_mapping(ctx: Context, video_path: str | Path) -> MappingAsset: def list_sessions() -> list[str]: - return list_session_names() + return [session.name for session in describe_sessions().sessions] def browse_session(name: str) -> tuple[list[str], dict[str, dict]]: + view = describe_session(name) + return list(view.file_paths), view.tree_payload + + +def describe_sessions() -> SessionCatalog: + active_context = get_active_context() + active_name = active_context.name if active_context is not None else None + summaries = tuple( + _build_session_summary(session_name, active_name=active_name) + for session_name in list_session_names() + ) + return SessionCatalog( + root_dirname=SESSION_ROOT_DIRNAME, + root_path=session_root(), + sessions=summaries, + ) + + +def describe_session(name: str) -> SessionBrowseView: session_name = _normalize_session_name(name) session_directory = get_session_directory(session_name) if not session_directory.exists(): @@ -62,7 +116,22 @@ def browse_session(name: str) -> tuple[list[str], dict[str, dict]]: f"Session '{session_name}' does not exist.", details={"session_name": session_name}, ) - return list_relative_paths(session_directory), build_file_tree(session_directory) + manifest = load_session_manifest( + session_directory / SESSION_MANIFEST_FILENAME, + session_name=session_name, + ) + file_paths = tuple(list_relative_paths(session_directory)) + return SessionBrowseView( + root_dirname=SESSION_ROOT_DIRNAME, + root_path=session_root(), + session_name=session_name, + session_path=session_directory, + demo_count=len(manifest.demos), + has_mapping=manifest.mapping is not None, + file_count=len(file_paths), + file_paths=file_paths, + tree_payload=build_file_tree(session_directory), + ) def _normalize_video_paths( @@ -104,3 +173,22 @@ def _normalize_session_name(name: str) -> str: if not isinstance(name, str) or not name.strip(): raise OPAIValidationError("Session name must be a non-empty string.") return name.strip() + + +def _build_session_summary( + session_name: str, + *, + active_name: str | None, +) -> SessionSummary: + session_directory = get_session_directory(session_name) + manifest = load_session_manifest( + session_directory / SESSION_MANIFEST_FILENAME, + session_name=session_name, + ) + return SessionSummary( + name=session_name, + demo_count=len(manifest.demos), + has_mapping=manifest.mapping is not None, + file_count=len(list_relative_paths(session_directory)), + is_active=session_name == active_name, + ) diff --git a/src/opai/domain/__init__.py b/src/opai/domain/__init__.py index 3857468..1acb649 100644 --- a/src/opai/domain/__init__.py +++ b/src/opai/domain/__init__.py @@ -1,9 +1,19 @@ -from opai.domain.calibration import CalibrationResult +from opai.domain.calibration import ( + CalibrationResult, + CalibrationVerificationFrame, + CalibrationVerificationResult, + CharucoBoardArtifacts, + CharucoBoardConfig, +) from opai.domain.context import Context from opai.domain.session import DemoAsset, MappingAsset, SessionManifest __all__ = [ "CalibrationResult", + "CalibrationVerificationFrame", + "CalibrationVerificationResult", + "CharucoBoardArtifacts", + "CharucoBoardConfig", "Context", "DemoAsset", "MappingAsset", diff --git a/src/opai/domain/calibration.py b/src/opai/domain/calibration.py index b0b4ffa..b8679c1 100644 --- a/src/opai/domain/calibration.py +++ b/src/opai/domain/calibration.py @@ -1,9 +1,21 @@ from __future__ import annotations from dataclasses import dataclass +from pathlib import Path import numpy as np +from opai.core.exceptions import OPAIValidationError + +DEFAULT_CHARUCO_DICTIONARY = "DICT_5X5_100" +DEFAULT_CHARUCO_SQUARES_X = 11 +DEFAULT_CHARUCO_SQUARES_Y = 8 +DEFAULT_CHARUCO_SQUARE_LENGTH = 0.03 +DEFAULT_CHARUCO_MARKER_LENGTH = 0.022 +DEFAULT_CHARUCO_IMAGE_WIDTH_PX = 2000 +DEFAULT_CHARUCO_IMAGE_HEIGHT_PX = 1400 +DEFAULT_CHARUCO_MARGIN_SIZE_PX = 20 + @dataclass class CalibrationIntrinsics: @@ -27,3 +39,85 @@ class CalibrationResult: intrinsics: CalibrationIntrinsics camera_matrix: np.ndarray dist_coeffs: np.ndarray + + +@dataclass(frozen=True) +class CalibrationVerificationFrame: + sampled_frame_index: int + detected_corner_count: int + mse_reproj_error: float + + +@dataclass(frozen=True) +class CalibrationVerificationResult: + requested_check_image_count: int + sampled_image_count: int + verified_image_count: int + skipped_image_count: int + total_detected_corner_count: int + mse_reproj_error: float + frame_results: tuple[CalibrationVerificationFrame, ...] + + +@dataclass(frozen=True) +class CharucoBoardConfig: + dictionary: str + squares_x: int + squares_y: int + square_length: float + marker_length: float + image_width_px: int + image_height_px: int + margin_size_px: int + + +@dataclass(frozen=True) +class CharucoBoardArtifacts: + image_path: Path + config_path: Path + config: CharucoBoardConfig + + +def validate_charuco_board_config(config: CharucoBoardConfig) -> None: + if not isinstance(config.dictionary, str) or not config.dictionary.strip(): + raise OPAIValidationError( + "dictionary must be a non-empty ArUco dictionary name.", + details={"dictionary": config.dictionary}, + ) + if config.squares_x <= 1 or config.squares_y <= 1: + raise OPAIValidationError( + "squares_x and squares_y must both be greater than 1.", + details={ + "squares_x": config.squares_x, + "squares_y": config.squares_y, + }, + ) + if config.square_length <= 0 or config.marker_length <= 0: + raise OPAIValidationError( + "square_length and marker_length must both be positive.", + details={ + "square_length": config.square_length, + "marker_length": config.marker_length, + }, + ) + if config.marker_length >= config.square_length: + raise OPAIValidationError( + "marker_length must be smaller than square_length.", + details={ + "square_length": config.square_length, + "marker_length": config.marker_length, + }, + ) + if config.image_width_px <= 0 or config.image_height_px <= 0: + raise OPAIValidationError( + "image_width_px and image_height_px must both be positive.", + details={ + "image_width_px": config.image_width_px, + "image_height_px": config.image_height_px, + }, + ) + if config.margin_size_px < 0: + raise OPAIValidationError( + "margin_size_px must be greater than or equal to 0.", + details={"margin_size_px": config.margin_size_px}, + ) diff --git a/src/opai/domain/plot.py b/src/opai/domain/plot.py new file mode 100644 index 0000000..fa8d992 --- /dev/null +++ b/src/opai/domain/plot.py @@ -0,0 +1,103 @@ +from __future__ import annotations + +import math +from collections.abc import Sequence +from dataclasses import dataclass + +import numpy as np + +BASE_SUBPLOT_WIDTH = 4.0 +BASE_SUBPLOT_HEIGHT = 3.0 +MAX_FIGURE_WIDTH = 16.0 +MAX_FIGURE_HEIGHT = 12.0 +MIN_SINGLE_ROW_FIGURE_HEIGHT = 4.5 +MAX_CANVAS_WIDTH = 4072 +MAX_CANVAS_HEIGHT = 2304 + + +@dataclass(frozen=True) +class PlotGrid: + item_count: int + nrows: int + ncols: int + + +def get_plot_grid( + item_count: int, + nrows: int | None = None, + ncols: int | None = None, +) -> PlotGrid: + if item_count <= 0: + raise ValueError("item_count must be positive.") + if nrows is not None and nrows <= 0: + raise ValueError("nrows must be positive when provided.") + if ncols is not None and ncols <= 0: + raise ValueError("ncols must be positive when provided.") + + if nrows is not None: + resolved_rows = nrows + resolved_cols = ncols or int(math.ceil(item_count / nrows)) + elif ncols is not None: + resolved_cols = ncols + resolved_rows = int(math.ceil(item_count / ncols)) + else: + resolved_rows = int(math.sqrt(item_count)) + resolved_cols = int(math.ceil(item_count / resolved_rows)) + + return PlotGrid( + item_count=item_count, + nrows=resolved_rows, + ncols=resolved_cols, + ) + + +def plot_frames( + frames: Sequence[np.ndarray], + nrows: int | None = None, + ncols: int | None = None, + *, + frames_are_bgr: bool = True, +) -> None: + from matplotlib import pyplot + + grid = get_plot_grid(len(frames), nrows=nrows, ncols=ncols) + fig, axes = pyplot.subplots( + grid.nrows, + grid.ncols, + figsize=_get_figsize(grid), + ) + flat_axes = np.atleast_1d(axes).reshape(-1) + + for axis, frame in zip(flat_axes, frames): + image = _prepare_frame(frame, grid) + if image.ndim != 2 and frames_are_bgr: + image = image[..., ::-1] + axis.imshow(image) + axis.set_axis_off() + + for axis in flat_axes[len(frames) :]: + axis.set_axis_off() + + fig.tight_layout() + pyplot.show() + pyplot.close(fig) + + +def _get_figsize( + grid: PlotGrid, +) -> tuple[float, float]: + width = BASE_SUBPLOT_WIDTH * grid.ncols + height = BASE_SUBPLOT_HEIGHT * grid.nrows + scale = min(1.0, MAX_FIGURE_WIDTH / width, MAX_FIGURE_HEIGHT / height) + resolved_height = height * scale + if grid.nrows == 1: + resolved_height = max(resolved_height, MIN_SINGLE_ROW_FIGURE_HEIGHT) + return width * scale, min(resolved_height, MAX_FIGURE_HEIGHT) + + +def _prepare_frame(frame: np.ndarray, grid: PlotGrid) -> np.ndarray: + max_height = max(1, MAX_CANVAS_HEIGHT // grid.nrows) + max_width = max(1, MAX_CANVAS_WIDTH // grid.ncols) + height, width = frame.shape[:2] + stride = max(1, math.ceil(height / max_height), math.ceil(width / max_width)) + return frame[::stride, ::stride] diff --git a/src/opai/infrastructure/__init__.py b/src/opai/infrastructure/__init__.py index e67b9a8..f7e1390 100644 --- a/src/opai/infrastructure/__init__.py +++ b/src/opai/infrastructure/__init__.py @@ -7,8 +7,12 @@ from opai.infrastructure.persistence import ( load_session_manifest, write_calibration_result, + write_calibration_verification_result, + write_charuco_board_config, + write_charuco_board_image, write_session_manifest, ) +from opai.infrastructure.video import sample_video_frames __all__ = [ "get_active_context", @@ -17,5 +21,9 @@ "list_session_names", "load_session_manifest", "write_calibration_result", + "write_calibration_verification_result", + "write_charuco_board_config", + "write_charuco_board_image", "write_session_manifest", + "sample_video_frames", ] diff --git a/src/opai/infrastructure/persistence.py b/src/opai/infrastructure/persistence.py index 49e6149..a9ba64e 100644 --- a/src/opai/infrastructure/persistence.py +++ b/src/opai/infrastructure/persistence.py @@ -5,7 +5,15 @@ from collections.abc import Sequence from pathlib import Path -from opai.domain.calibration import CalibrationResult +import cv2 +import numpy as np + +from opai.core.exceptions import OPAIWorkflowError +from opai.domain.calibration import ( + CalibrationResult, + CalibrationVerificationResult, + CharucoBoardConfig, +) from opai.domain.session import DemoAsset, MappingAsset, SessionManifest @@ -32,6 +40,74 @@ def write_calibration_result( }, } + output_path = session_directory / filename + output_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + print(f"Wrote calibration verification result to {output_path}") + return output_path + + +def write_calibration_verification_result( + session_directory: Path, + result: CalibrationVerificationResult, + filename: str = "calibration_verification.json", +) -> Path: + payload = { + "requested_check_image_count": result.requested_check_image_count, + "sampled_image_count": result.sampled_image_count, + "verified_image_count": result.verified_image_count, + "skipped_image_count": result.skipped_image_count, + "total_detected_corner_count": result.total_detected_corner_count, + "mse_reproj_error": result.mse_reproj_error, + "frame_results": [ + { + "sampled_frame_index": frame.sampled_frame_index, + "detected_corner_count": frame.detected_corner_count, + "mse_reproj_error": frame.mse_reproj_error, + } + for frame in result.frame_results + ], + } + + output_path = session_directory / filename + output_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + print(f"Wrote calibration verification result to {output_path}") + return output_path + + +def write_charuco_board_image( + session_directory: Path, + board_image: np.ndarray, + filename: str = "charuco_board.png", +) -> Path: + output_path = session_directory / filename + wrote_image = cv2.imwrite(str(output_path), board_image) + if not wrote_image: + raise OPAIWorkflowError( + "Failed to write the generated ChArUco board image.", + details={"path": str(output_path)}, + ) + return output_path + + +def write_charuco_board_config( + session_directory: Path, + config: CharucoBoardConfig, + *, + board_image_path: str, + filename: str = "charuco_config.json", +) -> Path: + payload = { + "dictionary": config.dictionary, + "squares_x": config.squares_x, + "squares_y": config.squares_y, + "square_length": config.square_length, + "marker_length": config.marker_length, + "image_width_px": config.image_width_px, + "image_height_px": config.image_height_px, + "margin_size_px": config.margin_size_px, + "board_image_path": board_image_path, + } + output_path = session_directory / filename output_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") return output_path diff --git a/src/opai/infrastructure/video.py b/src/opai/infrastructure/video.py new file mode 100644 index 0000000..883fcbd --- /dev/null +++ b/src/opai/infrastructure/video.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +from pathlib import Path + +import cv2 +import numpy as np + +from opai.core.exceptions import OPAIWorkflowError + + +def sample_video_frames( + video_path: str | Path, + frame_sample_step: int, +) -> tuple[np.ndarray, ...]: + path = Path(video_path).expanduser() + capture = cv2.VideoCapture(str(path)) + if not capture.isOpened(): + capture.release() + raise OPAIWorkflowError( + f"Unable to open video for calibration sampling: {path}", + details={"path": str(path)}, + ) + + sampled_frames: list[np.ndarray] = [] + frame_index = 0 + + try: + while True: + ok, frame = capture.read() + if not ok: + break + if frame_index % frame_sample_step == 0: + sampled_frames.append(frame) + frame_index += 1 + finally: + capture.release() + + return tuple(sampled_frames) diff --git a/src/opai/presentation/__init__.py b/src/opai/presentation/__init__.py index 0caa38d..139a4ee 100644 --- a/src/opai/presentation/__init__.py +++ b/src/opai/presentation/__init__.py @@ -3,10 +3,14 @@ add_mapping, browse_session, calibrate, + calibrate_with_video, + generate_charuco_board, get_context, init, list_sessions, main, + plot_video_frames, + verify_calibrated_parameters, ) __all__ = [ @@ -14,8 +18,12 @@ "add_mapping", "browse_session", "calibrate", + "calibrate_with_video", + "generate_charuco_board", "get_context", "init", "list_sessions", "main", + "plot_video_frames", + "verify_calibrated_parameters", ] diff --git a/src/opai/presentation/facade.py b/src/opai/presentation/facade.py index 738338b..abdf8d4 100644 --- a/src/opai/presentation/facade.py +++ b/src/opai/presentation/facade.py @@ -11,16 +11,37 @@ OPAIDependencyError, OPAIValidationError, ) -from opai.domain.calibration import CalibrationResult +from opai.domain.calibration import ( + DEFAULT_CHARUCO_DICTIONARY, + DEFAULT_CHARUCO_IMAGE_HEIGHT_PX, + DEFAULT_CHARUCO_IMAGE_WIDTH_PX, + DEFAULT_CHARUCO_MARGIN_SIZE_PX, + DEFAULT_CHARUCO_MARKER_LENGTH, + DEFAULT_CHARUCO_SQUARE_LENGTH, + DEFAULT_CHARUCO_SQUARES_X, + DEFAULT_CHARUCO_SQUARES_Y, + CalibrationResult, + CalibrationVerificationResult, + CharucoBoardArtifacts, + CharucoBoardConfig, +) from opai.domain.context import Context +from opai.domain.plot import plot_frames from opai.domain.session import DemoAsset, MappingAsset from opai.infrastructure.context_store import get_active_context, init_context -_SESSION_NAME_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$") +SESSION_NAME_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$") def init(name: str) -> Context: - normalized_name = _normalize_session_name(name) + if not isinstance(name, str) or not name.strip(): + raise OPAIValidationError("Session name must be a non-empty string.") + normalized_name = name.strip() + if not SESSION_NAME_PATTERN.fullmatch(normalized_name): + raise OPAIValidationError( + "Session name may only contain letters, numbers, '.', '_' and '-'.", + details={"session_name": normalized_name}, + ) return init_context(normalized_name) @@ -40,6 +61,9 @@ def calibrate( square_length: float, marker_length: float, dictionary: str, + plot_result: bool = False, + plot_nrows: int | None = None, + plot_ncols: int | None = None, ) -> CalibrationResult: ctx = get_context() try: @@ -58,9 +82,152 @@ def calibrate( square_length=square_length, marker_length=marker_length, dictionary=dictionary, + plot_result=plot_result, + plot_nrows=plot_nrows, + plot_ncols=plot_ncols, + ) + + +def generate_charuco_board( + dictionary: str = DEFAULT_CHARUCO_DICTIONARY, + squares_x: int = DEFAULT_CHARUCO_SQUARES_X, + squares_y: int = DEFAULT_CHARUCO_SQUARES_Y, + square_length: float = DEFAULT_CHARUCO_SQUARE_LENGTH, + marker_length: float = DEFAULT_CHARUCO_MARKER_LENGTH, + image_width_px: int = DEFAULT_CHARUCO_IMAGE_WIDTH_PX, + image_height_px: int = DEFAULT_CHARUCO_IMAGE_HEIGHT_PX, + margin_size_px: int = DEFAULT_CHARUCO_MARGIN_SIZE_PX, +) -> CharucoBoardArtifacts: + ctx = get_context() + try: + from opai.application.calibration import ( + generate_charuco_board as generate_charuco_board_with_context, + ) + except ModuleNotFoundError as exc: + raise OPAIDependencyError( + "ChArUco board generation dependencies are unavailable. Install the project's " + "OpenCV stack before calling opai.generate_charuco_board(...)." + ) from exc + + config = CharucoBoardConfig( + dictionary=dictionary, + squares_x=squares_x, + squares_y=squares_y, + square_length=square_length, + marker_length=marker_length, + image_width_px=image_width_px, + image_height_px=image_height_px, + margin_size_px=margin_size_px, + ) + return generate_charuco_board_with_context(ctx=ctx, config=config) + + +def calibrate_with_video( + video_path: str | Path, + frame_sample_step: int, + row_count: int, + col_count: int, + square_length: float, + marker_length: float, + dictionary: str, + plot_result: bool = False, + plot_nrows: int | None = None, + plot_ncols: int | None = None, +) -> CalibrationResult: + ctx = get_context() + try: + from opai.application.calibration import ( + calibrate as calibrate_with_context, + ) + from opai.application.calibration import ( + sample_video_frames as sample_video_frames_from_video, + ) + except ModuleNotFoundError as exc: + raise OPAIDependencyError( + "Calibration dependencies are unavailable. Install the project's " + "OpenCV calibration stack before calling opai.calibrate_with_video(...)." + ) from exc + + frames = sample_video_frames_from_video( + video_path=video_path, + frame_sample_step=frame_sample_step, + ) + return calibrate_with_context( + ctx=ctx, + frames=frames, + row_count=row_count, + col_count=col_count, + square_length=square_length, + marker_length=marker_length, + dictionary=dictionary, + plot_result=plot_result, + plot_nrows=plot_nrows, + plot_ncols=plot_ncols, ) +def verify_calibrated_parameters( + video_path: str | Path, + n_check_imgs: int, + charuco_config_json: str | Path | dict[str, object], + intrinsics_json: str | Path | dict[str, object], + plot_result: bool = False, + plot_nrows: int | None = None, + plot_ncols: int | None = None, +) -> CalibrationVerificationResult: + ctx = get_context() + from opai.application.calibration import ( + verify_calibrated_parameters as verify_with_context, + ) + + return verify_with_context( + ctx=ctx, + video_path=video_path, + n_check_imgs=n_check_imgs, + charuco_config_json=charuco_config_json, + intrinsics_json=intrinsics_json, + plot_result=plot_result, + plot_nrows=plot_nrows, + plot_ncols=plot_ncols, + ) + + +def plot_video_frames( + video_path: str | Path, + frame_sample_step: int, + nrows: int | None = None, + ncols: int | None = None, +) -> None: + try: + from opai.application.calibration import ( + sample_video_frames as sample_video_frames_from_video, + ) + except ModuleNotFoundError as exc: + raise OPAIDependencyError( + "Calibration dependencies are unavailable. Install the project's " + "OpenCV calibration stack before calling opai.plot_video_frames(...)." + ) from exc + + frames = sample_video_frames_from_video( + video_path=video_path, + frame_sample_step=frame_sample_step, + ) + try: + plot_frames( + frames, + nrows=nrows, + ncols=ncols, + frames_are_bgr=True, + ) + except ModuleNotFoundError as exc: + raise OPAIDependencyError( + "Frame plotting requires the 'matplotlib' package. Install project " + "dependencies before calling opai.plot_video_frames(...)." + ) from exc + except ValueError as exc: + raise OPAIValidationError(str(exc)) from exc + + def add_demos(video_paths: Sequence[str | Path]) -> tuple[DemoAsset, ...]: ctx = get_context() from opai.application.session import add_demos as add_demos_with_context @@ -76,50 +243,97 @@ def add_mapping(video_path: str | Path) -> MappingAsset: def list_sessions() -> list[str]: - from opai.application.session import list_sessions as list_available_sessions - - return list_available_sessions() - - -def browse_session(name: str) -> list[str]: - normalized_name = _normalize_session_name(name) try: from rich.console import Console from rich.tree import Tree except ModuleNotFoundError as exc: raise OPAIDependencyError( - "Session browsing requires the 'rich' package. Install project dependencies before calling opai.browse_session(...)." + "Session browsing requires the 'rich' package. Install project dependencies before calling opai.list_sessions(...) or opai.browse_session(...)." ) from exc + from opai.application.session import describe_sessions - from opai.application.session import browse_session as browse_named_session - - file_paths, tree_payload = browse_named_session(normalized_name) - tree = Tree(normalized_name) - _append_tree_nodes(tree, tree_payload) - Console().print(tree) - return file_paths - - -def main() -> None: - print( - "Use opai.init(name), opai.add_demos(...), opai.add_mapping(...), and opai.calibrate(...) from Python." + catalog = describe_sessions() + console = Console() + tree = Tree( + f"[bold]{catalog.root_dirname}[/] [dim]{catalog.root_path}[/]", + guide_style="dim", ) + session_names = [session.name for session in catalog.sessions] + if not catalog.sessions: + tree.add("[yellow]No sessions found[/]") + console.print(tree) + return session_names + + for session in catalog.sessions: + tags: list[str] = [] + if session.is_active: + tags.append("current") + tags.append(f"demos={session.demo_count}") + tags.append(f"mapping={'yes' if session.has_mapping else 'no'}") + tags.append(f"files={session.file_count}") + tree.add(f"[bold cyan]{session.name}[/] [dim]({', '.join(tags)})[/]") + console.print(tree) + return session_names -def _normalize_session_name(name: str) -> str: +def browse_session(name: str) -> list[str]: if not isinstance(name, str) or not name.strip(): raise OPAIValidationError("Session name must be a non-empty string.") normalized_name = name.strip() - if not _SESSION_NAME_PATTERN.fullmatch(normalized_name): + if not SESSION_NAME_PATTERN.fullmatch(normalized_name): raise OPAIValidationError( "Session name may only contain letters, numbers, '.', '_' and '-'.", details={"session_name": normalized_name}, ) - return normalized_name + + try: + from rich.console import Console + from rich.tree import Tree + except ModuleNotFoundError as exc: + raise OPAIDependencyError( + "Session browsing requires the 'rich' package. Install project dependencies before calling opai.list_sessions(...) or opai.browse_session(...)." + ) from exc + from opai.application.session import describe_session + + view = describe_session(normalized_name) + console = Console() + tree = Tree( + f"[bold]{view.root_dirname}[/] [dim]{view.root_path}[/]", + guide_style="dim", + ) + session_branch = tree.add( + "[bold magenta]" + f"{view.session_name}" + "[/] [dim]" + f"(path={view.session_path.name}, demos={view.demo_count}, " + f"mapping={'yes' if view.has_mapping else 'no'}, files={view.file_count})" + "[/]" + ) + session_branch.add(f"[dim]path:[/] [cyan]{view.session_path}[/]") + + pending_nodes: list[tuple[object, dict[str, dict]]] = [ + (session_branch, view.tree_payload) + ] + for parent, payload in pending_nodes: + for child_name, child in payload.items(): + is_directory = isinstance(child, dict) and bool(child) + label = ( + f"[bold blue]{child_name}/[/]" + if is_directory + else f"[green]{child_name}[/]" + ) + branch = parent.add(label) + if is_directory: + pending_nodes.append((branch, child)) + + console.print(tree) + return list(view.file_paths) -def _append_tree_nodes(tree, payload: dict[str, dict]) -> None: - for name, child in payload.items(): - branch = tree.add(name) - if isinstance(child, dict): - _append_tree_nodes(branch, child) +def main() -> None: + print( + "Use opai.init(name), opai.add_demos(...), opai.add_mapping(...), " + "opai.generate_charuco_board(...), opai.calibrate(...), " + "opai.calibrate_with_video(...), opai.verify_calibrated_parameters(...), " + "and opai.plot_video_frames(...) from Python." + ) diff --git a/tests/test_calibration.py b/tests/test_calibration.py index 62e4267..df615f0 100644 --- a/tests/test_calibration.py +++ b/tests/test_calibration.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json from types import SimpleNamespace import numpy as np @@ -7,13 +8,17 @@ from opai.application import calibration as calibration_module from opai.application.calibration import ( + _build_fisheye_calibration_points, _build_intrinsics, _compute_mse_reprojection_error, - _resolve_dictionary, calibrate, + sample_video_frames, + verify_calibrated_parameters, ) from opai.core.exceptions import OPAIValidationError, OPAIWorkflowError from opai.domain.context import Context +from opai.domain.plot import get_plot_grid +from opai.infrastructure import video as video_module class DummyBoard: @@ -31,7 +36,24 @@ def fake_cv2(monkeypatch: pytest.MonkeyPatch) -> SimpleNamespace: DICT_4X4_50=1, getPredefinedDictionary=lambda dictionary_id: {"id": dictionary_id}, ), - projectPoints=lambda **kwargs: (kwargs["objectPoints"][:, :, :2], None), + fisheye=SimpleNamespace( + CALIB_RECOMPUTE_EXTRINSIC=1, + CALIB_CHECK_COND=2, + CALIB_FIX_SKEW=4, + calibrate=lambda *args, **kwargs: ( + 0.1, + np.eye(3), + np.zeros((4, 1)), + (), + (), + ), + projectPoints=lambda object_points, *_args: ( + object_points[:, :, :2], + None, + ), + ), + TERM_CRITERIA_EPS=1, + TERM_CRITERIA_MAX_ITER=2, COLOR_BGR2GRAY=1, cvtColor=lambda frame, _: frame[:, :, 0], ) @@ -39,9 +61,15 @@ def fake_cv2(monkeypatch: pytest.MonkeyPatch) -> SimpleNamespace: return fake -def test_resolve_dictionary_rejects_unknown_name(fake_cv2: SimpleNamespace) -> None: +def test_calibrate_rejects_unknown_dictionary( + tmp_path, + fake_cv2: SimpleNamespace, +) -> None: + ctx = Context(name="session", session_directory=tmp_path) + frame = np.zeros((10, 10, 3), dtype=np.uint8) + with pytest.raises(OPAIValidationError, match="Unsupported ArUco dictionary"): - _resolve_dictionary("DICT_DOES_NOT_EXIST") + calibrate(ctx, [frame], 3, 3, 1.0, 0.5, "DICT_DOES_NOT_EXIST") def test_calibrate_rejects_invalid_frames(tmp_path, fake_cv2: SimpleNamespace) -> None: @@ -53,6 +81,56 @@ def test_calibrate_rejects_invalid_frames(tmp_path, fake_cv2: SimpleNamespace) - calibrate(ctx, [frame_a, frame_b], 3, 3, 1.0, 0.5, "DICT_4X4_50") +def test_build_fisheye_calibration_points_maps_ids_to_board_coordinates() -> None: + board = DummyBoard( + np.array( + [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + ], + dtype=np.float32, + ) + ) + charuco_corners = [ + np.array( + [ + [[10.0, 20.0]], + [[30.0, 40.0]], + ], + dtype=np.float32, + ) + ] + charuco_ids = [np.array([[2], [0]], dtype=np.int32)] + + object_points, image_points = _build_fisheye_calibration_points( + board=board, + charuco_corners=charuco_corners, + charuco_ids=charuco_ids, + ) + + assert np.array_equal( + object_points[0], + np.array( + [ + [[0.0, 1.0, 0.0]], + [[0.0, 0.0, 0.0]], + ], + dtype=np.float64, + ), + ) + assert np.array_equal( + image_points[0], + np.array( + [ + [[10.0, 20.0]], + [[30.0, 40.0]], + ], + dtype=np.float64, + ), + ) + + def test_build_intrinsics_zero_fills_missing_distortion() -> None: camera_matrix = np.array([[10.0, 1.5, 5.0], [0.0, 20.0, 6.0], [0.0, 0.0, 1.0]]) dist_coeffs = np.array([0.1, 0.2]) @@ -80,13 +158,19 @@ def test_compute_mse_reprojection_error_averages_squared_pixel_error( observed_corners = [np.array([[[1.0, 1.0]], [[4.0, 4.0]]], dtype=np.float32)] observed_ids = [np.array([[0], [1]], dtype=np.int32)] - def fake_project_points(**_: np.ndarray) -> tuple[np.ndarray, None]: + def fake_project_points( + object_points: np.ndarray, + _rvec: np.ndarray, + _tvec: np.ndarray, + _camera_matrix: np.ndarray, + _dist_coeffs: np.ndarray, + ) -> tuple[np.ndarray, None]: return np.array([[[2.0, 3.0]], [[5.0, 6.0]]], dtype=np.float32), None monkeypatch.setattr( calibration_module, "cv2", - SimpleNamespace(projectPoints=fake_project_points), + SimpleNamespace(fisheye=SimpleNamespace(projectPoints=fake_project_points)), ) mse = _compute_mse_reprojection_error( @@ -119,6 +203,274 @@ def test_compute_mse_reprojection_error_rejects_inconsistent_lengths() -> None: ) +def test_get_plot_grid_auto_computes_near_square_layout() -> None: + grid = get_plot_grid(5) + + assert grid.item_count == 5 + assert grid.nrows == 2 + assert grid.ncols == 3 + + +def test_sample_video_frames_rejects_non_positive_step(tmp_path) -> None: + video_path = tmp_path / "demo.mp4" + video_path.write_bytes(b"demo") + + with pytest.raises(OPAIValidationError, match="frame_sample_step"): + sample_video_frames(video_path, 0) + + +def test_sample_video_frames_rejects_missing_path(tmp_path) -> None: + with pytest.raises(OPAIValidationError, match="does not exist"): + sample_video_frames(tmp_path / "missing.mp4", 2) + + +def test_sample_video_frames_rejects_empty_sampling( + tmp_path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + video_path = tmp_path / "demo.mp4" + video_path.write_bytes(b"demo") + monkeypatch.setattr( + calibration_module, "sample_video_frames_from_path", lambda *_: () + ) + + with pytest.raises(OPAIWorkflowError, match="produced no frames"): + sample_video_frames(video_path, 2) + + +def test_verify_calibrated_parameters_writes_verification_artifact( + tmp_path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + ctx = Context(name="session", session_directory=tmp_path) + frames = tuple( + np.full((10, 12, 3), fill_value=index, dtype=np.uint8) for index in range(4) + ) + monkeypatch.setattr( + calibration_module, + "sample_video_frames", + lambda **_kwargs: frames, + ) + monkeypatch.setattr(calibration_module, "cv2", _build_verify_cv2()) + plot_calls: list[tuple[tuple[np.ndarray, ...], dict[str, object]]] = [] + + def fake_plot_frames( + plotted_frames: tuple[np.ndarray, ...] | list[np.ndarray], + **kwargs, + ) -> None: + plot_calls.append((tuple(plotted_frames), kwargs)) + + monkeypatch.setattr(calibration_module, "plot_frames", fake_plot_frames) + + result = verify_calibrated_parameters( + ctx=ctx, + video_path=tmp_path / "demo.mp4", + n_check_imgs=2, + charuco_config_json={ + "dictionary": "DICT_4X4_50", + "squares_x": 3, + "squares_y": 3, + "square_length": 1.0, + "marker_length": 0.5, + "image_width_px": 1200, + "image_height_px": 800, + "margin_size_px": 20, + }, + intrinsics_json={ + "image_width": 12, + "image_height": 10, + "intrinsics": { + "focal_length": 10.0, + "aspect_ratio": 0.5, + "principal_pt_x": 5.0, + "principal_pt_y": 6.0, + "radial_distortion_1": 0.1, + "radial_distortion_2": 0.2, + "radial_distortion_3": 0.3, + "radial_distortion_4": 0.4, + "skew": 0.0, + }, + }, + plot_result=True, + plot_nrows=1, + plot_ncols=2, + ) + + assert result.requested_check_image_count == 2 + assert result.sampled_image_count == 2 + assert result.verified_image_count == 2 + assert result.skipped_image_count == 0 + assert result.total_detected_corner_count == 8 + assert result.mse_reproj_error == pytest.approx(5.0) + assert [frame.sampled_frame_index for frame in result.frame_results] == [0, 3] + assert len(plot_calls) == 1 + assert len(plot_calls[0][0]) == 2 + assert plot_calls[0][1] == {"nrows": 1, "ncols": 2, "frames_are_bgr": True} + plotted_frame = plot_calls[0][0][0] + assert np.any( + np.all(plotted_frame == np.array([0, 255, 0], dtype=np.uint8), axis=-1) + ) + assert np.any( + np.all(plotted_frame == np.array([0, 0, 255], dtype=np.uint8), axis=-1) + ) + + verification_path = tmp_path / "calibration_verification.json" + payload = json.loads(verification_path.read_text(encoding="utf-8")) + assert payload["verified_image_count"] == 2 + assert payload["mse_reproj_error"] == pytest.approx(5.0) + + +def test_verify_calibrated_parameters_skips_plotting_by_default( + tmp_path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + ctx = Context(name="session", session_directory=tmp_path) + frames = tuple( + np.full((10, 12, 3), fill_value=index, dtype=np.uint8) for index in range(3) + ) + monkeypatch.setattr( + calibration_module, + "sample_video_frames", + lambda **_kwargs: frames, + ) + monkeypatch.setattr(calibration_module, "cv2", _build_verify_cv2()) + monkeypatch.setattr( + calibration_module, + "plot_frames", + lambda *_args, **_kwargs: pytest.fail( + "plot_frames should not run when plot_result=False" + ), + ) + + result = verify_calibrated_parameters( + ctx=ctx, + video_path=tmp_path / "demo.mp4", + n_check_imgs=2, + charuco_config_json={ + "dictionary": "DICT_4X4_50", + "squares_x": 3, + "squares_y": 3, + "square_length": 1.0, + "marker_length": 0.5, + "image_width_px": 1200, + "image_height_px": 800, + "margin_size_px": 20, + }, + intrinsics_json={ + "image_width": 12, + "image_height": 10, + "intrinsics": { + "focal_length": 10.0, + "aspect_ratio": 0.5, + "principal_pt_x": 5.0, + "principal_pt_y": 6.0, + "radial_distortion_1": 0.1, + "radial_distortion_2": 0.2, + "radial_distortion_3": 0.3, + "radial_distortion_4": 0.4, + "skew": 0.0, + }, + }, + ) + + assert result.verified_image_count == 2 + + +def test_verify_calibrated_parameters_rejects_non_positive_check_count( + tmp_path, +) -> None: + ctx = Context(name="session", session_directory=tmp_path) + + with pytest.raises(OPAIValidationError, match="n_check_imgs"): + verify_calibrated_parameters( + ctx=ctx, + video_path=tmp_path / "demo.mp4", + n_check_imgs=0, + charuco_config_json={}, + intrinsics_json={}, + ) + + +def test_verify_calibrated_parameters_rejects_intrinsics_image_size_mismatch( + tmp_path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + ctx = Context(name="session", session_directory=tmp_path) + monkeypatch.setattr( + calibration_module, + "sample_video_frames", + lambda **_kwargs: (np.zeros((10, 12, 3), dtype=np.uint8),), + ) + monkeypatch.setattr(calibration_module, "cv2", _build_verify_cv2()) + + with pytest.raises(OPAIValidationError, match="image size does not match"): + verify_calibrated_parameters( + ctx=ctx, + video_path=tmp_path / "demo.mp4", + n_check_imgs=1, + charuco_config_json={ + "dictionary": "DICT_4X4_50", + "squares_x": 3, + "squares_y": 3, + "square_length": 1.0, + "marker_length": 0.5, + "image_width_px": 1200, + "image_height_px": 800, + "margin_size_px": 20, + }, + intrinsics_json={ + "image_width": 11, + "image_height": 10, + "intrinsics": { + "focal_length": 10.0, + "aspect_ratio": 0.5, + "principal_pt_x": 5.0, + "principal_pt_y": 6.0, + "radial_distortion_1": 0.1, + "radial_distortion_2": 0.2, + "radial_distortion_3": 0.3, + "radial_distortion_4": 0.4, + "skew": 0.0, + }, + }, + ) + + +def test_infrastructure_video_sampling_starts_at_zero_and_respects_step( + monkeypatch: pytest.MonkeyPatch, + tmp_path, +) -> None: + frames = tuple( + np.full((2, 2, 3), fill_value=index, dtype=np.uint8) for index in range(5) + ) + + class FakeCapture: + def __init__(self, _path: str) -> None: + self._frames = list(frames) + self._released = False + + def isOpened(self) -> bool: + return True + + def read(self) -> tuple[bool, np.ndarray | None]: + if not self._frames: + return False, None + return True, self._frames.pop(0) + + def release(self) -> None: + self._released = True + + monkeypatch.setattr( + video_module, + "cv2", + SimpleNamespace(VideoCapture=FakeCapture), + ) + + sampled = video_module.sample_video_frames(tmp_path / "demo.mp4", 2) + + assert [int(frame[0, 0, 0]) for frame in sampled] == [0, 2, 4] + + def test_repo_exceptions_expose_error_codes_and_payload() -> None: error = OPAIValidationError( "Invalid board parameters.", @@ -138,3 +490,100 @@ def test_repo_exceptions_expose_error_codes_and_payload() -> None: ) assert overridden.error_code == "charuco_calibration_failed" + + +def _build_verify_cv2() -> SimpleNamespace: + def fake_cvt_color(frame: np.ndarray, code: int) -> np.ndarray: + if code == 1: + return frame[:, :, 0] + if code == 2: + return np.repeat(frame[:, :, None], 3, axis=2) + raise AssertionError(f"Unexpected color conversion code: {code}") + + def fake_circle( + image: np.ndarray, + center: tuple[int, int], + _radius: int, + color: tuple[int, int, int], + _thickness: int, + ) -> np.ndarray: + x, y = center + if 0 <= y < image.shape[0] and 0 <= x < image.shape[1]: + image[y, x] = np.array(color, dtype=image.dtype) + return image + + def fake_arrowed_line( + image: np.ndarray, + start: tuple[int, int], + end: tuple[int, int], + color: tuple[int, int, int], + _thickness: int, + *, + tipLength: float = 0.0, + ) -> np.ndarray: + del tipLength + midpoint = ( + int(round((start[0] + end[0]) / 2)), + int(round((start[1] + end[1]) / 2)), + ) + for x, y in (start, midpoint, end): + if 0 <= y < image.shape[0] and 0 <= x < image.shape[1]: + image[y, x] = np.array(color, dtype=image.dtype) + return image + + board_points = np.array( + [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [1.0, 1.0, 0.0], + ], + dtype=np.float64, + ) + observed_corners = np.array( + [ + [[1.0, 1.0]], + [[2.0, 1.0]], + [[1.0, 2.0]], + [[2.0, 2.0]], + ], + dtype=np.float64, + ) + observed_ids = np.array([[0], [1], [2], [3]], dtype=np.int32) + + class FakeCharucoDetector: + def __init__(self, _board) -> None: + self.board = _board + + def detectBoard(self, _image): + return observed_corners, observed_ids, None, None + + aruco = SimpleNamespace( + DICT_4X4_50=1, + getPredefinedDictionary=lambda _dictionary_id: "dictionary", + CharucoBoard=lambda *_args, **_kwargs: SimpleNamespace( + getChessboardCorners=lambda: board_points + ), + CharucoDetector=FakeCharucoDetector, + ) + fisheye = SimpleNamespace( + projectPoints=lambda _object_points, *_args: ( + observed_corners + np.array([[[1.0, 2.0]]], dtype=np.float64), + None, + ) + ) + + return SimpleNamespace( + aruco=aruco, + fisheye=fisheye, + COLOR_BGR2GRAY=1, + COLOR_GRAY2BGR=2, + cvtColor=fake_cvt_color, + arrowedLine=fake_arrowed_line, + circle=fake_circle, + solvePnP=lambda *_args, **_kwargs: ( + True, + np.zeros((3, 1), dtype=np.float64), + np.zeros((3, 1), dtype=np.float64), + ), + ) diff --git a/tests/test_charuco.py b/tests/test_charuco.py new file mode 100644 index 0000000..c7798e7 --- /dev/null +++ b/tests/test_charuco.py @@ -0,0 +1,124 @@ +from __future__ import annotations + +import json +from types import SimpleNamespace + +import numpy as np +import pytest + +import opai +from opai.application import calibration as calibration_module +from opai.core.exceptions import OPAIContextError, OPAIValidationError +from opai.domain.calibration import CharucoBoardConfig, validate_charuco_board_config +from opai.infrastructure import context_store +from opai.infrastructure import persistence as persistence_module + + +@pytest.fixture(autouse=True) +def reset_active_context(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(context_store, "_ACTIVE_CONTEXT", None) + + +def test_validate_charuco_board_config_rejects_invalid_marker_length() -> None: + config = CharucoBoardConfig( + dictionary="DICT_5X5_100", + squares_x=11, + squares_y=8, + square_length=0.02, + marker_length=0.02, + image_width_px=2000, + image_height_px=1400, + margin_size_px=20, + ) + + with pytest.raises(OPAIValidationError, match="smaller than square_length"): + validate_charuco_board_config(config) + + +def test_generate_charuco_board_requires_context(monkeypatch) -> None: + monkeypatch.setattr(context_store, "_ACTIVE_CONTEXT", None) + + with pytest.raises(OPAIContextError, match="Call opai.init"): + opai.generate_charuco_board() + + +def test_generate_charuco_board_writes_image_and_config(tmp_path, monkeypatch) -> None: + monkeypatch.chdir(tmp_path) + opai.init("session-001") + fake_cv2 = _build_fake_cv2() + monkeypatch.setattr(calibration_module, "cv2", fake_cv2) + monkeypatch.setattr(persistence_module, "cv2", fake_cv2) + + artifacts = opai.generate_charuco_board() + + image_path = tmp_path / ".opai_sessions" / "session-001" / "charuco_board.png" + config_path = tmp_path / ".opai_sessions" / "session-001" / "charuco_config.json" + + assert artifacts.image_path == image_path + assert artifacts.config_path == config_path + assert image_path.read_bytes() == b"fake-png" + + payload = json.loads(config_path.read_text(encoding="utf-8")) + assert payload == { + "dictionary": "DICT_5X5_100", + "squares_x": 11, + "squares_y": 8, + "square_length": 0.03, + "marker_length": 0.022, + "image_width_px": 2000, + "image_height_px": 1400, + "margin_size_px": 20, + "board_image_path": "charuco_board.png", + } + + +def test_generate_charuco_board_rejects_unknown_dictionary( + tmp_path, monkeypatch +) -> None: + monkeypatch.chdir(tmp_path) + opai.init("session-001") + fake_cv2 = _build_fake_cv2() + monkeypatch.setattr(calibration_module, "cv2", fake_cv2) + monkeypatch.setattr(persistence_module, "cv2", fake_cv2) + + with pytest.raises(OPAIValidationError, match="Unsupported ArUco dictionary"): + opai.generate_charuco_board(dictionary="DICT_DOES_NOT_EXIST") + + +def _build_fake_cv2() -> SimpleNamespace: + class FakeBoard: + def __init__( + self, + size: tuple[int, int], + square_length: float, + marker_length: float, + dictionary: object, + ) -> None: + self.size = size + self.square_length = square_length + self.marker_length = marker_length + self.dictionary = dictionary + + def generateImage( + self, + image_size: tuple[int, int], + *, + marginSize: int, + ) -> np.ndarray: + width, height = image_size + return np.full((height, width), fill_value=marginSize, dtype=np.uint8) + + def fake_imwrite(path: str, _image: np.ndarray) -> bool: + from pathlib import Path + + Path(path).write_bytes(b"fake-png") + return True + + return SimpleNamespace( + aruco=SimpleNamespace( + DICT_5X5_100=100, + getPredefinedDictionary=lambda dictionary_id: {"id": dictionary_id}, + CharucoBoard=FakeBoard, + ), + imwrite=fake_imwrite, + ) diff --git a/tests/test_facade.py b/tests/test_facade.py index 5b4e800..3b770ef 100644 --- a/tests/test_facade.py +++ b/tests/test_facade.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import json import sys from builtins import __import__ as builtin_import @@ -12,6 +14,7 @@ OPAIContextError, OPAIDependencyError, OPAIValidationError, + OPAIWorkflowError, ) from opai.infrastructure import context_store @@ -21,6 +24,30 @@ def test_calibrate_requires_context() -> None: opai.calibrate([], 3, 3, 1.0, 0.5, "DICT_4X4_50") +def test_calibrate_with_video_requires_context(tmp_path, monkeypatch) -> None: + monkeypatch.chdir(tmp_path) + video_path = tmp_path / "demo.mp4" + video_path.write_bytes(b"demo") + + with pytest.raises(OPAIContextError, match="Call opai.init"): + opai.calibrate_with_video(video_path, 2, 3, 3, 1.0, 0.5, "DICT_4X4_50") + + +def test_verify_calibrated_parameters_requires_context(tmp_path, monkeypatch) -> None: + monkeypatch.chdir(tmp_path) + monkeypatch.setattr(context_store, "_ACTIVE_CONTEXT", None) + video_path = tmp_path / "demo.mp4" + video_path.write_bytes(b"demo") + + with pytest.raises(OPAIContextError, match="Call opai.init"): + opai.verify_calibrated_parameters( + video_path=video_path, + n_check_imgs=2, + charuco_config_json={}, + intrinsics_json={}, + ) + + def test_init_creates_context_directory(tmp_path, monkeypatch) -> None: monkeypatch.chdir(tmp_path) ctx = opai.init("session-001") @@ -70,11 +97,20 @@ def test_init_rejects_invalid_session_name() -> None: opai.init("../bad-session") -def test_calibrate_writes_artifact(tmp_path, monkeypatch) -> None: +def test_calibrate_writes_artifact_without_plotting_by_default( + tmp_path, monkeypatch +) -> None: fake_cv2 = _build_fake_cv2() monkeypatch.setattr(calibration_module, "cv2", fake_cv2) monkeypatch.chdir(tmp_path) opai.init("session-001") + monkeypatch.setattr( + calibration_module, + "plot_frames", + lambda *_args, **_kwargs: pytest.fail( + "plot_frames should not run when plot_result=False" + ), + ) frame = np.zeros((10, 12, 3), dtype=np.uint8) result = opai.calibrate( @@ -95,6 +131,377 @@ def test_calibrate_writes_artifact(tmp_path, monkeypatch) -> None: assert result.intrinsic_type == "FISHEYE" +def test_calibrate_accepts_custom_plot_grid(tmp_path, monkeypatch) -> None: + fake_cv2 = _build_fake_cv2() + monkeypatch.setattr(calibration_module, "cv2", fake_cv2) + monkeypatch.chdir(tmp_path) + opai.init("session-001") + pyplot = _build_fake_pyplot(nrows=1, ncols=5) + monkeypatch.setitem(sys.modules, "matplotlib", SimpleNamespace(pyplot=pyplot)) + monkeypatch.setitem(sys.modules, "matplotlib.pyplot", pyplot) + + frame = np.zeros((10, 12, 3), dtype=np.uint8) + opai.calibrate( + [frame], + 3, + 3, + 1.0, + 0.5, + "DICT_4X4_50", + plot_result=True, + plot_nrows=1, + plot_ncols=5, + ) + + assert pyplot.subplots_calls == [((1, 5), {"figsize": (16.0, 4.5)})] + + +def test_calibrate_with_video_writes_artifact_without_plotting_by_default( + tmp_path, + monkeypatch, +) -> None: + fake_cv2 = _build_fake_cv2() + monkeypatch.setattr(calibration_module, "cv2", fake_cv2) + monkeypatch.chdir(tmp_path) + opai.init("session-001") + + frames = tuple( + np.full((10, 12, 3), fill_value=index, dtype=np.uint8) for index in range(5) + ) + video_path = tmp_path / "demo.mp4" + video_path.write_bytes(b"demo") + monkeypatch.setattr( + calibration_module, + "sample_video_frames", + lambda **kwargs: frames, + ) + monkeypatch.setattr( + calibration_module, + "plot_frames", + lambda *_args, **_kwargs: pytest.fail( + "plot_frames should not run when plot_result=False" + ), + ) + + result = opai.calibrate_with_video( + video_path=video_path, + frame_sample_step=2, + row_count=3, + col_count=3, + square_length=1.0, + marker_length=0.5, + dictionary="DICT_4X4_50", + plot_result=False, + plot_nrows=1, + plot_ncols=5, + ) + + output_path = tmp_path / ".opai_sessions" / "session-001" / "calibration.json" + assert output_path.exists() + payload = json.loads(output_path.read_text(encoding="utf-8")) + assert payload["image_height"] == 10 + assert payload["image_width"] == 12 + assert result.intrinsic_type == "FISHEYE" + + +def test_calibrate_with_video_plots_detected_corners_when_enabled( + tmp_path, + monkeypatch, +) -> None: + fake_cv2 = _build_fake_cv2() + monkeypatch.setattr(calibration_module, "cv2", fake_cv2) + monkeypatch.chdir(tmp_path) + opai.init("session-001") + + frames = tuple( + np.full((10, 12, 3), fill_value=index, dtype=np.uint8) for index in range(5) + ) + video_path = tmp_path / "demo.mp4" + video_path.write_bytes(b"demo") + monkeypatch.setattr( + calibration_module, + "sample_video_frames", + lambda **kwargs: frames, + ) + pyplot = _build_fake_pyplot(nrows=1, ncols=5) + monkeypatch.setitem(sys.modules, "matplotlib", SimpleNamespace(pyplot=pyplot)) + monkeypatch.setitem(sys.modules, "matplotlib.pyplot", pyplot) + + result = opai.calibrate_with_video( + video_path=video_path, + frame_sample_step=2, + row_count=3, + col_count=3, + square_length=1.0, + marker_length=0.5, + dictionary="DICT_4X4_50", + plot_result=True, + plot_nrows=1, + plot_ncols=5, + ) + + output_path = tmp_path / ".opai_sessions" / "session-001" / "calibration.json" + assert output_path.exists() + payload = json.loads(output_path.read_text(encoding="utf-8")) + assert payload["image_height"] == 10 + assert payload["image_width"] == 12 + assert result.intrinsic_type == "FISHEYE" + assert pyplot.subplots_calls == [((1, 5), {"figsize": (16.0, 4.5)})] + assert pyplot.show_count == 1 + assert pyplot.close_calls == [pyplot.figure] + assert np.array_equal(pyplot.axes[0].images[0], frames[0][..., ::-1]) + + +def test_verify_calibrated_parameters_uses_session_relative_json_paths( + tmp_path, + monkeypatch, +) -> None: + fake_cv2 = _build_fake_cv2() + monkeypatch.setattr(calibration_module, "cv2", fake_cv2) + monkeypatch.chdir(tmp_path) + ctx = opai.init("session-001") + + frames = tuple( + np.full((10, 12, 3), fill_value=index, dtype=np.uint8) for index in range(3) + ) + video_path = tmp_path / "demo.mp4" + video_path.write_bytes(b"demo") + monkeypatch.setattr( + calibration_module, + "sample_video_frames", + lambda **_kwargs: frames, + ) + plot_calls: list[tuple[tuple[np.ndarray, ...], dict[str, object]]] = [] + + def fake_plot_frames( + plotted_frames: tuple[np.ndarray, ...] | list[np.ndarray], + **kwargs, + ) -> None: + plot_calls.append((tuple(plotted_frames), kwargs)) + + monkeypatch.setattr(calibration_module, "plot_frames", fake_plot_frames) + + charuco_path = ctx.session_directory / "charuco_config.json" + charuco_path.write_text( + json.dumps( + { + "dictionary": "DICT_4X4_50", + "squares_x": 3, + "squares_y": 3, + "square_length": 1.0, + "marker_length": 0.5, + "image_width_px": 1200, + "image_height_px": 800, + "margin_size_px": 20, + "board_image_path": "charuco_board.png", + } + ), + encoding="utf-8", + ) + intrinsics_path = ctx.session_directory / "calibration.json" + intrinsics_path.write_text( + json.dumps( + { + "mse_reproj_error": 0.1, + "image_height": 10, + "image_width": 12, + "intrinsic_type": "FISHEYE", + "intrinsics": { + "aspect_ratio": 0.5, + "focal_length": 10.0, + "principal_pt_x": 5.0, + "principal_pt_y": 6.0, + "radial_distortion_1": 0.1, + "radial_distortion_2": 0.2, + "radial_distortion_3": 0.3, + "radial_distortion_4": 0.4, + "skew": 0.0, + }, + } + ), + encoding="utf-8", + ) + + result = opai.verify_calibrated_parameters( + video_path=video_path, + n_check_imgs=2, + charuco_config_json="charuco_config.json", + intrinsics_json="calibration.json", + plot_result=True, + plot_nrows=1, + plot_ncols=2, + ) + + assert result.sampled_image_count == 2 + assert result.verified_image_count == 2 + assert result.total_detected_corner_count == 8 + assert len(plot_calls) == 1 + assert len(plot_calls[0][0]) == 2 + assert plot_calls[0][1] == {"nrows": 1, "ncols": 2, "frames_are_bgr": True} + output_path = ( + tmp_path / ".opai_sessions" / "session-001" / "calibration_verification.json" + ) + assert output_path.exists() + payload = json.loads(output_path.read_text(encoding="utf-8")) + assert payload["verified_image_count"] == 2 + + +def test_calibrate_with_video_requires_matplotlib_for_detected_corner_plotting( + tmp_path, + monkeypatch, +) -> None: + fake_cv2 = _build_fake_cv2() + monkeypatch.setattr(calibration_module, "cv2", fake_cv2) + monkeypatch.chdir(tmp_path) + opai.init("session-001") + + frames = tuple( + np.full((10, 12, 3), fill_value=index, dtype=np.uint8) for index in range(2) + ) + video_path = tmp_path / "demo.mp4" + video_path.write_bytes(b"demo") + monkeypatch.setattr( + calibration_module, + "sample_video_frames", + lambda **kwargs: frames, + ) + + def fake_import(name, globals=None, locals=None, fromlist=(), level=0): + if name == "matplotlib": + raise ModuleNotFoundError("No module named 'matplotlib'") + return builtin_import(name, globals, locals, fromlist, level) + + monkeypatch.setattr("builtins.__import__", fake_import) + + with pytest.raises(OPAIDependencyError, match="matplotlib"): + opai.calibrate_with_video( + video_path=video_path, + frame_sample_step=2, + row_count=3, + col_count=3, + square_length=1.0, + marker_length=0.5, + dictionary="DICT_4X4_50", + plot_result=True, + ) + + +def test_plot_video_frames_uses_auto_grid_defaults_without_context( + tmp_path, + monkeypatch, +) -> None: + frames = tuple( + np.full((10, 12, 3), fill_value=index, dtype=np.uint8) for index in range(5) + ) + video_path = tmp_path / "demo.mp4" + video_path.write_bytes(b"demo") + monkeypatch.setattr(context_store, "_ACTIVE_CONTEXT", None) + monkeypatch.setattr( + calibration_module, + "sample_video_frames", + lambda **kwargs: frames, + ) + pyplot = _build_fake_pyplot(nrows=2, ncols=3) + monkeypatch.setitem(sys.modules, "matplotlib", SimpleNamespace(pyplot=pyplot)) + monkeypatch.setitem(sys.modules, "matplotlib.pyplot", pyplot) + + opai.plot_video_frames(video_path, frame_sample_step=2) + + assert pyplot.subplots_calls == [((2, 3), {"figsize": (12.0, 6.0)})] + assert pyplot.show_count == 1 + assert pyplot.close_calls == [pyplot.figure] + assert pyplot.figure.tight_layout_calls == 1 + assert np.array_equal( + pyplot.axes[0].images[0], + frames[0][..., ::-1], + ) + assert np.shares_memory(pyplot.axes[0].images[0], frames[0]) + assert all(axis.axis_off_calls == 1 for axis in pyplot.axes[:5]) + assert pyplot.axes[5].axis_off_calls == 1 + + +def test_plot_video_frames_accepts_custom_grid(tmp_path, monkeypatch) -> None: + frames = tuple( + np.full((10, 12, 3), fill_value=index, dtype=np.uint8) for index in range(5) + ) + video_path = tmp_path / "demo.mp4" + video_path.write_bytes(b"demo") + monkeypatch.setattr( + calibration_module, + "sample_video_frames", + lambda **kwargs: frames, + ) + pyplot = _build_fake_pyplot(nrows=1, ncols=5) + monkeypatch.setitem(sys.modules, "matplotlib", SimpleNamespace(pyplot=pyplot)) + monkeypatch.setitem(sys.modules, "matplotlib.pyplot", pyplot) + + opai.plot_video_frames(video_path, frame_sample_step=2, nrows=1, ncols=5) + + assert pyplot.subplots_calls[0][0] == (1, 5) + assert pyplot.subplots_calls[0][1]["figsize"] == pytest.approx((16.0, 4.5)) + assert pyplot.show_count == 1 + assert pyplot.close_calls == [pyplot.figure] + assert all(axis.axis_off_calls == 1 for axis in pyplot.axes) + + +def test_plot_video_frames_requires_matplotlib(tmp_path, monkeypatch) -> None: + video_path = tmp_path / "demo.mp4" + video_path.write_bytes(b"demo") + monkeypatch.setattr( + calibration_module, + "sample_video_frames", + lambda **kwargs: (np.zeros((4, 4, 3), dtype=np.uint8),), + ) + + def fake_import(name, globals=None, locals=None, fromlist=(), level=0): + if name == "matplotlib": + raise ModuleNotFoundError("No module named 'matplotlib'") + return builtin_import(name, globals, locals, fromlist, level) + + monkeypatch.setattr("builtins.__import__", fake_import) + + with pytest.raises(OPAIDependencyError, match="matplotlib"): + opai.plot_video_frames(video_path, frame_sample_step=2) + + +def test_plot_video_frames_propagates_sampling_failure(tmp_path, monkeypatch) -> None: + video_path = tmp_path / "demo.mp4" + video_path.write_bytes(b"demo") + monkeypatch.setattr( + calibration_module, + "sample_video_frames", + lambda **kwargs: (_ for _ in ()).throw( + OPAIWorkflowError("Calibration failed: video sampling produced no frames.") + ), + ) + + with pytest.raises(OPAIWorkflowError, match="produced no frames"): + opai.plot_video_frames(video_path, frame_sample_step=2) + + +def test_plot_video_frames_downsamples_oversized_frames_before_plotting( + tmp_path, + monkeypatch, +) -> None: + frame = np.arange(4000 * 6000 * 3, dtype=np.uint8).reshape(4000, 6000, 3) + video_path = tmp_path / "demo.mp4" + video_path.write_bytes(b"demo") + monkeypatch.setattr( + calibration_module, + "sample_video_frames", + lambda **kwargs: (frame,), + ) + pyplot = _build_fake_pyplot(nrows=1, ncols=1) + monkeypatch.setitem(sys.modules, "matplotlib", SimpleNamespace(pyplot=pyplot)) + monkeypatch.setitem(sys.modules, "matplotlib.pyplot", pyplot) + + opai.plot_video_frames(video_path, frame_sample_step=2) + + plotted = pyplot.axes[0].images[0] + assert plotted.shape == (2000, 3000, 3) + assert np.shares_memory(plotted, frame) + + def test_add_demos_requires_context(tmp_path, monkeypatch) -> None: monkeypatch.chdir(tmp_path) monkeypatch.setattr(context_store, "_ACTIVE_CONTEXT", None) @@ -165,8 +572,39 @@ def test_list_sessions_returns_names(tmp_path, monkeypatch) -> None: monkeypatch.chdir(tmp_path) opai.init("session-b") opai.init("session-a") + recorder = _install_fake_rich(monkeypatch) assert opai.list_sessions() == ["session-a", "session-b"] + tree = recorder["prints"][0][0] + assert tree.label.startswith("[bold].opai_sessions[/]") + assert [child.label for child in tree.children] == [ + "[bold cyan]session-a[/] [dim](current, demos=0, mapping=no, files=1)[/]", + "[bold cyan]session-b[/] [dim](demos=0, mapping=no, files=1)[/]", + ] + + +def test_list_sessions_shows_empty_state(tmp_path, monkeypatch) -> None: + monkeypatch.chdir(tmp_path) + recorder = _install_fake_rich(monkeypatch) + + assert opai.list_sessions() == [] + tree = recorder["prints"][0][0] + assert tree.label.startswith("[bold].opai_sessions[/]") + assert [child.label for child in tree.children] == ["[yellow]No sessions found[/]"] + + +def test_list_sessions_requires_rich(tmp_path, monkeypatch) -> None: + monkeypatch.chdir(tmp_path) + + def fake_import(name, globals=None, locals=None, fromlist=(), level=0): + if name.startswith("rich"): + raise ModuleNotFoundError("No module named 'rich'") + return builtin_import(name, globals, locals, fromlist, level) + + monkeypatch.setattr("builtins.__import__", fake_import) + + with pytest.raises(OPAIDependencyError, match="rich"): + opai.list_sessions() def test_browse_session_returns_files_without_changing_active_context( @@ -177,12 +615,27 @@ def test_browse_session_returns_files_without_changing_active_context( demo_path = tmp_path / "demo.mp4" demo_path.write_bytes(b"demo") opai.add_demos([demo_path]) - _install_fake_rich(monkeypatch) + recorder = _install_fake_rich(monkeypatch) files = opai.browse_session("session-001") assert "captures/demos/demo-0001/demo.mp4" in files assert opai.get_context().name == ctx.name + tree = recorder["prints"][0][0] + assert tree.label.startswith("[bold].opai_sessions[/]") + session_branch = tree.children[0] + assert session_branch.label == ( + "[bold magenta]session-001[/] " + "[dim](path=session-001, demos=1, mapping=no, files=2)[/]" + ) + assert ( + session_branch.children[0].label + == f"[dim]path:[/] [cyan]{ctx.session_directory}[/]" + ) + assert [child.label for child in session_branch.children[1:]] == [ + "[bold blue]captures/[/]", + "[green]session.json[/]", + ] def test_browse_session_requires_rich(tmp_path, monkeypatch) -> None: @@ -201,6 +654,62 @@ def fake_import(name, globals=None, locals=None, fromlist=(), level=0): def _build_fake_cv2() -> SimpleNamespace: + def fake_circle( + image: np.ndarray, + center: tuple[int, int], + _radius: int, + color: tuple[int, int, int], + _thickness: int, + ) -> np.ndarray: + x, y = center + if 0 <= y < image.shape[0] and 0 <= x < image.shape[1]: + image[y, x] = np.array(color, dtype=image.dtype) + return image + + def fake_arrowed_line( + image: np.ndarray, + start: tuple[int, int], + end: tuple[int, int], + color: tuple[int, int, int], + _thickness: int, + *, + tipLength: float = 0.0, + ) -> np.ndarray: + del tipLength + midpoint = ( + int(round((start[0] + end[0]) / 2)), + int(round((start[1] + end[1]) / 2)), + ) + for x, y in (start, midpoint, end): + if 0 <= y < image.shape[0] and 0 <= x < image.shape[1]: + image[y, x] = np.array(color, dtype=image.dtype) + return image + + def fake_fisheye_calibrate( + object_points, + image_points, + image_size, + camera_matrix, + dist_coeffs, + *, + flags, + criteria, + ): + frame_count = len(object_points) + assert len(image_points) == frame_count + assert image_size == (12, 10) + assert camera_matrix.shape == (3, 3) + assert dist_coeffs.shape == (4, 1) + assert flags == 7 + assert criteria == (3, 100, 1e-6) + return ( + 0.1, + np.array([[10.0, 0.0, 5.0], [0.0, 20.0, 6.0], [0.0, 0.0, 1.0]]), + np.array([[0.1], [0.2], [0.3], [0.4]]), + [np.zeros((3, 1), dtype=np.float32) for _ in range(frame_count)], + [np.zeros((3, 1), dtype=np.float32) for _ in range(frame_count)], + ) + board = SimpleNamespace( getChessboardCorners=lambda: np.array( [ @@ -213,49 +722,74 @@ def _build_fake_cv2() -> SimpleNamespace: ) ) + detected_corners = np.array( + [ + [[1.0, 1.0]], + [[2.0, 1.0]], + [[1.0, 2.0]], + [[2.0, 2.0]], + ], + dtype=np.float32, + ) + detected_ids = np.array([[0], [1], [2], [3]], dtype=np.int32) + + class FakeCharucoDetector: + def __init__(self, _board) -> None: + self.board = _board + + def detectBoard(self, _image): + return detected_corners, detected_ids, None, None + + def fake_cvt_color(frame: np.ndarray, code: int) -> np.ndarray: + if code == 1: + return frame[:, :, 0] + if code == 2: + return np.repeat(frame[:, :, None], 3, axis=2) + raise AssertionError(f"Unexpected color conversion code: {code}") + aruco = SimpleNamespace( DICT_4X4_50=1, getPredefinedDictionary=lambda _: "dictionary", CharucoBoard=lambda *args, **kwargs: board, - detectMarkers=lambda *args, **kwargs: ( - [np.zeros((4, 1, 2), dtype=np.float32)], - np.array([[0]], dtype=np.int32), - None, - ), - interpolateCornersCharuco=lambda **kwargs: ( - 4, - np.array( - [ - [[1.0, 1.0]], - [[2.0, 1.0]], - [[1.0, 2.0]], - [[2.0, 2.0]], - ], - dtype=np.float32, - ), - np.array([[0], [1], [2], [3]], dtype=np.int32), - ), - calibrateCameraCharuco=lambda **kwargs: ( - 0.1, - np.array([[10.0, 0.0, 5.0], [0.0, 20.0, 6.0], [0.0, 0.0, 1.0]]), - np.array([0.1, 0.2, 0.3, 0.4]), - [np.zeros((3, 1), dtype=np.float32)], - [np.zeros((3, 1), dtype=np.float32)], - ), + CharucoDetector=FakeCharucoDetector, + drawDetectedCornersCharuco=lambda **kwargs: kwargs["image"], + ) + fisheye = SimpleNamespace( + CALIB_RECOMPUTE_EXTRINSIC=1, + CALIB_CHECK_COND=2, + CALIB_FIX_SKEW=4, + calibrate=fake_fisheye_calibrate, + projectPoints=lambda object_points, *_args: (object_points[:, :, :2], None), ) return SimpleNamespace( aruco=aruco, + fisheye=fisheye, + TERM_CRITERIA_EPS=1, + TERM_CRITERIA_MAX_ITER=2, COLOR_BGR2GRAY=1, - cvtColor=lambda frame, _: frame[:, :, 0], - projectPoints=lambda **kwargs: (kwargs["objectPoints"][:, :, :2], None), + COLOR_GRAY2BGR=2, + cvtColor=fake_cvt_color, + arrowedLine=fake_arrowed_line, + circle=fake_circle, + solvePnP=lambda *_args, **_kwargs: ( + True, + np.zeros((3, 1), dtype=np.float64), + np.zeros((3, 1), dtype=np.float64), + ), + waitKey=lambda _delay: 0, ) -def _install_fake_rich(monkeypatch: pytest.MonkeyPatch) -> None: +def _install_fake_rich( + monkeypatch: pytest.MonkeyPatch, +) -> dict[str, list[tuple[object, ...]]]: + recorder: dict[str, list[tuple[object, ...]]] = {"prints": []} + class FakeTree: - def __init__(self, label: str) -> None: + def __init__(self, label: str, **kwargs) -> None: self.label = label + self.kwargs = kwargs self.children = [] def add(self, label: str): @@ -265,10 +799,51 @@ def add(self, label: str): class FakeConsole: def print(self, *_args, **_kwargs) -> None: - return None + recorder["prints"].append(_args) monkeypatch.setitem(sys.modules, "rich", SimpleNamespace()) monkeypatch.setitem( sys.modules, "rich.console", SimpleNamespace(Console=FakeConsole) ) monkeypatch.setitem(sys.modules, "rich.tree", SimpleNamespace(Tree=FakeTree)) + return recorder + + +def _build_fake_pyplot(*, nrows: int, ncols: int): + class FakeAxis: + def __init__(self) -> None: + self.images: list[np.ndarray] = [] + self.axis_off_calls = 0 + + def imshow(self, image: np.ndarray) -> None: + self.images.append(image) + + def set_axis_off(self) -> None: + self.axis_off_calls += 1 + + class FakeFigure: + def __init__(self) -> None: + self.tight_layout_calls = 0 + + def tight_layout(self) -> None: + self.tight_layout_calls += 1 + + class FakePyplot: + def __init__(self) -> None: + self.figure = FakeFigure() + self.axes = [FakeAxis() for _ in range(nrows * ncols)] + self.subplots_calls: list[tuple[tuple[int, int], dict[str, object]]] = [] + self.show_count = 0 + self.close_calls: list[FakeFigure] = [] + + def subplots(self, rows: int, cols: int, **kwargs): + self.subplots_calls.append(((rows, cols), kwargs)) + return self.figure, np.array(self.axes, dtype=object).reshape(rows, cols) + + def show(self) -> None: + self.show_count += 1 + + def close(self, figure) -> None: + self.close_calls.append(figure) + + return FakePyplot() diff --git a/uv.lock b/uv.lock index 6034924..3e1ee26 100644 --- a/uv.lock +++ b/uv.lock @@ -384,6 +384,172 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, ] +[[package]] +name = "contourpy" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.12' and python_full_version < '3.14'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/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 = "debugpy" version = "1.8.20" @@ -445,7 +611,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -479,6 +645,63 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, ] +[[package]] +name = "fonttools" +version = "4.62.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/ff/532ed43808b469c807e8cb6b21358da3fe6fd51486b3a8c93db0bb5d957f/fonttools-4.62.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ad5cca75776cd453b1b035b530e943334957ae152a36a88a320e779d61fc980c", size = 2873740, upload-time = "2026-03-13T13:52:11.822Z" }, + { url = "https://files.pythonhosted.org/packages/85/e4/2318d2b430562da7227010fb2bb029d2fa54d7b46443ae8942bab224e2a0/fonttools-4.62.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b3ae47e8636156a9accff64c02c0924cbebad62854c4a6dbdc110cd5b4b341a", size = 2417649, upload-time = "2026-03-13T13:52:14.605Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/40f15523b5188598018e7956899fed94eb7debec89e2dd70cb4a8df90492/fonttools-4.62.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b9e288b4da2f64fd6180644221749de651703e8d0c16bd4b719533a3a7d6e3", size = 4935213, upload-time = "2026-03-13T13:52:17.399Z" }, + { url = "https://files.pythonhosted.org/packages/42/09/7dbe3d7023f57d9b580cfa832109d521988112fd59dddfda3fddda8218f9/fonttools-4.62.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bca7a1c1faf235ffe25d4f2e555246b4750220b38de8261d94ebc5ce8a23c23", size = 4892374, upload-time = "2026-03-13T13:52:20.175Z" }, + { url = "https://files.pythonhosted.org/packages/d1/2d/84509a2e32cb925371560ef5431365d8da2183c11d98e5b4b8b4e42426a5/fonttools-4.62.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4e0fcf265ad26e487c56cb12a42dffe7162de708762db951e1b3f755319507d", size = 4911856, upload-time = "2026-03-13T13:52:22.777Z" }, + { url = "https://files.pythonhosted.org/packages/a5/80/df28131379eed93d9e6e6fccd3bf6e3d077bebbfe98cc83f21bbcd83ed02/fonttools-4.62.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2d850f66830a27b0d498ee05adb13a3781637b1826982cd7e2b3789ef0cc71ae", size = 5031712, upload-time = "2026-03-13T13:52:25.14Z" }, + { url = "https://files.pythonhosted.org/packages/3d/03/3c8f09aad64230cd6d921ae7a19f9603c36f70930b00459f112706f6769a/fonttools-4.62.1-cp310-cp310-win32.whl", hash = "sha256:486f32c8047ccd05652aba17e4a8819a3a9d78570eb8a0e3b4503142947880ed", size = 1507878, upload-time = "2026-03-13T13:52:28.149Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ec/f53f626f8f3e89f4cadd8fc08f3452c8fd182c951ad5caa35efac22b29ab/fonttools-4.62.1-cp310-cp310-win_amd64.whl", hash = "sha256:5a648bde915fba9da05ae98856987ca91ba832949a9e2888b48c47ef8b96c5a9", size = 1556766, upload-time = "2026-03-13T13:52:30.814Z" }, + { url = "https://files.pythonhosted.org/packages/88/39/23ff32561ec8d45a4d48578b4d241369d9270dc50926c017570e60893701/fonttools-4.62.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7", size = 2871039, upload-time = "2026-03-13T13:52:33.127Z" }, + { url = "https://files.pythonhosted.org/packages/24/7f/66d3f8a9338a9b67fe6e1739f47e1cd5cee78bd3bc1206ef9b0b982289a5/fonttools-4.62.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14", size = 2416346, upload-time = "2026-03-13T13:52:35.676Z" }, + { url = "https://files.pythonhosted.org/packages/aa/53/5276ceba7bff95da7793a07c5284e1da901cf00341ce5e2f3273056c0cca/fonttools-4.62.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6acb4109f8bee00fec985c8c7afb02299e35e9c94b57287f3ea542f28bd0b0a7", size = 5100897, upload-time = "2026-03-13T13:52:38.102Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a1/40a5c4d8e28b0851d53a8eeeb46fbd73c325a2a9a165f290a5ed90e6c597/fonttools-4.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b", size = 5071078, upload-time = "2026-03-13T13:52:41.305Z" }, + { url = "https://files.pythonhosted.org/packages/e3/be/d378fca4c65ea1956fee6d90ace6e861776809cbbc5af22388a090c3c092/fonttools-4.62.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a5d8825e1140f04e6c99bb7d37a9e31c172f3bc208afbe02175339e699c710e1", size = 5076908, upload-time = "2026-03-13T13:52:44.122Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d9/ae6a1d0693a4185a84605679c8a1f719a55df87b9c6e8e817bfdd9ef5936/fonttools-4.62.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:268abb1cb221e66c014acc234e872b7870d8b5d4657a83a8f4205094c32d2416", size = 5202275, upload-time = "2026-03-13T13:52:46.591Z" }, + { url = "https://files.pythonhosted.org/packages/54/6c/af95d9c4efb15cabff22642b608342f2bd67137eea6107202d91b5b03184/fonttools-4.62.1-cp311-cp311-win32.whl", hash = "sha256:942b03094d7edbb99bdf1ae7e9090898cad7bf9030b3d21f33d7072dbcb51a53", size = 2293075, upload-time = "2026-03-13T13:52:48.711Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/bf54c5b3f2be34e1f143e6db838dfdc54f2ffa3e68c738934c82f3b2a08d/fonttools-4.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2", size = 2344593, upload-time = "2026-03-13T13:52:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/dbacced3953544b9a93088cc10ef2b596d348c983d5c67a404fa41ec51ba/fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974", size = 2870219, upload-time = "2026-03-13T13:52:53.664Z" }, + { url = "https://files.pythonhosted.org/packages/66/9e/a769c8e99b81e5a87ab7e5e7236684de4e96246aae17274e5347d11ebd78/fonttools-4.62.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9", size = 2414891, upload-time = "2026-03-13T13:52:56.493Z" }, + { url = "https://files.pythonhosted.org/packages/69/64/f19a9e3911968c37e1e620e14dfc5778299e1474f72f4e57c5ec771d9489/fonttools-4.62.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936", size = 5033197, upload-time = "2026-03-13T13:52:59.179Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392", size = 4988768, upload-time = "2026-03-13T13:53:02.761Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/0f904540d3e6ab463c1243a0d803504826a11604c72dd58c2949796a1762/fonttools-4.62.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04", size = 4971512, upload-time = "2026-03-13T13:53:05.678Z" }, + { url = "https://files.pythonhosted.org/packages/29/0b/5cbef6588dc9bd6b5c9ad6a4d5a8ca384d0cea089da31711bbeb4f9654a6/fonttools-4.62.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d", size = 5122723, upload-time = "2026-03-13T13:53:08.662Z" }, + { url = "https://files.pythonhosted.org/packages/4a/47/b3a5342d381595ef439adec67848bed561ab7fdb1019fa522e82101b7d9c/fonttools-4.62.1-cp312-cp312-win32.whl", hash = "sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c", size = 2281278, upload-time = "2026-03-13T13:53:10.998Z" }, + { url = "https://files.pythonhosted.org/packages/28/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42", size = 2331414, upload-time = "2026-03-13T13:53:13.992Z" }, + { url = "https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79", size = 2865155, upload-time = "2026-03-13T13:53:16.132Z" }, + { url = "https://files.pythonhosted.org/packages/03/c5/0e3966edd5ec668d41dfe418787726752bc07e2f5fd8c8f208615e61fa89/fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe", size = 2412802, upload-time = "2026-03-13T13:53:18.878Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/e6ac4b44026de7786fe46e3bfa0c87e51d5d70a841054065d49cd62bb909/fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68", size = 5013926, upload-time = "2026-03-13T13:53:21.379Z" }, + { url = "https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1", size = 4964575, upload-time = "2026-03-13T13:53:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/46/76/7d051671e938b1881670528fec69cc4044315edd71a229c7fd712eaa5119/fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069", size = 4953693, upload-time = "2026-03-13T13:53:26.569Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ae/b41f8628ec0be3c1b934fc12b84f4576a5c646119db4d3bdd76a217c90b5/fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9", size = 5094920, upload-time = "2026-03-13T13:53:29.329Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/53a1e9469331a23dcc400970a27a4caa3d9f6edbf5baab0260285238b884/fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24", size = 2279928, upload-time = "2026-03-13T13:53:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056", size = 2330514, upload-time = "2026-03-13T13:53:34.991Z" }, + { url = "https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca", size = 2864442, upload-time = "2026-03-13T13:53:37.509Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b2/e521803081f8dc35990816b82da6360fa668a21b44da4b53fc9e77efcd62/fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca", size = 2410901, upload-time = "2026-03-13T13:53:40.55Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/8c3511ff06e53110039358dbbdc1a65d72157a054638387aa2ada300a8b8/fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782", size = 4999608, upload-time = "2026-03-13T13:53:42.798Z" }, + { url = "https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae", size = 4912726, upload-time = "2026-03-13T13:53:45.405Z" }, + { url = "https://files.pythonhosted.org/packages/70/b9/ac677cb07c24c685cf34f64e140617d58789d67a3dd524164b63648c6114/fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7", size = 4951422, upload-time = "2026-03-13T13:53:48.326Z" }, + { url = "https://files.pythonhosted.org/packages/e6/10/11c08419a14b85b7ca9a9faca321accccc8842dd9e0b1c8a72908de05945/fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a", size = 5060979, upload-time = "2026-03-13T13:53:51.366Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3c/12eea4a4cf054e7ab058ed5ceada43b46809fce2bf319017c4d63ae55bb4/fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800", size = 2283733, upload-time = "2026-03-13T13:53:53.606Z" }, + { url = "https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e", size = 2335663, upload-time = "2026-03-13T13:53:56.23Z" }, + { url = "https://files.pythonhosted.org/packages/42/c5/4d2ed3ca6e33617fc5624467da353337f06e7f637707478903c785bd8e20/fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82", size = 2947288, upload-time = "2026-03-13T13:53:59.397Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e9/7ab11ddfda48ed0f89b13380e5595ba572619c27077be0b2c447a63ff351/fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260", size = 2449023, upload-time = "2026-03-13T13:54:01.642Z" }, + { url = "https://files.pythonhosted.org/packages/b2/10/a800fa090b5e8819942e54e19b55fc7c21fe14a08757c3aa3ca8db358939/fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4", size = 5137599, upload-time = "2026-03-13T13:54:04.495Z" }, + { url = "https://files.pythonhosted.org/packages/37/dc/8ccd45033fffd74deb6912fa1ca524643f584b94c87a16036855b498a1ed/fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b", size = 4920933, upload-time = "2026-03-13T13:54:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/99/eb/e618adefb839598d25ac8136cd577925d6c513dc0d931d93b8af956210f0/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87", size = 5016232, upload-time = "2026-03-13T13:54:10.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5f/9b5c9bfaa8ec82def8d8168c4f13615990d6ce5996fe52bd49bfb5e05134/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c", size = 5042987, upload-time = "2026-03-13T13:54:13.569Z" }, + { url = "https://files.pythonhosted.org/packages/90/aa/dfbbe24c6a6afc5c203d90cc0343e24bcbb09e76d67c4d6eef8c2558d7ba/fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a", size = 2348021, upload-time = "2026-03-13T13:54:16.98Z" }, + { url = "https://files.pythonhosted.org/packages/13/6f/ae9c4e4dd417948407b680855c2c7790efb52add6009aaecff1e3bc50e8e/fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e", size = 2414147, upload-time = "2026-03-13T13:54:19.416Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" }, +] + [[package]] name = "fqdn" version = "1.5.1" @@ -914,6 +1137,130 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl", hash = "sha256:e4355b148fdcf34d312bbbc80f22467d6d20460e8b8736bf235577dd18506968", size = 59830, upload-time = "2025-10-22T13:59:16.767Z" }, ] +[[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/ac/f8/06549565caa026e540b7e7bab5c5a90eb7ca986015f4c48dace243cd24d9/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374", size = 122802, upload-time = "2026-03-09T13:12:37.515Z" }, + { url = "https://files.pythonhosted.org/packages/84/eb/8476a0818850c563ff343ea7c9c05dcdcbd689a38e01aa31657df01f91fa/kiwisolver-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd", size = 66216, upload-time = "2026-03-09T13:12:38.812Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/f9c8a6b4c21aed4198566e45923512986d6cef530e7263b3a5f823546561/kiwisolver-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476", size = 63917, upload-time = "2026-03-09T13:12:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0e/ba4ae25d03722f64de8b2c13e80d82ab537a06b30fc7065183c6439357e3/kiwisolver-1.5.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22", size = 1628776, upload-time = "2026-03-09T13:12:41.976Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e4/3f43a011bc8a0860d1c96f84d32fa87439d3feedf66e672fef03bf5e8bac/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b", size = 1228164, upload-time = "2026-03-09T13:12:44.002Z" }, + { url = "https://files.pythonhosted.org/packages/4b/34/3a901559a1e0c218404f9a61a93be82d45cb8f44453ba43088644980f033/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e", size = 1246656, upload-time = "2026-03-09T13:12:45.557Z" }, + { url = "https://files.pythonhosted.org/packages/87/9e/f78c466ea20527822b95ad38f141f2de1dcd7f23fb8716b002b0d91bbe59/kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb", size = 1295562, upload-time = "2026-03-09T13:12:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/0a/66/fd0e4a612e3a286c24e6d6f3a5428d11258ed1909bc530ba3b59807fd980/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537", size = 2178473, upload-time = "2026-03-09T13:12:50.254Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8e/6cac929e0049539e5ee25c1ee937556f379ba5204840d03008363ced662d/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4", size = 2274035, upload-time = "2026-03-09T13:12:51.785Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d3/9d0c18f1b52ea8074b792452cf17f1f5a56bd0302a85191f405cfbf9da16/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c", size = 2443217, upload-time = "2026-03-09T13:12:53.329Z" }, + { url = "https://files.pythonhosted.org/packages/45/2a/6e19368803a038b2a90857bf4ee9e3c7b667216d045866bf22d3439fd75e/kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede", size = 2249196, upload-time = "2026-03-09T13:12:55.057Z" }, + { url = "https://files.pythonhosted.org/packages/75/2b/3f641dfcbe72e222175d626bacf2f72c3b34312afec949dd1c50afa400f5/kiwisolver-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2", size = 73389, upload-time = "2026-03-09T13:12:56.496Z" }, + { url = "https://files.pythonhosted.org/packages/da/88/299b137b9e0025d8982e03d2d52c123b0a2b159e84b0ef1501ef446339cf/kiwisolver-1.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875", size = 64782, upload-time = "2026-03-09T13:12:57.609Z" }, + { 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/17/6f/6fd4f690a40c2582fa34b97d2678f718acf3706b91d270c65ecb455d0a06/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4", size = 59606, upload-time = "2026-03-09T13:15:40.81Z" }, + { url = "https://files.pythonhosted.org/packages/82/a0/2355d5e3b338f13ce63f361abb181e3b6ea5fffdb73f739b3e80efa76159/kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca", size = 57537, upload-time = "2026-03-09T13:15:42.071Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b9/1d50e610ecadebe205b71d6728fd224ce0e0ca6aba7b9cbe1da049203ac5/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f", size = 79888, upload-time = "2026-03-09T13:15:43.317Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ee/b85ffcd75afed0357d74f0e6fc02a4507da441165de1ca4760b9f496390d/kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed", size = 77584, upload-time = "2026-03-09T13:15:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/6b/dd/644d0dde6010a8583b4cd66dd41c5f83f5325464d15c4f490b3340ab73b4/kiwisolver-1.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc", size = 73390, upload-time = "2026-03-09T13:15:45.832Z" }, + { 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 = "lark" version = "1.3.1" @@ -1020,6 +1367,81 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "matplotlib" +version = "3.10.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/be/a30bd917018ad220c400169fba298f2bb7003c8ccbc0c3e24ae2aacad1e8/matplotlib-3.10.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7", size = 8239828, upload-time = "2025-12-10T22:55:02.313Z" }, + { url = "https://files.pythonhosted.org/packages/58/27/ca01e043c4841078e82cf6e80a6993dfecd315c3d79f5f3153afbb8e1ec6/matplotlib-3.10.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656", size = 8128050, upload-time = "2025-12-10T22:55:04.997Z" }, + { url = "https://files.pythonhosted.org/packages/cb/aa/7ab67f2b729ae6a91bcf9dcac0affb95fb8c56f7fd2b2af894ae0b0cf6fa/matplotlib-3.10.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df", size = 8700452, upload-time = "2025-12-10T22:55:07.47Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/2d5817b0acee3c49b7e7ccfbf5b273f284957cc8e270adf36375db353190/matplotlib-3.10.8-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17", size = 9534928, upload-time = "2025-12-10T22:55:10.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5b/8e66653e9f7c39cb2e5cab25fce4810daffa2bff02cbf5f3077cea9e942c/matplotlib-3.10.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933", size = 9586377, upload-time = "2025-12-10T22:55:12.362Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/fd0bbadf837f81edb0d208ba8f8cb552874c3b16e27cb91a31977d90875d/matplotlib-3.10.8-cp310-cp310-win_amd64.whl", hash = "sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a", size = 8128127, upload-time = "2025-12-10T22:55:14.436Z" }, + { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" }, + { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" }, + { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, + { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, + { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" }, + { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" }, + { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" }, + { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" }, + { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" }, + { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" }, + { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" }, + { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" }, + { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" }, + { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/f5/43/31d59500bb950b0d188e149a2e552040528c13d6e3d6e84d0cccac593dcd/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8", size = 8237252, upload-time = "2025-12-10T22:56:39.529Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2c/615c09984f3c5f907f51c886538ad785cf72e0e11a3225de2c0f9442aecc/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7", size = 8124693, upload-time = "2025-12-10T22:56:41.758Z" }, + { url = "https://files.pythonhosted.org/packages/91/e1/2757277a1c56041e1fc104b51a0f7b9a4afc8eb737865d63cababe30bc61/matplotlib-3.10.8-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3", size = 8702205, upload-time = "2025-12-10T22:56:43.415Z" }, + { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" }, + { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, +] + [[package]] name = "matplotlib-inline" version = "0.2.1" @@ -1293,7 +1715,10 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "jupyterlab" }, - { name = "opencv-contrib-python" }, + { name = "matplotlib" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "opencv-contrib-python-headless" }, { name = "rich" }, ] @@ -1308,7 +1733,9 @@ dev = [ [package.metadata] requires-dist = [ { name = "jupyterlab", specifier = ">=4.5.6" }, - { name = "opencv-contrib-python", specifier = ">=4.13.0.92" }, + { name = "matplotlib", specifier = ">=3.10.7" }, + { name = "numpy", specifier = ">=2.2.6" }, + { name = "opencv-contrib-python-headless", specifier = ">=4.13.0.92" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = "==3.3.2" }, { name = "pyright", marker = "extra == 'dev'", specifier = ">=1.1.308" }, { name = "pytest", marker = "extra == 'dev'" }, @@ -1318,7 +1745,7 @@ requires-dist = [ provides-extras = ["dev"] [[package]] -name = "opencv-contrib-python" +name = "opencv-contrib-python-headless" version = "4.13.0.92" source = { registry = "https://pypi.org/simple" } dependencies = [ @@ -1326,14 +1753,14 @@ dependencies = [ { name = "numpy", version = "2.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/4c/a45c96b9fe90b2c48ee604f5176eb7deb46ce7c2e87c8d819d2945dbcab6/opencv_contrib_python-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:53c8ab81376210dda5836307eb6bda7266f39a3820a9a070c7131510ba815fe1", size = 52041546, upload-time = "2026-02-05T07:01:29.918Z" }, - { url = "https://files.pythonhosted.org/packages/8b/6c/ba1f3177927deeb3002b62fb8db89daea3b5dc732d61de5bf4c73ed6ebf7/opencv_contrib_python-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:1973d0fc773873f9d1b5bf0d1b65895da2f47b06ba033b7d58393f5c28ba0778", size = 38830319, upload-time = "2026-02-05T07:01:47.222Z" }, - { url = "https://files.pythonhosted.org/packages/ff/7a/fe87eaf109b454af4a2579f46958b3cafb0f804b9c788c108760723a9bb7/opencv_contrib_python-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f9cb522dd9e465dfca3536c15288f7936b9827432fb9c885eaf94dc5f88c2a3", size = 53339457, upload-time = "2026-02-05T10:09:02.332Z" }, - { url = "https://files.pythonhosted.org/packages/b6/27/3665ca4b75ddfd218f9ab139f0463d9571e87aaf59391d3c4f5546c08df7/opencv_contrib_python-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f9ceec5886419860a31b518991a99e978e5a6a78dca1470103ad4ede0155f156", size = 76591184, upload-time = "2026-02-05T10:11:51.298Z" }, - { url = "https://files.pythonhosted.org/packages/f3/11/10c46e9527c4591d5264117debd8fe0e21bb23dbf378ce760add6b1e85b6/opencv_contrib_python-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a3c54377c5cf9c45d9b1a207df26dc8fe4f1042d07036cb17d80930c04b25d97", size = 52544155, upload-time = "2026-02-05T10:13:32.068Z" }, - { url = "https://files.pythonhosted.org/packages/ba/f6/3c645c21358079097201090de7c30d110f5ec3fa01008e3ee81b0a77a354/opencv_contrib_python-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:fc5ee50e2be9d40e913536f7f20cc6f87f25d8e413ebb32a3335ab6edf245d3e", size = 79150872, upload-time = "2026-02-05T10:16:03.465Z" }, - { url = "https://files.pythonhosted.org/packages/90/d7/bf4622e0ed8a93f5a685c76933e287477cf185a160c66478cf144fece489/opencv_contrib_python-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:f5d02357f4d5575c300eab3ec1c7ecfed3a9a53e55a76927bab7cfc9e0a67b68", size = 36829959, upload-time = "2026-02-05T07:02:22.25Z" }, - { url = "https://files.pythonhosted.org/packages/d9/98/a03f69ff6fb86a67d584ecc990d85a95e6930b96e3f39ad1f8e019cb8ada/opencv_contrib_python-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:cb694dcf76bb2c8d7fa573fc1a99339e8b6640194d7778381e74cc3445369e45", size = 46486178, upload-time = "2026-02-05T07:02:19.551Z" }, + { url = "https://files.pythonhosted.org/packages/70/b5/9af5b81d9279e9982e21dad52f8a6aec10f7c891ae1e3d3d1b3ce111f8e7/opencv_contrib_python_headless-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:b0467988c2d56c283b00fb808e0b57f5db2e3ca7743164a3b3efc733bfa03d3a", size = 52041681, upload-time = "2026-02-05T07:01:39.651Z" }, + { url = "https://files.pythonhosted.org/packages/c5/42/f0aef27baf1f376007b018b00f6c304c42c20d31aa8491633c53b18912cb/opencv_contrib_python_headless-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:79e503b77880d806a1b106ff8182c6f898347ccfd1db58ffc9a6369acc236c4c", size = 38830456, upload-time = "2026-02-05T07:01:56.47Z" }, + { url = "https://files.pythonhosted.org/packages/14/84/e6b3568f9147b4f114e881fb0e733fd97bdca15452feba78b510351584d1/opencv_contrib_python_headless-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:449c1f00a685a3a7dff8d6fa93a70fbfe0de5537c24358ea03a1d996d12b33e8", size = 39355323, upload-time = "2026-02-05T10:17:31.671Z" }, + { url = "https://files.pythonhosted.org/packages/6e/09/89714580c617cf6e9f66eed9137759fc017ab6ab093c2a03227e8ee19578/opencv_contrib_python_headless-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c9b028adc04f6579f37227eb1d648bead14fd6fefc58da86df37c8320351f7bd", size = 62147375, upload-time = "2026-02-05T10:20:03.076Z" }, + { url = "https://files.pythonhosted.org/packages/6f/29/abdd2ff2f8f07e9aa37c70edc9987b8aa63730ae70957c378f6f2e9d72d2/opencv_contrib_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:cdd974b34801f24735d18b1057cfaab1698d5cb02c9bba01dab7dc47201f2ef6", size = 40840722, upload-time = "2026-02-05T10:21:27.877Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0a/3194fdf035ef5123bd8cc3e3ad1a96c1ddeeedd0fdd12aaa0d2cfeb1649a/opencv_contrib_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9e26469baed9069f627ea56fa46819690c4545580362071dd09f1dcf47a40f2f", size = 66610130, upload-time = "2026-02-05T10:23:32.427Z" }, + { url = "https://files.pythonhosted.org/packages/ca/4b/afe9b43c02b86b675a3d3ac6fc220473a88016e1acb487f5138efd2d2630/opencv_contrib_python_headless-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:696a6dd84d309a499efc63644e375f035447b1da777faa2954f2dea7626cc0e7", size = 36708602, upload-time = "2026-02-05T07:02:36.041Z" }, + { url = "https://files.pythonhosted.org/packages/23/22/9fdc70520eb915b46d816f9cc5415458b1bd114a65d7a7e657cbd9b863e5/opencv_contrib_python_headless-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:dcbb12d04ae74f5dcd782e3b166e1894c6fbdfaaf30866588746205d2a0cde5a", size = 46345416, upload-time = "2026-02-05T07:02:33.446Z" }, ] [[package]] @@ -1384,6 +1811,104 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, ] +[[package]] +name = "pillow" +version = "12.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/30/5bd3d794762481f8c8ae9c80e7b76ecea73b916959eb587521358ef0b2f9/pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0", size = 5304099, upload-time = "2026-02-11T04:20:06.13Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c1/aab9e8f3eeb4490180e357955e15c2ef74b31f64790ff356c06fb6cf6d84/pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713", size = 4657880, upload-time = "2026-02-11T04:20:09.291Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0a/9879e30d56815ad529d3985aeff5af4964202425c27261a6ada10f7cbf53/pillow-12.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b", size = 6222587, upload-time = "2026-02-11T04:20:10.82Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5f/a1b72ff7139e4f89014e8d451442c74a774d5c43cd938fb0a9f878576b37/pillow-12.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b", size = 8027678, upload-time = "2026-02-11T04:20:12.455Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c2/c7cb187dac79a3d22c3ebeae727abee01e077c8c7d930791dc592f335153/pillow-12.1.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4", size = 6335777, upload-time = "2026-02-11T04:20:14.441Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7b/f9b09a7804ec7336effb96c26d37c29d27225783dc1501b7d62dcef6ae25/pillow-12.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4", size = 7027140, upload-time = "2026-02-11T04:20:16.387Z" }, + { url = "https://files.pythonhosted.org/packages/98/b2/2fa3c391550bd421b10849d1a2144c44abcd966daadd2f7c12e19ea988c4/pillow-12.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e", size = 6449855, upload-time = "2026-02-11T04:20:18.554Z" }, + { url = "https://files.pythonhosted.org/packages/96/ff/9caf4b5b950c669263c39e96c78c0d74a342c71c4f43fd031bb5cb7ceac9/pillow-12.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff", size = 7151329, upload-time = "2026-02-11T04:20:20.646Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f8/4b24841f582704da675ca535935bccb32b00a6da1226820845fac4a71136/pillow-12.1.1-cp310-cp310-win32.whl", hash = "sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40", size = 6325574, upload-time = "2026-02-11T04:20:22.43Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f9/9f6b01c0881d7036063aa6612ef04c0e2cad96be21325a1e92d0203f8e91/pillow-12.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23", size = 7032347, upload-time = "2026-02-11T04:20:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/79/13/c7922edded3dcdaf10c59297540b72785620abc0538872c819915746757d/pillow-12.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9", size = 2453457, upload-time = "2026-02-11T04:20:25.392Z" }, + { url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" }, + { url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" }, + { url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" }, + { url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" }, + { url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" }, + { url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, + { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, + { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, + { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, + { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" }, + { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" }, + { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" }, + { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" }, + { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, + { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" }, + { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, + { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" }, + { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, + { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" }, + { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" }, + { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" }, + { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" }, + { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" }, + { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" }, + { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" }, + { url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" }, + { url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" }, + { url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" }, + { url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" }, +] + [[package]] name = "platformdirs" version = "4.9.4" @@ -1503,6 +2028,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "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 = "pyright" version = "1.1.408"