diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 8e24765e2d..3878d68ccd 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -8,7 +8,7 @@ This file provides context-aware guidance for GitHub Copilot when working in the **Supervision** is a Python library providing reusable computer vision utilities for working with object detection models (YOLO, SAM, etc.). It offers tools for detections processing, tracking, annotation, and dataset management. -- **Languages**: Python 3.9+ +- **Languages**: Python 3.10+ - **Key Dependencies**: NumPy, OpenCV, SciPy - **License**: MIT diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 1933fdc7b2..fc385fc757 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -29,7 +29,7 @@ jobs: fail-fast: false matrix: os: ["ubuntu-latest", "windows-latest", "macos-latest"] - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13"] runs-on: ${{ matrix.os }} steps: - name: 📥 Checkout the repository diff --git a/README.md b/README.md index 3f35d27cd5..313cf6e8d6 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ ## 💻 install Pip install the supervision package in a -[**Python>=3.9**](https://www.python.org/) environment. +[**Python>=3.10**](https://www.python.org/) environment. ```bash pip install supervision diff --git a/docs/changelog.md b/docs/changelog.md index 1303ab4a0a..000d79ea18 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,7 +5,13 @@ date_modified: 2026-04-30 # Changelog -### UnReleased +### UnReleased upcoming + +!!! failure "Python 3.9 Support Terminated" + + With the `supervision-0.29.0` release, we are terminating official support for Python 3.9, which reached end-of-life in October 2025. The minimum supported Python version is now **3.10**. + + Users on Python 3.9 should upgrade their environment before updating supervision. - Added [#2267](https://github.com/roboflow/supervision/pull/2267): [`DetectionDataset.as_coco`](https://supervision.roboflow.com/latest/datasets/core/#supervision.dataset.core.DetectionDataset.as_coco) and `save_coco_annotations` now accept `starting_image_id` and `starting_annotation_id` parameters (both default to `1`, preserving existing behavior) and return a `(next_image_id, next_annotation_id)` tuple. Feed the returned values into the next split's call to produce globally unique COCO ids across train/valid/test exports. Fixes id collisions reported in [#768](https://github.com/roboflow/supervision/issues/768). **Note**: the return type changes from `None` to `tuple[int, int]` — callers that assert `result is None` must be updated. diff --git a/examples/compact_mask/benchmark.py b/examples/compact_mask/benchmark.py index 7a7a1fb3e8..052e1b4683 100644 --- a/examples/compact_mask/benchmark.py +++ b/examples/compact_mask/benchmark.py @@ -20,11 +20,11 @@ import math import time import tracemalloc +from collections.abc import Callable from concurrent.futures import ThreadPoolExecutor from dataclasses import dataclass, field from datetime import datetime, timezone from pathlib import Path -from typing import Callable import cv2 import numpy as np diff --git a/examples/count_people_in_zone/inference_example.py b/examples/count_people_in_zone/inference_example.py index 677af69391..2b21126671 100644 --- a/examples/count_people_in_zone/inference_example.py +++ b/examples/count_people_in_zone/inference_example.py @@ -135,7 +135,7 @@ def main( target_video_path: str | None = None, confidence_threshold: float = 0.3, iou_threshold: float = 0.7, -): +) -> None: """ Counting people in zones with Inference and Supervision. diff --git a/examples/count_people_in_zone/ultralytics_example.py b/examples/count_people_in_zone/ultralytics_example.py index 35a09616f0..6caf3c237e 100644 --- a/examples/count_people_in_zone/ultralytics_example.py +++ b/examples/count_people_in_zone/ultralytics_example.py @@ -133,7 +133,7 @@ def main( target_video_path: str | None = None, confidence_threshold: float = 0.3, iou_threshold: float = 0.7, -): +) -> None: """ Counting people in zones with YOLO and Supervision. diff --git a/examples/heatmap_and_track/script.py b/examples/heatmap_and_track/script.py index dcbbe12f92..5a8667ee1c 100644 --- a/examples/heatmap_and_track/script.py +++ b/examples/heatmap_and_track/script.py @@ -1,123 +1,121 @@ -from typing import Optional - -import cv2 -from ultralytics import YOLO - -import supervision as sv -from supervision.assets import VideoAssets, download_assets - - -def download_video() -> str: - download_assets(VideoAssets.PEOPLE_WALKING) - return VideoAssets.PEOPLE_WALKING.value - - -def main( - source_weights_path: str, - source_video_path: Optional[str] = None, - target_video_path: str = "output.mp4", - confidence_threshold: float = 0.35, - iou_threshold: float = 0.5, - heatmap_alpha: float = 0.5, - radius: int = 25, - track_activation_threshold: float = 0.35, - track_seconds: int = 5, - minimum_matching_threshold: float = 0.99, -) -> None: - """ - Heatmap and Tracking with Supervision. - - Args: - source_weights_path: Path to the source weights file - source_video_path: Path to the source video file - target_video_path: Path to the target video file - confidence_threshold: Confidence threshold for the model - iou_threshold: IOU threshold for the model - heatmap_alpha: Opacity of the overlay mask, between 0 and 1 - radius: Radius of the heat circle - track_activation_threshold: Detection confidence threshold for track activation - track_seconds: Number of seconds to buffer when a track is lost - minimum_matching_threshold: Threshold for matching tracks with detections - """ - ### instantiate model - model = YOLO(source_weights_path) - source_video_path = source_video_path or download_video() - - ### heatmap config - heat_map_annotator = sv.HeatMapAnnotator( - position=sv.Position.BOTTOM_CENTER, - opacity=heatmap_alpha, - radius=radius, - kernel_size=25, - top_hue=0, - low_hue=125, - ) - - ### annotation config - label_annotator = sv.LabelAnnotator(text_position=sv.Position.CENTER) - - ### get the video fps - cap = cv2.VideoCapture(source_video_path) - fps = int(cap.get(cv2.CAP_PROP_FPS)) - cap.release() - - ### tracker config - byte_tracker = sv.ByteTrack( - track_activation_threshold=track_activation_threshold, - lost_track_buffer=track_seconds * fps, - minimum_matching_threshold=minimum_matching_threshold, - frame_rate=fps, - ) - - ### video config - video_info = sv.VideoInfo.from_video_path(video_path=source_video_path) - frames_generator = sv.get_video_frames_generator( - source_path=source_video_path, stride=1 - ) - - ### Detect, track, annotate, save - with sv.VideoSink(target_path=target_video_path, video_info=video_info) as sink: - for frame in frames_generator: - result = model( - source=frame, - classes=[0], # only person class - conf=confidence_threshold, - iou=iou_threshold, - # show_conf = True, - # save_txt = True, - # save_conf = True, - # save = True, - device=None, # use None = CPU, 0 = single GPU, or [0,1] = dual GPU - )[0] - - detections = sv.Detections.from_ultralytics(result) # get detections - - detections = byte_tracker.update_with_detections( - detections - ) # update tracker - - ### draw heatmap - annotated_frame = heat_map_annotator.annotate( - scene=frame.copy(), detections=detections - ) - - ### draw other attributes from `detections` object - labels = [ - f"#{tracker_id}" - for class_id, tracker_id in zip( - detections.class_id, detections.tracker_id - ) - ] - - label_annotator.annotate( - scene=annotated_frame, detections=detections, labels=labels - ) - - sink.write_frame(frame=annotated_frame) - - -if __name__ == "__main__": - from jsonargparse import auto_cli, set_parsing_settings - - set_parsing_settings(parse_optionals_as_positionals=True) - auto_cli(main, as_positional=False) +import cv2 +from ultralytics import YOLO + +import supervision as sv +from supervision.assets import VideoAssets, download_assets + + +def download_video() -> str: + download_assets(VideoAssets.PEOPLE_WALKING) + return VideoAssets.PEOPLE_WALKING.value + + +def main( + source_weights_path: str, + source_video_path: str | None = None, + target_video_path: str = "output.mp4", + confidence_threshold: float = 0.35, + iou_threshold: float = 0.5, + heatmap_alpha: float = 0.5, + radius: int = 25, + track_activation_threshold: float = 0.35, + track_seconds: int = 5, + minimum_matching_threshold: float = 0.99, +) -> None: + """ + Heatmap and Tracking with Supervision. + + Args: + source_weights_path: Path to the source weights file + source_video_path: Path to the source video file + target_video_path: Path to the target video file + confidence_threshold: Confidence threshold for the model + iou_threshold: IOU threshold for the model + heatmap_alpha: Opacity of the overlay mask, between 0 and 1 + radius: Radius of the heat circle + track_activation_threshold: Detection confidence threshold for track activation + track_seconds: Number of seconds to buffer when a track is lost + minimum_matching_threshold: Threshold for matching tracks with detections + """ + ### instantiate model + model = YOLO(source_weights_path) + source_video_path = source_video_path or download_video() + + ### heatmap config + heat_map_annotator = sv.HeatMapAnnotator( + position=sv.Position.BOTTOM_CENTER, + opacity=heatmap_alpha, + radius=radius, + kernel_size=25, + top_hue=0, + low_hue=125, + ) + + ### annotation config + label_annotator = sv.LabelAnnotator(text_position=sv.Position.CENTER) + + ### get the video fps + cap = cv2.VideoCapture(source_video_path) + fps = int(cap.get(cv2.CAP_PROP_FPS)) + cap.release() + + ### tracker config + byte_tracker = sv.ByteTrack( + track_activation_threshold=track_activation_threshold, + lost_track_buffer=track_seconds * fps, + minimum_matching_threshold=minimum_matching_threshold, + frame_rate=fps, + ) + + ### video config + video_info = sv.VideoInfo.from_video_path(video_path=source_video_path) + frames_generator = sv.get_video_frames_generator( + source_path=source_video_path, stride=1 + ) + + ### Detect, track, annotate, save + with sv.VideoSink(target_path=target_video_path, video_info=video_info) as sink: + for frame in frames_generator: + result = model( + source=frame, + classes=[0], # only person class + conf=confidence_threshold, + iou=iou_threshold, + # show_conf = True, + # save_txt = True, + # save_conf = True, + # save = True, + device=None, # use None = CPU, 0 = single GPU, or [0,1] = dual GPU + )[0] + + detections = sv.Detections.from_ultralytics(result) # get detections + + detections = byte_tracker.update_with_detections( + detections + ) # update tracker + + ### draw heatmap + annotated_frame = heat_map_annotator.annotate( + scene=frame.copy(), detections=detections + ) + + ### draw other attributes from `detections` object + labels = [ + f"#{tracker_id}" + for class_id, tracker_id in zip( + detections.class_id, detections.tracker_id + ) + ] + + label_annotator.annotate( + scene=annotated_frame, detections=detections, labels=labels + ) + + sink.write_frame(frame=annotated_frame) + + +if __name__ == "__main__": + from jsonargparse import auto_cli, set_parsing_settings + + set_parsing_settings(parse_optionals_as_positionals=True) + auto_cli(main, as_positional=False) diff --git a/examples/speed_estimation/inference_example.py b/examples/speed_estimation/inference_example.py index cb4f77c0df..21ec95b2a6 100644 --- a/examples/speed_estimation/inference_example.py +++ b/examples/speed_estimation/inference_example.py @@ -44,7 +44,7 @@ def main( roboflow_api_key: str | None = None, confidence_threshold: float = 0.3, iou_threshold: float = 0.7, -): +) -> None: """ Vehicle Speed Estimation using Inference and Supervision. diff --git a/examples/speed_estimation/ultralytics_example.py b/examples/speed_estimation/ultralytics_example.py index 423e53acea..3678a55462 100644 --- a/examples/speed_estimation/ultralytics_example.py +++ b/examples/speed_estimation/ultralytics_example.py @@ -41,7 +41,7 @@ def main( target_video_path: str, confidence_threshold: float = 0.3, iou_threshold: float = 0.7, -): +) -> None: """ Vehicle Speed Estimation using Ultralytics and Supervision. diff --git a/examples/speed_estimation/yolo_nas_example.py b/examples/speed_estimation/yolo_nas_example.py index 9fe4148baf..0100e38cb6 100644 --- a/examples/speed_estimation/yolo_nas_example.py +++ b/examples/speed_estimation/yolo_nas_example.py @@ -42,7 +42,7 @@ def main( target_video_path: str, confidence_threshold: float = 0.3, iou_threshold: float = 0.7, -): +) -> None: """ Vehicle Speed Estimation using YOLO-NAS and Supervision. diff --git a/examples/time_in_zone/inference_stream_example.py b/examples/time_in_zone/inference_stream_example.py index 3f659d1c06..e88a55fc04 100644 --- a/examples/time_in_zone/inference_stream_example.py +++ b/examples/time_in_zone/inference_stream_example.py @@ -15,7 +15,7 @@ class CustomSink: - def __init__(self, zone_configuration_path: str, classes: list[int]): + def __init__(self, zone_configuration_path: str, classes: list[int]) -> None: self.classes = classes self.tracker = sv.ByteTrack(minimum_matching_threshold=0.5) self.fps_monitor = sv.FPSMonitor() diff --git a/examples/time_in_zone/rfdetr_file_example.py b/examples/time_in_zone/rfdetr_file_example.py index bb6625aa99..2c9e4aa6e0 100644 --- a/examples/time_in_zone/rfdetr_file_example.py +++ b/examples/time_in_zone/rfdetr_file_example.py @@ -25,7 +25,7 @@ class ModelSize(Enum): LARGE = "large" @classmethod - def list(cls): + def list(cls) -> list[str]: return list(map(lambda c: c.value, cls)) @classmethod @@ -44,7 +44,9 @@ def from_value(cls, value: ModelSize | str) -> ModelSize: ) -def load_model(checkpoint: ModelSize | str, device: str, resolution: int): +def load_model( + checkpoint: ModelSize | str, device: str, resolution: int +) -> RFDETRBase | RFDETRLarge | RFDETRMedium | RFDETRNano | RFDETRSmall: checkpoint = ModelSize.from_value(checkpoint) if checkpoint == ModelSize.NANO: diff --git a/examples/time_in_zone/rfdetr_naive_stream_example.py b/examples/time_in_zone/rfdetr_naive_stream_example.py index 5f78bcd0fa..b517277474 100644 --- a/examples/time_in_zone/rfdetr_naive_stream_example.py +++ b/examples/time_in_zone/rfdetr_naive_stream_example.py @@ -25,7 +25,7 @@ class ModelSize(Enum): LARGE = "large" @classmethod - def list(cls): + def list(cls) -> list[str]: return list(map(lambda c: c.value, cls)) @classmethod @@ -44,7 +44,9 @@ def from_value(cls, value: ModelSize | str) -> ModelSize: ) -def load_model(checkpoint: ModelSize | str, device: str, resolution: int): +def load_model( + checkpoint: ModelSize | str, device: str, resolution: int +) -> RFDETRBase | RFDETRLarge | RFDETRMedium | RFDETRNano | RFDETRSmall: checkpoint = ModelSize.from_value(checkpoint) if checkpoint == ModelSize.NANO: diff --git a/examples/time_in_zone/rfdetr_stream_example.py b/examples/time_in_zone/rfdetr_stream_example.py index 7eff2cbc98..678faaf90d 100644 --- a/examples/time_in_zone/rfdetr_stream_example.py +++ b/examples/time_in_zone/rfdetr_stream_example.py @@ -21,7 +21,7 @@ class ModelSize(Enum): LARGE = "large" @classmethod - def list(cls): + def list(cls) -> list[str]: return [c.value for c in cls] @classmethod @@ -41,7 +41,9 @@ def from_value(cls, value: ModelSize | str) -> ModelSize: ) -def load_model(checkpoint: ModelSize | str, device: str, resolution: int): +def load_model( + checkpoint: ModelSize | str, device: str, resolution: int +) -> RFDETRBase | RFDETRLarge | RFDETRMedium | RFDETRNano | RFDETRSmall: checkpoint = ModelSize.from_value(checkpoint) if checkpoint == ModelSize.NANO: return RFDETRNano(device=device, resolution=resolution) @@ -77,7 +79,7 @@ def adjust_resolution(checkpoint: ModelSize | str, resolution: int) -> int: class CustomSink: - def __init__(self, zone_configuration_path: str, classes: list[int]): + def __init__(self, zone_configuration_path: str, classes: list[int]) -> None: self.classes = classes self.tracker = sv.ByteTrack(minimum_matching_threshold=0.8) self.fps_monitor = sv.FPSMonitor() diff --git a/examples/time_in_zone/scripts/draw_zones.py b/examples/time_in_zone/scripts/draw_zones.py index de7a7bab6d..e52d0602c3 100644 --- a/examples/time_in_zone/scripts/draw_zones.py +++ b/examples/time_in_zone/scripts/draw_zones.py @@ -119,7 +119,9 @@ def redraw_polygons(image: np.ndarray) -> None: ) -def save_polygons_to_json(polygons, target_path): +def save_polygons_to_json( + polygons: list[list[tuple[int, int]]], target_path: str | os.PathLike[str] +) -> None: data_to_save = polygons if polygons[-1] else polygons[:-1] with open(target_path, "w") as f: json.dump(data_to_save, f) diff --git a/examples/time_in_zone/ultralytics_stream_example.py b/examples/time_in_zone/ultralytics_stream_example.py index b18462b339..75935680cb 100644 --- a/examples/time_in_zone/ultralytics_stream_example.py +++ b/examples/time_in_zone/ultralytics_stream_example.py @@ -18,7 +18,7 @@ class CustomSink: - def __init__(self, zone_configuration_path: str, classes: list[int]): + def __init__(self, zone_configuration_path: str, classes: list[int]) -> None: self.classes = classes self.tracker = sv.ByteTrack(minimum_matching_threshold=0.8) self.fps_monitor = sv.FPSMonitor() diff --git a/examples/traffic_analysis/inference_example.py b/examples/traffic_analysis/inference_example.py index 85c1ff4a8c..c6bd739e0a 100644 --- a/examples/traffic_analysis/inference_example.py +++ b/examples/traffic_analysis/inference_example.py @@ -103,7 +103,7 @@ def __init__( ) self.detections_manager = DetectionsManager() - def process_video(self): + def process_video(self) -> None: frame_generator = sv.get_video_frames_generator( source_path=self.source_video_path ) diff --git a/examples/traffic_analysis/ultralytics_example.py b/examples/traffic_analysis/ultralytics_example.py index 9db82a65ee..76d916e69d 100644 --- a/examples/traffic_analysis/ultralytics_example.py +++ b/examples/traffic_analysis/ultralytics_example.py @@ -100,7 +100,7 @@ def __init__( ) self.detections_manager = DetectionsManager() - def process_video(self): + def process_video(self) -> None: frame_generator = sv.get_video_frames_generator( source_path=self.source_video_path ) diff --git a/pyproject.toml b/pyproject.toml index 563f9c6d0f..2d37cf4fcc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ maintainers = [ authors = [ { name = "Roboflow et al.", email = "develop@roboflow.com" }, ] -requires-python = ">=3.9" +requires-python = ">=3.10" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", @@ -33,7 +33,6 @@ classifiers = [ "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -80,12 +79,12 @@ dev = [ ] docs = [ "mike>=2", - "mkdocs-git-committers-plugin-2>=2.4.1; python_version>='3.9' and python_version<'4'", + "mkdocs-git-committers-plugin-2>=2.4.1; python_version>='3.10' and python_version<'4'", "mkdocs-git-revision-date-localized-plugin>=1.2.4", "mkdocs-jupyter>=0.24.3", "mkdocs-material[imaging]>=9.7", "mkdocstrings>=0.25.2,<0.31", - "mkdocstrings-python>=1.10.9,<2", # todo: breaking changes in 2.x + "mkdocstrings-python>=1.10.9,<2", # todo: breaking changes in 2.x ] build = [ "build>=0.10,<1.5", @@ -101,7 +100,7 @@ packages.find.include = [ "supervision*" ] # exclude = [ "docs*", "tests*", "examples*" ] [tool.ruff] -target-version = "py39" +target-version = "py310" line-length = 88 indent-width = 4 # Exclude a variety of commonly ignored directories. @@ -184,7 +183,7 @@ quiet-level = 3 ignore-words-list = "STrack,sTrack,strack" [tool.mypy] -python_version = "3.9" +python_version = "3.10" ignore_missing_imports = false explicit_package_bases = true strict = true @@ -200,6 +199,7 @@ overrides = [ "tests.*", "examples.*", ], ignore_errors = true }, + { module = "deprecate", ignore_missing_imports = true }, ] [tool.pytest] diff --git a/src/supervision/annotators/core.py b/src/supervision/annotators/core.py index d754ac79ca..c2fd8fc9df 100644 --- a/src/supervision/annotators/core.py +++ b/src/supervision/annotators/core.py @@ -2,7 +2,7 @@ from functools import lru_cache from math import sqrt -from typing import Any, cast, overload +from typing import cast, overload import cv2 import numpy as np @@ -110,7 +110,7 @@ def __init__( border_radius: int = 0, smart_position: bool = False, max_line_length: int | None = None, - ): + ) -> None: """ Initializes the _BaseLabelAnnotator. @@ -148,7 +148,7 @@ def _adjust_labels_in_frame( resolution_wh: tuple[int, int], labels: list[str], label_properties: npt.NDArray[np.float32], - ) -> npt.NDArray[np.uint8]: + ) -> npt.NDArray[np.float32]: """ Adjusts the position of labels to ensure they stay within the frame boundaries. @@ -183,7 +183,10 @@ def _adjust_labels_in_frame( adjusted_properties[:, :4], resolution_wh ) - return adjusted_properties + return cast( + npt.NDArray[np.float32], + np.asarray(adjusted_properties, dtype=np.float32), + ) class BoxAnnotator(BaseAnnotator): @@ -196,7 +199,7 @@ def __init__( color: Color | ColorPalette | str = ColorPalette.DEFAULT, thickness: int = 2, color_lookup: ColorLookup = ColorLookup.CLASS, - ): + ) -> None: """ Args: color: The color or color palette to use for @@ -283,7 +286,7 @@ def __init__( color: Color | ColorPalette | str = ColorPalette.DEFAULT, thickness: int = 2, color_lookup: ColorLookup = ColorLookup.CLASS, - ): + ) -> None: """ Args: color: The color or color palette to use for @@ -374,7 +377,7 @@ def __init__( color: Color | ColorPalette | str = ColorPalette.DEFAULT, opacity: float = 0.5, color_lookup: ColorLookup = ColorLookup.CLASS, - ): + ) -> None: """ Args: color: The color or color palette to use for @@ -485,7 +488,7 @@ def __init__( color: Color | ColorPalette | str = ColorPalette.DEFAULT, thickness: int = 2, color_lookup: ColorLookup = ColorLookup.CLASS, - ): + ) -> None: """ Args: color: The color or color palette to use for @@ -558,7 +561,7 @@ def annotate( for polygon in mask_to_polygons(mask=mask): scene = draw_polygon( scene=scene, - polygon=polygon, + polygon=polygon.astype(np.int32), color=color, thickness=self.thickness, ) @@ -576,7 +579,7 @@ def __init__( color: Color | ColorPalette | str = ColorPalette.DEFAULT, opacity: float = 0.5, color_lookup: ColorLookup = ColorLookup.CLASS, - ): + ) -> None: """ Args: color: The color or color palette to use for @@ -673,7 +676,7 @@ def __init__( opacity: float = 0.8, kernel_size: int = 40, color_lookup: ColorLookup = ColorLookup.CLASS, - ): + ) -> None: """ Args: color: The color or color palette to use for @@ -754,7 +757,10 @@ def annotate( color_bgr = color.as_bgr() colored_mask[mask] = color_bgr - colored_mask = cv2.blur(colored_mask, (self.kernel_size, self.kernel_size)) + colored_mask = cast( + npt.NDArray[np.uint8], + cv2.blur(colored_mask, (self.kernel_size, self.kernel_size)), + ) colored_mask[fmask] = [0, 0, 0] gray = cv2.cvtColor(colored_mask, cv2.COLOR_BGR2GRAY) alpha = self.opacity * gray / gray.max() @@ -776,7 +782,7 @@ def __init__( start_angle: int = -45, end_angle: int = 235, color_lookup: ColorLookup = ColorLookup.CLASS, - ): + ) -> None: """ Args: color: The color or color palette to use for @@ -874,7 +880,7 @@ def __init__( thickness: int = 4, corner_length: int = 15, color_lookup: ColorLookup = ColorLookup.CLASS, - ): + ) -> None: """ Args: color: The color or color palette to use for @@ -968,7 +974,7 @@ def __init__( color: Color | ColorPalette | str = ColorPalette.DEFAULT, thickness: int = 2, color_lookup: ColorLookup = ColorLookup.CLASS, - ): + ) -> None: """ Args: color: The color or color palette to use for @@ -1064,7 +1070,7 @@ def __init__( color_lookup: ColorLookup = ColorLookup.CLASS, outline_thickness: int = 0, outline_color: Color | ColorPalette | str = Color.BLACK, - ): + ) -> None: """ Args: color: The color or color palette to use for @@ -1179,7 +1185,7 @@ def __init__( border_radius: int = 0, smart_position: bool = False, max_line_length: int | None = None, - ): + ) -> None: """ Args: color: The color or color palette to use for @@ -1278,7 +1284,7 @@ def annotate( if self.smart_position: xyxy = label_properties[:, :4] - xyxy = spread_out_boxes(xyxy) + xyxy = cast(npt.NDArray[np.float32], spread_out_boxes(xyxy)) label_properties[:, :4] = xyxy label_properties = self._adjust_labels_in_frame( @@ -1301,7 +1307,7 @@ def _get_label_properties( self, detections: Detections, labels: list[str], - ) -> Any: + ) -> npt.NDArray[np.float32]: label_properties = [] anchors_coordinates: npt.NDArray[np.int32] = detections.get_anchors_coordinates( anchor=self.text_anchor @@ -1495,7 +1501,7 @@ def __init__( border_radius: int = 0, smart_position: bool = False, max_line_length: int | None = None, - ): + ) -> None: """ Args: color: The color or color palette to use for @@ -1594,7 +1600,7 @@ def annotate( if self.smart_position: xyxy = label_properties[:, :4] - xyxy = spread_out_boxes(xyxy) + xyxy = spread_out_boxes(xyxy) # type: ignore[assignment] label_properties[:, :4] = xyxy label_properties = self._adjust_labels_in_frame( @@ -1615,7 +1621,7 @@ def annotate( def _get_label_properties( self, draw: ImageDraw.ImageDraw, detections: Detections, labels: list[str] - ) -> Any: + ) -> npt.NDArray[np.float32]: label_properties = [] anchor_coordinates: npt.NDArray[np.int32] = detections.get_anchors_coordinates( @@ -1762,7 +1768,7 @@ def __init__( icon_resolution_wh: tuple[int, int] = (64, 64), icon_position: Position = Position.TOP_CENTER, offset_xy: tuple[int, int] = (0, 0), - ): + ) -> None: """ Args: icon_resolution_wh: The size of drawn icons. @@ -1854,11 +1860,10 @@ def _load_icon(self, icon_path: str) -> npt.NDArray[np.uint8]: raise FileNotFoundError( f"Error: Couldn't load the icon image from {icon_path}" ) - icon = cast( + return cast( npt.NDArray[np.uint8], letterbox_image(image=icon, resolution_wh=self.icon_resolution_wh), ) - return icon class BlurAnnotator(BaseAnnotator): @@ -1866,7 +1871,7 @@ class BlurAnnotator(BaseAnnotator): A class for blurring regions in an image using provided detections. """ - def __init__(self, kernel_size: int | None = None): + def __init__(self, kernel_size: int | None = None) -> None: """ Args: kernel_size: The size of the average pooling kernel used for blurring. @@ -1920,7 +1925,8 @@ def annotate( return scene image_height, image_width = scene.shape[:2] clipped_xyxy: npt.NDArray[np.int32] = clip_boxes( - xyxy=detections.xyxy, resolution_wh=(image_width, image_height) + xyxy=cast(npt.NDArray[np.float32], detections.xyxy), + resolution_wh=(image_width, image_height), ).astype(int) for x1, y1, x2, y2 in clipped_xyxy: @@ -1932,7 +1938,7 @@ def annotate( if self.kernel_size is not None else calculate_dynamic_kernel_size(x1, y1, x2, y2) ) - roi = cv2.blur(roi, (kernel_size, kernel_size)) + roi = cast(npt.NDArray[np.uint8], cv2.blur(roi, (kernel_size, kernel_size))) scene[y1:y2, x1:x2] = roi return scene @@ -1957,7 +1963,7 @@ def __init__( thickness: int = 2, smooth: bool = False, color_lookup: ColorLookup = ColorLookup.CLASS, - ): + ) -> None: """ Args: color: The color to draw the trace, can be @@ -2038,7 +2044,7 @@ def annotate( ) filtered_detections: Detections = detections[ detections.tracker_id != PENDING_TRACK_ID - ] # type: ignore + ] self.trace.put(filtered_detections) for detection_idx in range(len(filtered_detections)): @@ -2065,8 +2071,20 @@ def annotate( try: x, y = unique_xy[:, 0], unique_xy[:, 1] tck, _u = splprep([x, y], s=20) - xy_new = splev(np.linspace(0, 1, 100), tck) - spline_points = np.stack(xy_new, axis=1).astype(np.int32) + x_new, y_new = splev( + np.linspace(0, 1, 100), + cast( + tuple[ + npt.NDArray[np.float64], + npt.NDArray[np.float64], + int, + ], + tck, + ), + ) + spline_points = np.stack((x_new, y_new), axis=1).astype( + np.int32 + ) except ValueError: spline_points = unique_xy.astype(np.int32) else: @@ -2098,7 +2116,7 @@ def __init__( kernel_size: int = 25, top_hue: int = 0, low_hue: int = 125, - ): + ) -> None: """ Args: position: The position of the heatmap. Defaults to @@ -2162,7 +2180,7 @@ def annotate(self, scene: ImageType, detections: Detections) -> ImageType: if self.heat_mask is None: self.heat_mask = np.zeros(scene.shape[:2], dtype=np.float32) - mask = np.zeros(scene.shape[:2]) + mask: npt.NDArray[np.float32] = np.zeros(scene.shape[:2], dtype=np.float32) for xy in detections.get_anchors_coordinates(self.position): x, y = int(xy[0]), int(xy[1]) cv2.circle( @@ -2173,18 +2191,25 @@ def annotate(self, scene: ImageType, detections: Detections) -> ImageType: thickness=-1, # fill ) self.heat_mask = mask + self.heat_mask - temp = self.heat_mask.copy() - temp = self.low_hue - temp / temp.max() * (self.low_hue - self.top_hue) - temp = temp.astype(np.uint8) + heat_values = self.heat_mask.copy() + max_heat_value = heat_values.max() + if max_heat_value == 0: + return scene + heat_values = self.low_hue - heat_values / max_heat_value * ( + self.low_hue - self.top_hue + ) + heat_values = heat_values.astype(np.uint8) if self.kernel_size is not None: - temp = cv2.blur(temp, (self.kernel_size, self.kernel_size)) + heat_values = cv2.blur(heat_values, (self.kernel_size, self.kernel_size)) hsv = np.full(scene.shape, 255, dtype=np.uint8) - hsv[..., 0] = temp - temp = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR) - mask = cv2.cvtColor(self.heat_mask.astype(np.uint8), cv2.COLOR_GRAY2BGR) > 0 - scene[mask] = cv2.addWeighted(temp, self.opacity, scene, 1 - self.opacity, 0)[ - mask - ] + hsv[..., 0] = heat_values + heat_bgr = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR) + mask_bool = ( + cv2.cvtColor(self.heat_mask.astype(np.uint8), cv2.COLOR_GRAY2BGR) > 0 + ) + scene[mask_bool] = cv2.addWeighted( + heat_bgr, self.opacity, scene, 1 - self.opacity, 0 + )[mask_bool] return scene @@ -2193,7 +2218,7 @@ class PixelateAnnotator(BaseAnnotator): A class for pixelating regions in an image using provided detections. """ - def __init__(self, pixel_size: int | None = None): + def __init__(self, pixel_size: int | None = None) -> None: """ Args: pixel_size: The size of the pixelation. If not set, a dynamic size is @@ -2247,7 +2272,8 @@ def annotate( return scene image_height, image_width = scene.shape[:2] clipped_xyxy: npt.NDArray[np.int32] = clip_boxes( - xyxy=detections.xyxy, resolution_wh=(image_width, image_height) + xyxy=cast(npt.NDArray[np.float32], detections.xyxy), + resolution_wh=(image_width, image_height), ).astype(int) for x1, y1, x2, y2 in clipped_xyxy: @@ -2297,7 +2323,7 @@ def __init__( color_lookup: ColorLookup = ColorLookup.CLASS, outline_thickness: int = 0, outline_color: Color | ColorPalette | str = Color.BLACK, - ): + ) -> None: """ Args: color: The color or color palette to use for @@ -2416,7 +2442,7 @@ def __init__( thickness: int = 2, color_lookup: ColorLookup = ColorLookup.CLASS, roundness: float = 0.6, - ): + ) -> None: """ Args: color: The color or color palette to use for @@ -2554,7 +2580,7 @@ def __init__( position: Position = Position.TOP_CENTER, color_lookup: ColorLookup = ColorLookup.CLASS, border_thickness: int | None = None, - ): + ) -> None: """ Args: height: The height in pixels of the percentage bar. @@ -2747,7 +2773,7 @@ def __init__( border_color: Color | ColorPalette | str = ColorPalette.DEFAULT, border_thickness: int = 2, border_color_lookup: ColorLookup = ColorLookup.CLASS, - ): + ) -> None: """ Args: position: The anchor position for placing the cropped and scaled @@ -2910,7 +2936,7 @@ def __init__( color: Color = Color.BLACK, opacity: float = 0.5, force_box: bool = False, - ): + ) -> None: """ Args: color: The color to use for annotating detections. @@ -2999,7 +3025,7 @@ def __init__( label_2: str = "", label_overlap: str = "", label_scale: float = 1.0, - ): + ) -> None: """ Args: color_1: Color of areas only present in the first set of @@ -3136,7 +3162,7 @@ def _mask_from_xyxy( return mask resolution_wh = scene.shape[1], scene.shape[0] - polygons = xyxy_to_polygons(detections.xyxy) + polygons = xyxy_to_polygons(cast(npt.NDArray[np.float32], detections.xyxy)) for polygon in polygons: polygon_mask = polygon_to_mask(polygon, resolution_wh=resolution_wh) @@ -3153,7 +3179,9 @@ def _mask_from_obb( resolution_wh = scene.shape[1], scene.shape[0] - for polygon in detections.data[ORIENTED_BOX_COORDINATES]: + for polygon in np.asarray( + detections.data[ORIENTED_BOX_COORDINATES], dtype=np.float32 + ): polygon_mask = polygon_to_mask(polygon, resolution_wh=resolution_wh) mask |= polygon_mask.astype(np.bool_) return mask diff --git a/src/supervision/annotators/utils.py b/src/supervision/annotators/utils.py index cfeaf98194..f55e91be0a 100644 --- a/src/supervision/annotators/utils.py +++ b/src/supervision/annotators/utils.py @@ -3,7 +3,7 @@ import re import textwrap from enum import Enum -from typing import Any +from typing import cast import numpy as np import numpy.typing as npt @@ -155,7 +155,7 @@ def resolve_color( return get_color_by_index(color=color, idx=idx) -def wrap_text(text: Any, max_line_length: int | None = None) -> list[str]: +def wrap_text(text: object, max_line_length: int | None = None) -> list[str]: """ Wrap `text` to the specified maximum line length, respecting existing newlines. Falls back to str() if `text` is not already a string. @@ -254,9 +254,9 @@ def get_labels_text( def snap_boxes( - xyxy: np.ndarray[Any, np.dtype[np.float32]], + xyxy: npt.NDArray[np.float32], resolution_wh: tuple[int, int], -) -> np.ndarray[Any, np.dtype[np.float32]]: +) -> npt.NDArray[np.float32]: """ Shifts `label` bounding boxes into the frame so that they are fully contained within the given resolution, prioritizing the top/left edge. @@ -297,7 +297,7 @@ def snap_boxes( ``` """ - result = np.copy(xyxy) + result: npt.NDArray[np.float32] = np.array(xyxy, dtype=np.float32, copy=True) width, height = resolution_wh # X-axis (prioritize left edge) @@ -316,7 +316,7 @@ def snap_boxes( bottom_shift = height - result[bottom_overflow, 3] result[bottom_overflow, 1:4:2] += bottom_shift[:, np.newaxis] - return result.astype(np.float32) # type: ignore + return cast(npt.NDArray[np.float32], result.astype(np.float32, copy=False)) class Trace: @@ -330,9 +330,9 @@ def __init__( self.max_size = max_size self.anchor = anchor - self.frame_id = np.array([], dtype=int) - self.xy = np.empty((0, 2), dtype=np.float32) - self.tracker_id = np.array([], dtype=int) + self.frame_id: npt.NDArray[np.int_] = np.array([], dtype=int) + self.xy: npt.NDArray[np.float32] = np.empty((0, 2), dtype=np.float32) + self.tracker_id: npt.NDArray[np.int_] = np.array([], dtype=int) def put(self, detections: Detections) -> None: frame_id: npt.NDArray[np.int_] = np.full( @@ -364,11 +364,11 @@ def put(self, detections: Detections) -> None: self.current_frame_id += 1 - def get(self, tracker_id: int) -> np.ndarray[Any, np.dtype[np.float32]]: - filtered: np.ndarray[Any, np.dtype[np.float32]] = ( - self.xy[self.tracker_id == tracker_id].copy().astype(np.float32, copy=False) + def get(self, tracker_id: int) -> npt.NDArray[np.float32]: + xy: npt.NDArray[np.float32] = np.asarray( + self.xy[self.tracker_id == tracker_id], dtype=np.float32 ) - return filtered + return xy def hex_to_rgba(hex_color: str) -> tuple[int, int, int, int]: diff --git a/src/supervision/dataset/core.py b/src/supervision/dataset/core.py index fdadce68c6..3bd0833649 100644 --- a/src/supervision/dataset/core.py +++ b/src/supervision/dataset/core.py @@ -6,6 +6,7 @@ from dataclasses import dataclass from itertools import chain from pathlib import Path +from typing import cast import cv2 import numpy as np @@ -105,7 +106,7 @@ def _get_image(self, image_path: str) -> npt.NDArray[np.uint8]: image = cv2.imread(image_path) if image is None: raise ValueError(f"Could not read image from path: {image_path}") - return image + return cast(npt.NDArray[np.uint8], image) def __len__(self) -> int: return len(self._images_in_memory) or len(self.image_paths) @@ -749,7 +750,7 @@ def _get_image(self, image_path: str) -> npt.NDArray[np.uint8]: image = cv2.imread(image_path) if image is None: raise ValueError(f"Could not read image from path: {image_path}") - return image + return cast(npt.NDArray[np.uint8], image) def __len__(self) -> int: return len(self._images_in_memory) or len(self.image_paths) diff --git a/src/supervision/dataset/formats/coco.py b/src/supervision/dataset/formats/coco.py index d13440bd22..4581760a09 100644 --- a/src/supervision/dataset/formats/coco.py +++ b/src/supervision/dataset/formats/coco.py @@ -1,7 +1,7 @@ import warnings from datetime import datetime from pathlib import Path -from typing import TYPE_CHECKING, Any, Union, cast +from typing import TYPE_CHECKING, TypeAlias, TypedDict, cast import numpy as np import numpy.typing as npt @@ -11,6 +11,7 @@ map_detections_class_id, ) from supervision.detection.core import Detections +from supervision.detection.utils._typing import DetectionDataType from supervision.detection.utils.converters import ( mask_to_rle, polygon_to_mask, @@ -22,10 +23,44 @@ if TYPE_CHECKING: from supervision.dataset.core import DetectionDataset -CocoDict = dict[str, Any] +class _TypeCocoCategory(TypedDict): + id: int + name: str + supercategory: str -def coco_categories_to_classes(coco_categories: list[CocoDict]) -> list[str]: + +class _TypeCocoImage(TypedDict, total=False): + id: int + file_name: str + width: int + height: int + license: int + date_captured: str + + +_TypeCocoRLE: TypeAlias = dict[str, list[int] | str | bytes] + + +class _TypeCocoAnnotation(TypedDict, total=False): + id: int + image_id: int + category_id: int + bbox: list[float] + area: float + segmentation: list[list[float]] | _TypeCocoRLE + iscrowd: int + + +class _TypeCocoFile(TypedDict): + categories: list[_TypeCocoCategory] + images: list[_TypeCocoImage] + annotations: list[_TypeCocoAnnotation] + + +def coco_categories_to_classes( + coco_categories: list[_TypeCocoCategory], +) -> list[str]: return [ category["name"] for category in sorted(coco_categories, key=lambda category: category["id"]) @@ -33,7 +68,7 @@ def coco_categories_to_classes(coco_categories: list[CocoDict]) -> list[str]: def build_coco_class_index_mapping( - coco_categories: list[CocoDict], target_classes: list[str] + coco_categories: list[_TypeCocoCategory], target_classes: list[str] ) -> dict[int, int]: source_class_to_index = { category["name"]: category["id"] for category in coco_categories @@ -44,7 +79,7 @@ def build_coco_class_index_mapping( } -def classes_to_coco_categories(classes: list[str]) -> list[CocoDict]: +def classes_to_coco_categories(classes: list[str]) -> list[_TypeCocoCategory]: return [ { "id": class_id, @@ -56,19 +91,16 @@ def classes_to_coco_categories(classes: list[str]) -> list[CocoDict]: def group_coco_annotations_by_image_id( - coco_annotations: list[CocoDict], -) -> dict[int, list[CocoDict]]: - annotations: dict[int, list[CocoDict]] = {} + coco_annotations: list[_TypeCocoAnnotation], +) -> dict[int, list[_TypeCocoAnnotation]]: + annotations: dict[int, list[_TypeCocoAnnotation]] = {} for annotation in coco_annotations: - image_id = annotation["image_id"] - if image_id not in annotations: - annotations[image_id] = [] - annotations[image_id].append(annotation) + annotations.setdefault(annotation["image_id"], []).append(annotation) return annotations def coco_annotations_to_masks( - image_annotations: list[CocoDict], resolution_wh: tuple[int, int] + image_annotations: list[_TypeCocoAnnotation], resolution_wh: tuple[int, int] ) -> npt.NDArray[np.bool_]: height, width = resolution_wh[1], resolution_wh[0] empty_mask: npt.NDArray[np.bool_] = np.zeros((height, width), dtype=bool) @@ -82,16 +114,33 @@ def coco_annotations_to_masks( masks.append(empty_mask.copy()) continue - if image_annotation.get("iscrowd", 0): + if isinstance(segmentation, dict): + if "counts" not in segmentation: + warnings.warn( + "Skipping annotation " + f"{image_annotation.get('id', '?')}: segmentation is a dict but " + "missing 'counts' key (expected RLE format)", + stacklevel=2, + ) + masks.append(empty_mask.copy()) + continue masks.append( - rle_to_mask(rle=segmentation["counts"], resolution_wh=resolution_wh) + rle_to_mask( + rle=segmentation["counts"], + resolution_wh=resolution_wh, + ) ) continue if not isinstance(segmentation, list): masks.append(empty_mask.copy()) continue - polygons = segmentation if isinstance(segmentation[0], list) else [segmentation] + segmentation_list = segmentation + polygons = ( + segmentation_list + if isinstance(segmentation_list[0], list) + else [segmentation_list] + ) object_mask = empty_mask.copy() for polygon in polygons: @@ -117,7 +166,7 @@ def coco_annotations_to_masks( def coco_annotations_to_detections( - image_annotations: list[CocoDict], + image_annotations: list[_TypeCocoAnnotation], resolution_wh: tuple[int, int], with_masks: bool, use_iscrowd: bool = True, @@ -125,22 +174,23 @@ def coco_annotations_to_detections( if not image_annotations: return Detections.empty() - class_ids = [ + class_ids: list[int] = [ image_annotation["category_id"] for image_annotation in image_annotations ] - xyxy = [image_annotation["bbox"] for image_annotation in image_annotations] - xyxy = np.asarray(xyxy, dtype=np.float32) + xyxy_list: list[list[float]] = [ + image_annotation["bbox"] for image_annotation in image_annotations + ] + xyxy = np.asarray(xyxy_list, dtype=np.float32) xyxy[:, 2:4] += xyxy[:, 0:2] - data: dict[str, npt.NDArray[np.generic]] = {} + data: DetectionDataType = {} if use_iscrowd: iscrowd = [ image_annotation["iscrowd"] for image_annotation in image_annotations ] area = [image_annotation["area"] for image_annotation in image_annotations] - data = dict( - iscrowd=np.asarray(iscrowd, dtype=int), area=np.asarray(area, dtype=float) - ) + data["iscrowd"] = np.asarray(iscrowd, dtype=np.int64) + data["area"] = np.asarray(area, dtype=np.float64) if with_masks: mask = coco_annotations_to_masks( @@ -161,29 +211,31 @@ def detections_to_coco_annotations( min_image_area_percentage: float = 0.0, max_image_area_percentage: float = 1.0, approximation_percentage: float = 0.75, -) -> tuple[list[CocoDict], int]: - coco_annotations: list[CocoDict] = [] +) -> tuple[list[_TypeCocoAnnotation], int]: + coco_annotations: list[_TypeCocoAnnotation] = [] for xyxy, mask, _, class_id, _, data in detections: if class_id is None: raise ValueError("Detections must include class_id for COCO export.") box_width, box_height = xyxy[2] - xyxy[0], xyxy[3] - xyxy[1] - segmentation: Union[list[list[float]], dict[str, list[int]]] = [] + segmentation: list[list[float]] | _TypeCocoRLE = [] if mask is not None: + mask_bool = np.asarray(mask, dtype=bool) if "iscrowd" in data: iscrowd = int(np.asarray(data["iscrowd"]).item()) else: iscrowd = int( - contains_holes(mask=mask) or contains_multiple_segments(mask=mask) + contains_holes(mask=mask_bool) + or contains_multiple_segments(mask=mask_bool) ) if iscrowd: segmentation = { - "counts": cast(list[int], mask_to_rle(mask=mask, compressed=False)), - "size": list(mask.shape[:2]), + "counts": mask_to_rle(mask=mask_bool), + "size": list(mask_bool.shape[:2]), } else: polygons = approximate_mask_with_polygons( - mask=mask, + mask=mask_bool, min_image_area_percentage=min_image_area_percentage, max_image_area_percentage=max_image_area_percentage, approximation_percentage=approximation_percentage, @@ -203,7 +255,7 @@ def detections_to_coco_annotations( iscrowd = int(np.asarray(data.get("iscrowd", 0)).item()) area: float = float(np.asarray(data.get("area", box_width * box_height)).item()) - coco_annotation = { + coco_annotation: _TypeCocoAnnotation = { "id": annotation_id, "image_id": image_id, "category_id": int(class_id), @@ -248,10 +300,11 @@ def get_coco_class_index_mapping(annotations_path: str) -> dict[int, int]: A mapping from new class id (sequential ranging from 0 to 79) to original COCO class id (1 to 90 with skipped ids). """ - coco_data = read_json_file(annotations_path) + coco_data = cast(_TypeCocoFile, read_json_file(annotations_path)) classes = coco_categories_to_classes(coco_categories=coco_data["categories"]) class_mapping = build_coco_class_index_mapping( - coco_categories=coco_data["categories"], target_classes=classes + coco_categories=coco_data["categories"], + target_classes=classes, ) return {v: k for k, v in class_mapping.items()} @@ -291,11 +344,13 @@ def load_coco_annotations( attacks when loading user-supplied annotation files. Symlinked images pointing outside the resolved images directory are also rejected. """ - coco_data = read_json_file(file_path=annotations_path) - classes = coco_categories_to_classes(coco_categories=coco_data["categories"]) + coco_data = cast(_TypeCocoFile, read_json_file(file_path=annotations_path)) + coco_categories = coco_data["categories"] + classes = coco_categories_to_classes(coco_categories=coco_categories) class_index_mapping = build_coco_class_index_mapping( - coco_categories=coco_data["categories"], target_classes=classes + coco_categories=coco_categories, + target_classes=classes, ) coco_images = coco_data["images"] @@ -303,8 +358,8 @@ def load_coco_annotations( coco_annotations=coco_data["annotations"] ) - images = [] - annotations = {} + images: list[str] = [] + annotations: dict[str, Detections] = {} images_directory_resolved = Path(images_directory_path).resolve() for coco_image in coco_images: @@ -363,7 +418,7 @@ def load_coco_annotations( return classes, images, annotations -def _with_seg_mask(annotation: dict[str, Any]) -> bool: +def _with_seg_mask(annotation: _TypeCocoAnnotation) -> bool: return bool(annotation.get("segmentation")) @@ -443,15 +498,15 @@ def save_coco_annotations( } ] - coco_annotations = [] - coco_images = [] + coco_annotations: list[_TypeCocoAnnotation] = [] + coco_images: list[_TypeCocoImage] = [] coco_categories = classes_to_coco_categories(classes=dataset.classes) image_id, annotation_id = starting_image_id, starting_annotation_id for image_path, image, annotation in dataset: image_height, image_width, _ = image.shape image_name = f"{Path(image_path).stem}{Path(image_path).suffix}" - coco_image = { + coco_image: _TypeCocoImage = { "id": image_id, "license": 1, "file_name": image_name, diff --git a/src/supervision/dataset/formats/pascal_voc.py b/src/supervision/dataset/formats/pascal_voc.py index 91b6664cdc..2f763e5c28 100644 --- a/src/supervision/dataset/formats/pascal_voc.py +++ b/src/supervision/dataset/formats/pascal_voc.py @@ -2,6 +2,7 @@ import os from pathlib import Path +from typing import cast from xml.etree.ElementTree import Element, SubElement import cv2 @@ -27,7 +28,7 @@ def object_to_pascal_voc( object_name.text = name # https://github.com/roboflow/supervision/issues/144 - xyxy += 1 + xyxy = np.asarray(xyxy).copy() + 1 bndbox = SubElement(root, "bndbox") xmin = SubElement(bndbox, "xmin") @@ -41,7 +42,7 @@ def object_to_pascal_voc( if polygon is not None: # https://github.com/roboflow/supervision/issues/144 - polygon += 1 + polygon = np.asarray(polygon).copy() + 1 object_polygon = SubElement(root, "polygon") for index, point in enumerate(polygon, start=1): x_coordinate, y_coordinate = point @@ -57,7 +58,7 @@ def detections_to_pascal_voc( detections: Detections, classes: list[str], filename: str, - image_shape: tuple[int, int, int], + image_shape: tuple[int, ...], min_image_area_percentage: float = 0.0, max_image_area_percentage: float = 1.0, approximation_percentage: float = 0.75, @@ -82,7 +83,8 @@ class ids in the Detections object. Returns: An XML string in Pascal VOC format representing the detections. """ - height, width, depth = image_shape + height, width = image_shape[:2] + depth = image_shape[2] if len(image_shape) > 2 else 1 # Create root element annotation = Element("annotation") @@ -125,7 +127,7 @@ class ids in the Detections object. name = classes[class_id] if mask is not None: polygons = approximate_mask_with_polygons( - mask=mask, + mask=cast(npt.NDArray[np.bool_], mask), min_image_area_percentage=min_image_area_percentage, max_image_area_percentage=max_image_area_percentage, approximation_percentage=approximation_percentage, @@ -141,7 +143,9 @@ class ids in the Detections object. annotation.append(next_object) # Generate XML string - xml_string = str(parseString(tostring(annotation)).toprettyxml(indent=" ")) + xml_string = str( + parseString(tostring(annotation, encoding="unicode")).toprettyxml(indent=" ") + ) return xml_string @@ -186,6 +190,7 @@ def load_pascal_voc_annotations( tree = parse(annotation_path) root = tree.getroot() + assert root is not None image = cv2.imread(image_path) if image is None: diff --git a/src/supervision/dataset/formats/yolo.py b/src/supervision/dataset/formats/yolo.py index 9abaff4595..bb1b3cb0ce 100644 --- a/src/supervision/dataset/formats/yolo.py +++ b/src/supervision/dataset/formats/yolo.py @@ -2,8 +2,9 @@ import os import warnings +from collections.abc import Sequence from pathlib import Path -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, cast import numpy as np import numpy.typing as npt @@ -12,6 +13,7 @@ from supervision.config import ORIENTED_BOX_COORDINATES from supervision.dataset.utils import approximate_mask_with_polygons from supervision.detection.core import Detections +from supervision.detection.utils._typing import DetectionDataType from supervision.detection.utils.converters import polygon_to_mask, polygon_to_xyxy from supervision.utils.file import ( list_files_with_extensions, @@ -49,7 +51,7 @@ def _parse_polygon(values: list[str]) -> npt.NDArray[np.float32]: def _polygons_to_masks( - polygons: list[npt.NDArray[np.number]], resolution_wh: tuple[int, int] + polygons: Sequence[npt.NDArray[np.number]], resolution_wh: tuple[int, int] ) -> npt.NDArray[np.bool_]: return np.array( [ @@ -68,7 +70,7 @@ def _with_seg_mask(lines: list[str]) -> bool: def _extract_class_names(file_path: str) -> list[str]: - data: dict[str, Any] = read_yaml_file(file_path=file_path) + data = read_yaml_file(file_path=file_path) if not isinstance(data, dict): raise ValueError( f"Expected mapping in data.yaml at '{file_path}'," @@ -99,31 +101,36 @@ def yolo_annotations_to_detections( if len(lines) == 0: return Detections.empty() - class_id, relative_xyxy, relative_polygon, relative_xyxyxyxy = [], [], [], [] + class_id_list: list[int] = [] + relative_xyxy_list: list[npt.NDArray[np.float32]] = [] + relative_polygon_list: list[npt.NDArray[np.float32]] = [] + relative_xyxyxyxy_list: list[npt.NDArray[np.float32]] = [] w, h = resolution_wh for line in lines: values = line.split() - class_id.append(int(values[0])) + class_id_list.append(int(values[0])) if len(values) == 5: box = _parse_box(values=values[1:]) - relative_xyxy.append(box) + relative_xyxy_list.append(box) if with_masks: - relative_polygon.append(_box_to_polygon(box=box)) + relative_polygon_list.append(_box_to_polygon(box=box)) elif len(values) > 5: polygon = _parse_polygon(values=values[1:]) - relative_xyxy.append(polygon_to_xyxy(polygon=polygon)) + relative_xyxy_list.append( + np.asarray(polygon_to_xyxy(polygon=polygon), dtype=np.float32) + ) if is_obb: - relative_xyxyxyxy.append(np.array(values[1:])) + relative_xyxyxyxy_list.append(np.array(values[1:], dtype=np.float32)) if with_masks: - relative_polygon.append(polygon) + relative_polygon_list.append(polygon) - class_id = np.array(class_id, dtype=int) - relative_xyxy = np.array(relative_xyxy, dtype=np.float32) + class_id = np.array(class_id_list, dtype=np.int64) + relative_xyxy = np.array(relative_xyxy_list, dtype=np.float32) xyxy = relative_xyxy * np.array([w, h, w, h], dtype=np.float32) - data = {} + data: DetectionDataType = {} if is_obb: - relative_xyxyxyxy = np.array(relative_xyxyxyxy, dtype=np.float32) + relative_xyxyxyxy = np.array(relative_xyxyxyxy_list, dtype=np.float32) xyxyxyxy = relative_xyxyxyxy.reshape(-1, 4, 2) xyxyxyxy *= np.array([w, h], dtype=np.float32) data[ORIENTED_BOX_COORDINATES] = xyxyxyxy @@ -133,7 +140,7 @@ def yolo_annotations_to_detections( polygons = [ polygon * np.array(resolution_wh, dtype=np.float32) - for polygon in relative_polygon + for polygon in relative_polygon_list ] mask = _polygons_to_masks(polygons=polygons, resolution_wh=resolution_wh) return Detections(class_id=class_id, xyxy=xyxy, data=data, mask=mask) @@ -229,10 +236,10 @@ def load_yolo_annotations( def object_to_yolo( xyxy: npt.NDArray[np.number], class_id: int, - image_shape: tuple[int, int, int], + image_shape: tuple[int, ...], polygon: npt.NDArray[np.number] | None = None, ) -> str: - h, w, _ = image_shape + h, w = image_shape[:2] if polygon is None: xyxy_relative = xyxy / np.array([w, h, w, h], dtype=np.float32) x_min, y_min, x_max, y_max = xyxy_relative @@ -250,7 +257,7 @@ def object_to_yolo( def detections_to_yolo_annotations( detections: Detections, - image_shape: tuple[int, int, int], + image_shape: tuple[int, ...], min_image_area_percentage: float = 0.0, max_image_area_percentage: float = 1.0, approximation_percentage: float = 0.75, @@ -268,7 +275,7 @@ def detections_to_yolo_annotations( if mask is not None: polygons = approximate_mask_with_polygons( - mask=mask, + mask=cast(npt.NDArray[np.bool_], mask), min_image_area_percentage=min_image_area_percentage, max_image_area_percentage=max_image_area_percentage, approximation_percentage=approximation_percentage, @@ -284,7 +291,9 @@ def detections_to_yolo_annotations( annotation.append(next_object) else: next_object = object_to_yolo( - xyxy=xyxy, class_id=class_id_int, image_shape=image_shape + xyxy=xyxy, + class_id=class_id_int, + image_shape=image_shape, ) annotation.append(next_object) return annotation diff --git a/src/supervision/dataset/utils.py b/src/supervision/dataset/utils.py index ab8fa98b6e..e26222dc62 100644 --- a/src/supervision/dataset/utils.py +++ b/src/supervision/dataset/utils.py @@ -5,7 +5,7 @@ import random import shutil from pathlib import Path -from typing import TYPE_CHECKING, Any, TypeVar +from typing import TYPE_CHECKING, TypeVar, cast import cv2 import numpy as np @@ -31,16 +31,16 @@ def mask_to_rle( mask: npt.NDArray[np.bool_], compressed: bool = False ) -> list[int] | str: """Deprecated. Use `supervision.detection.utils.converters.mask_to_rle`.""" - return void(mask, compressed) # type: ignore[no-any-return] + return cast(list[int] | str, void(mask, compressed)) @deprecated(target=_rle_to_mask, deprecated_in="0.28.0", remove_in="0.30.0") # type: ignore[untyped-decorator] def rle_to_mask( - rle: npt.NDArray[np.integer[Any]] | list[int] | str | bytes, + rle: npt.NDArray[np.integer] | list[int] | str | bytes, resolution_wh: tuple[int, int], ) -> npt.NDArray[np.bool_]: """Deprecated. Use `supervision.detection.utils.converters.rle_to_mask`.""" - return void(rle, resolution_wh) + return cast(npt.NDArray[np.bool_], void(rle, resolution_wh)) if TYPE_CHECKING: @@ -60,7 +60,7 @@ def approximate_mask_with_polygons( minimum_detection_area = min_image_area_percentage * image_area maximum_detection_area = max_image_area_percentage * image_area - polygons = mask_to_polygons(mask=mask) + polygons = cast(list[npt.NDArray[np.number]], mask_to_polygons(mask=mask)) if len(polygons) == 1: polygons = filter_polygons_by_area( polygons=polygons, min_area=None, max_area=maximum_detection_area diff --git a/src/supervision/detection/compact_mask.py b/src/supervision/detection/compact_mask.py index b2ffdac024..9244159e51 100644 --- a/src/supervision/detection/compact_mask.py +++ b/src/supervision/detection/compact_mask.py @@ -13,7 +13,7 @@ import os from collections.abc import Iterator -from typing import Any +from typing import cast, overload import numpy as np import numpy.typing as npt @@ -323,9 +323,10 @@ def _rle_resize( col_cache: dict[int, list[int]] = {} scaled_cols = [] for src_c in col_map: - if src_c not in col_cache: - col_cache[src_c] = _rle_scale_col(per_col[src_c], crop_h, row_map) - scaled_cols.append(col_cache[src_c]) + src_c_int = int(src_c) + if src_c_int not in col_cache: + col_cache[src_c_int] = _rle_scale_col(per_col[src_c_int], crop_h, row_map) + scaled_cols.append(col_cache[src_c_int]) return _rle_join_cols(scaled_cols, new_total) @@ -476,7 +477,7 @@ def __init__( def from_dense( cls, masks: npt.NDArray[np.bool_], - xyxy: npt.NDArray[Any], + xyxy: npt.NDArray[np.number], image_shape: tuple[int, int], ) -> CompactMask: """Create a :class:`CompactMask` from a dense ``(N, H, W)`` bool array. @@ -720,7 +721,7 @@ def bbox_xyxy(self) -> npt.NDArray[np.int32]: return np.column_stack((x1, y1, x2, y2)).astype(np.int32, copy=False) @property - def dtype(self) -> np.dtype[Any]: + def dtype(self) -> np.dtype[np.bool_]: """Return ``np.dtype(bool)`` — always. Returns: @@ -763,7 +764,9 @@ def area(self) -> npt.NDArray[np.int64]: """ return np.array([_rle_area(rle) for rle in self._rles], dtype=np.int64) - def sum(self, axis: int | tuple[int, ...] | None = None) -> npt.NDArray[Any] | int: + def sum( + self, axis: int | tuple[int, ...] | None = None + ) -> npt.NDArray[np.int64] | np.int64: """NumPy-compatible sum with a fast path for per-mask area. When ``axis=(1, 2)``, returns the per-mask True-pixel count via @@ -790,11 +793,32 @@ def sum(self, axis: int | tuple[int, ...] | None = None) -> npt.NDArray[Any] | i """ if axis == (1, 2): return self.area - return self.to_dense().sum(axis=axis) + return cast(npt.NDArray[np.int64] | np.int64, self.to_dense().sum(axis=axis)) + + @overload + def __getitem__(self, index: int | np.integer) -> npt.NDArray[np.bool_]: ... + + @overload + def __getitem__( + self, + index: slice + | list[int] + | list[bool] + | npt.NDArray[np.int_] + | npt.NDArray[np.bool_], + ) -> CompactMask: ... def __getitem__( self, - index: int | slice | list[Any] | npt.NDArray[Any], + index: ( + int + | np.integer + | slice + | list[int] + | list[bool] + | npt.NDArray[np.int_] + | npt.NDArray[np.bool_] + ), ) -> npt.NDArray[np.bool_] | CompactMask: """Index into the mask collection. @@ -860,7 +884,9 @@ def __getitem__( new_offsets: npt.NDArray[np.int32] = self._offsets[idx_arr] return CompactMask(new_rles, new_crop_shapes, new_offsets, self._image_shape) - def __array__(self, dtype: np.dtype[Any] | None = None) -> npt.NDArray[Any]: + def __array__( + self, dtype: np.dtype[np.generic] | None = None + ) -> npt.NDArray[np.generic]: """NumPy interop: materialise as a dense ``(N, H, W)`` array. Called by ``np.asarray(compact_mask)`` and similar NumPy functions. diff --git a/src/supervision/detection/core.py b/src/supervision/detection/core.py index 30baca78b8..942da29337 100644 --- a/src/supervision/detection/core.py +++ b/src/supervision/detection/core.py @@ -1,9 +1,10 @@ from __future__ import annotations -from collections.abc import Iterator +import builtins +from collections.abc import Iterator, Mapping, Sequence from dataclasses import dataclass, field from functools import reduce -from typing import Any, cast +from typing import Protocol, cast, overload import numpy as np import numpy.typing as npt @@ -18,12 +19,15 @@ process_transformers_v4_segmentation_result, process_transformers_v5_segmentation_result, ) +from supervision.detection.utils._typing import DetectionDataType from supervision.detection.utils.converters import ( mask_to_xyxy, polygon_to_mask, xywh_to_xyxy, ) from supervision.detection.utils.internal import ( + _TypeRoboflowResult, + _TypeUltralyticsResult, extract_ultralytics_masks, get_data_item, is_data_equal, @@ -60,6 +64,114 @@ from supervision.validators import validate_detections_fields, validate_resolution +class _TypeTensorLike(Protocol): + def cpu(self) -> _TypeTensorLike: + """Return a CPU copy of the tensor-like object.""" + + def numpy(self) -> npt.NDArray[np.generic]: + """Convert the tensor-like object to a NumPy array.""" + + def astype(self, dtype: object) -> _TypeTensorLike: + """Cast the tensor-like object to the requested dtype.""" + + def int(self) -> _TypeTensorLike: + """Cast the tensor-like object to integers.""" + + def numel(self) -> builtins.int: + """Return the number of elements.""" + + +class _TypeYoloV5Results(Protocol): + pred: Sequence[_TypeTensorLike] + + +class _TypeUltralyticsBoxes(Protocol): + cls: _TypeTensorLike + xyxy: _TypeTensorLike + conf: _TypeTensorLike + id: _TypeTensorLike | None + + +class _TypeUltralyticsOBB(Protocol): + cls: _TypeTensorLike + xyxyxyxy: _TypeTensorLike + xyxy: _TypeTensorLike + conf: _TypeTensorLike + id: _TypeTensorLike | None + + +class _TypeUltralyticsResults(_TypeUltralyticsResult, Protocol): + names: dict[int, str] + boxes: _TypeUltralyticsBoxes | None + obb: _TypeUltralyticsOBB | None + + def __len__(self) -> int: + """Return the number of detections in the result.""" + + +class _TypeYoloNasPrediction(Protocol): + bboxes_xyxy: npt.NDArray[np.float32] + confidence: npt.NDArray[np.float32] + labels: npt.NDArray[np.int_] + + +class _TypeYoloNasResults(Protocol): + prediction: _TypeYoloNasPrediction + + +class _TypeTensorflowResult(Protocol): + def __getitem__(self, key: str) -> Sequence[_TypeTensorLike]: + """Return a tensor sequence for a given prediction key.""" + + +class _TypeDeepSparseResults(Protocol): + boxes: Sequence[npt.NDArray[np.float32]] + scores: Sequence[npt.NDArray[np.float32]] + labels: Sequence[npt.NDArray[np.int_]] + + +class _TypeMMDetInstances(Protocol): + bboxes: _TypeTensorLike + scores: _TypeTensorLike + labels: _TypeTensorLike + masks: _TypeTensorLike + + def __contains__(self, item: object) -> bool: + """Return whether a field is available.""" + + +class _TypeMMDetResults(Protocol): + pred_instances: _TypeMMDetInstances + + +class _TypeDetectron2Boxes(Protocol): + tensor: _TypeTensorLike + + +class _TypeDetectron2Instances(Protocol): + pred_boxes: _TypeDetectron2Boxes + scores: _TypeTensorLike + pred_classes: _TypeTensorLike + pred_masks: _TypeTensorLike | None + + +class _TypeDetectron2Results(Protocol): + instances: _TypeDetectron2Instances + + +class _TypeNcnnRect(Protocol): + x: np.generic + y: np.generic + w: np.generic + h: np.generic + + +class _TypeNcnnResult(Protocol): + rect: _TypeNcnnRect + prob: float + label: int + + @dataclass class Detections: """ @@ -149,13 +261,13 @@ class simplifies data manipulation and filtering, providing a uniform API for as the video name, camera parameters, timestamp, or other global metadata. """ # noqa: E501 // docs - xyxy: npt.NDArray[np.generic] - mask: npt.NDArray[np.generic] | CompactMask | None = None - confidence: npt.NDArray[np.generic] | None = None - class_id: npt.NDArray[np.generic] | None = None - tracker_id: npt.NDArray[np.generic] | None = None - data: dict[str, npt.NDArray[np.generic] | list[Any]] = field(default_factory=dict) - metadata: dict[str, Any] = field(default_factory=dict) + xyxy: npt.NDArray[np.number] + mask: npt.NDArray[np.bool_] | CompactMask | None = None + confidence: npt.NDArray[np.floating] | None = None + class_id: npt.NDArray[np.integer] | None = None + tracker_id: npt.NDArray[np.integer] | None = None + data: DetectionDataType = field(default_factory=dict) + metadata: dict[str, object] = field(default_factory=dict) def __post_init__(self) -> None: validate_detections_fields( @@ -177,12 +289,12 @@ def __iter__( self, ) -> Iterator[ tuple[ - npt.NDArray[np.generic], - npt.NDArray[np.generic] | None, - np.generic | None, - np.generic | None, - np.generic | None, - dict[str, npt.NDArray[np.generic] | list[Any]], + npt.NDArray[np.number], + npt.NDArray[np.bool_] | CompactMask | None, + np.floating | None, + np.integer | None, + np.integer | None, + DetectionDataType, ] ]: """ @@ -190,32 +302,66 @@ def __iter__( `(xyxy, mask, confidence, class_id, tracker_id, data)` for each detection. """ for i in range(len(self.xyxy)): + mask = ( + cast(npt.NDArray[np.bool_], self.mask[i]) + if self.mask is not None + else None + ) + confidence = ( + cast(np.floating, self.confidence[i]) + if self.confidence is not None + else None + ) + class_id = ( + cast(np.integer, self.class_id[i]) + if self.class_id is not None + else None + ) + tracker_id = ( + cast(np.integer, self.tracker_id[i]) + if self.tracker_id is not None + else None + ) yield ( self.xyxy[i], - self.mask[i] if self.mask is not None else None, - self.confidence[i] if self.confidence is not None else None, - self.class_id[i] if self.class_id is not None else None, - self.tracker_id[i] if self.tracker_id is not None else None, + mask, + confidence, + class_id, + tracker_id, get_data_item(self.data, i), ) def __eq__(self, other: object) -> bool: if not isinstance(other, Detections): return NotImplemented - return all( - [ - np.array_equal(self.xyxy, other.xyxy), - np.array_equal(self.mask, other.mask), - np.array_equal(self.class_id, other.class_id), - np.array_equal(self.confidence, other.confidence), - np.array_equal(self.tracker_id, other.tracker_id), - is_data_equal(self.data, other.data), - is_metadata_equal(self.metadata, other.metadata), - ] + if not np.array_equal(self.xyxy, other.xyxy): + return False + if self.mask is None or other.mask is None: + if self.mask is not other.mask: + return False + elif not np.array_equal(np.asarray(self.mask), np.asarray(other.mask)): + return False + if self.class_id is None or other.class_id is None: + if self.class_id is not other.class_id: + return False + elif not np.array_equal(self.class_id, other.class_id): + return False + if self.confidence is None or other.confidence is None: + if self.confidence is not other.confidence: + return False + elif not np.array_equal(self.confidence, other.confidence): + return False + if self.tracker_id is None or other.tracker_id is None: + if self.tracker_id is not other.tracker_id: + return False + elif not np.array_equal(self.tracker_id, other.tracker_id): + return False + return is_data_equal(self.data, other.data) and is_metadata_equal( + self.metadata, other.metadata ) @classmethod - def from_yolov5(cls, yolov5_results: Any) -> Detections: + def from_yolov5(cls, yolov5_results: _TypeYoloV5Results) -> Detections: """ Creates a Detections instance from a [YOLOv5](https://github.com/ultralytics/yolov5) inference result. @@ -238,16 +384,21 @@ def from_yolov5(cls, yolov5_results: Any) -> Detections: detections = sv.Detections.from_yolov5(result) ``` """ - yolov5_detections_predictions = yolov5_results.pred[0].cpu().cpu().numpy() + yolov5_detections_predictions = cast( + npt.NDArray[np.float32], + yolov5_results.pred[0].cpu().cpu().numpy(), + ) return cls( xyxy=yolov5_detections_predictions[:, :4], confidence=yolov5_detections_predictions[:, 4], - class_id=yolov5_detections_predictions[:, 5].astype(int), + class_id=yolov5_detections_predictions[:, 5].astype(np.int64), ) @classmethod - def from_ultralytics(cls, ultralytics_results: Any) -> Detections: + def from_ultralytics( + cls, ultralytics_results: _TypeUltralyticsResults + ) -> Detections: """ Creates a `sv.Detections` instance from a [YOLOv8](https://github.com/ultralytics/ultralytics) inference result. @@ -279,15 +430,32 @@ def from_ultralytics(cls, ultralytics_results: Any) -> Detections: """ if hasattr(ultralytics_results, "obb") and ultralytics_results.obb is not None: - class_id = ultralytics_results.obb.cls.cpu().numpy().astype(int) - class_names = np.array([ultralytics_results.names[i] for i in class_id]) - oriented_box_coordinates = ultralytics_results.obb.xyxyxyxy.cpu().numpy() + class_id = cast( + npt.NDArray[np.int64], + ultralytics_results.obb.cls.cpu().numpy().astype(np.int64), + ) + class_names = np.array( + [ultralytics_results.names[int(i)] for i in class_id], dtype=str + ) + oriented_box_coordinates = cast( + npt.NDArray[np.float32], + ultralytics_results.obb.xyxyxyxy.cpu().numpy().astype(np.float32), + ) return cls( - xyxy=ultralytics_results.obb.xyxy.cpu().numpy(), - confidence=ultralytics_results.obb.conf.cpu().numpy(), + xyxy=cast( + npt.NDArray[np.float32], + ultralytics_results.obb.xyxy.cpu().numpy().astype(np.float32), + ), + confidence=cast( + npt.NDArray[np.float32], + ultralytics_results.obb.conf.cpu().numpy().astype(np.float32), + ), class_id=class_id, tracker_id=( - ultralytics_results.obb.id.int().cpu().numpy() + cast( + npt.NDArray[np.int64], + ultralytics_results.obb.id.int().cpu().numpy().astype(np.int64), + ) if ultralytics_results.obb.id is not None else None ), @@ -299,25 +467,44 @@ def from_ultralytics(cls, ultralytics_results: Any) -> Detections: if hasattr(ultralytics_results, "boxes") and ultralytics_results.boxes is None: masks = extract_ultralytics_masks(ultralytics_results) + if masks is None: + return cls.empty() return cls( - xyxy=mask_to_xyxy(masks), + xyxy=cast(npt.NDArray[np.float32], mask_to_xyxy(masks)), mask=masks, - class_id=np.arange(len(ultralytics_results)), + class_id=np.arange(len(ultralytics_results), dtype=np.int64), ) if ( hasattr(ultralytics_results, "boxes") and ultralytics_results.boxes is not None ): - class_id = ultralytics_results.boxes.cls.cpu().numpy().astype(int) - class_names = np.array([ultralytics_results.names[i] for i in class_id]) + class_id = cast( + npt.NDArray[np.int64], + ultralytics_results.boxes.cls.cpu().numpy().astype(np.int64), + ) + class_names = np.array( + [ultralytics_results.names[int(i)] for i in class_id], dtype=str + ) return cls( - xyxy=ultralytics_results.boxes.xyxy.cpu().numpy(), - confidence=ultralytics_results.boxes.conf.cpu().numpy(), + xyxy=cast( + npt.NDArray[np.float32], + ultralytics_results.boxes.xyxy.cpu().numpy().astype(np.float32), + ), + confidence=cast( + npt.NDArray[np.float32], + ultralytics_results.boxes.conf.cpu().numpy().astype(np.float32), + ), class_id=class_id, mask=extract_ultralytics_masks(ultralytics_results), tracker_id=( - ultralytics_results.boxes.id.int().cpu().numpy() + cast( + npt.NDArray[np.int64], + ultralytics_results.boxes.id.int() + .cpu() + .numpy() + .astype(np.int64), + ) if ultralytics_results.boxes.id is not None else None ), @@ -327,7 +514,7 @@ def from_ultralytics(cls, ultralytics_results: Any) -> Detections: return cls.empty() @classmethod - def from_yolo_nas(cls, yolo_nas_results: Any) -> Detections: + def from_yolo_nas(cls, yolo_nas_results: _TypeYoloNasResults) -> Detections: """ Creates a Detections instance from a [YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md) @@ -360,12 +547,12 @@ def from_yolo_nas(cls, yolo_nas_results: Any) -> Detections: return cls( xyxy=yolo_nas_results.prediction.bboxes_xyxy, confidence=yolo_nas_results.prediction.confidence, - class_id=yolo_nas_results.prediction.labels.astype(int), + class_id=yolo_nas_results.prediction.labels.astype(np.int64), ) @classmethod def from_tensorflow( - cls, tensorflow_results: dict[str, Any], resolution_wh: tuple[int, int] + cls, tensorflow_results: _TypeTensorflowResult, resolution_wh: tuple[int, int] ) -> Detections: """ Creates a Detections instance from a @@ -398,18 +585,26 @@ def from_tensorflow( ``` """ - boxes = tensorflow_results["detection_boxes"][0].numpy() + boxes = cast( + npt.NDArray[np.float32], tensorflow_results["detection_boxes"][0].numpy() + ) boxes[:, [0, 2]] *= resolution_wh[0] boxes[:, [1, 3]] *= resolution_wh[1] boxes = boxes[:, [1, 0, 3, 2]] return cls( xyxy=boxes, - confidence=tensorflow_results["detection_scores"][0].numpy(), - class_id=tensorflow_results["detection_classes"][0].numpy().astype(int), + confidence=cast( + npt.NDArray[np.float32], + tensorflow_results["detection_scores"][0].numpy(), + ), + class_id=cast( + npt.NDArray[np.int64], + tensorflow_results["detection_classes"][0].numpy().astype(np.int64), + ), ) @classmethod - def from_deepsparse(cls, deepsparse_results: Any) -> Detections: + def from_deepsparse(cls, deepsparse_results: _TypeDeepSparseResults) -> Detections: """ Creates a Detections instance from a [DeepSparse](https://github.com/neuralmagic/deepsparse) @@ -439,13 +634,13 @@ def from_deepsparse(cls, deepsparse_results: Any) -> Detections: return cls.empty() return cls( - xyxy=np.array(deepsparse_results.boxes[0]), - confidence=np.array(deepsparse_results.scores[0]), - class_id=np.array(deepsparse_results.labels[0]).astype(float).astype(int), + xyxy=np.array(deepsparse_results.boxes[0], dtype=np.float32), + confidence=np.array(deepsparse_results.scores[0], dtype=np.float32), + class_id=np.array(deepsparse_results.labels[0], dtype=np.int64), ) @classmethod - def from_mmdetection(cls, mmdet_results: Any) -> Detections: + def from_mmdetection(cls, mmdet_results: _TypeMMDetResults) -> Detections: """ Creates a Detections instance from a [mmdetection](https://github.com/open-mmlab/mmdetection) and @@ -472,11 +667,23 @@ def from_mmdetection(cls, mmdet_results: Any) -> Detections: """ return cls( - xyxy=mmdet_results.pred_instances.bboxes.cpu().numpy(), - confidence=mmdet_results.pred_instances.scores.cpu().numpy(), - class_id=mmdet_results.pred_instances.labels.cpu().numpy().astype(int), + xyxy=cast( + npt.NDArray[np.float32], + mmdet_results.pred_instances.bboxes.cpu().numpy().astype(np.float32), + ), + confidence=cast( + npt.NDArray[np.float32], + mmdet_results.pred_instances.scores.cpu().numpy().astype(np.float32), + ), + class_id=cast( + npt.NDArray[np.int64], + mmdet_results.pred_instances.labels.cpu().numpy().astype(np.int64), + ), mask=( - mmdet_results.pred_instances.masks.cpu().numpy() + cast( + npt.NDArray[np.bool_], + mmdet_results.pred_instances.masks.cpu().numpy(), + ) if "masks" in mmdet_results.pred_instances else None ), @@ -485,7 +692,7 @@ def from_mmdetection(cls, mmdet_results: Any) -> Detections: @classmethod def from_transformers( cls, - transformers_results: dict[str, Any], + transformers_results: dict[str, object], id2label: dict[int, str] | None = None, ) -> Detections: """ @@ -564,7 +771,7 @@ def from_transformers( ) @classmethod - def from_detectron2(cls, detectron2_results: Any) -> Detections: + def from_detectron2(cls, detectron2_results: Mapping[str, object]) -> Detections: """ Create a Detections object from the [Detectron2](https://github.com/facebookresearch/detectron2) inference result. @@ -595,23 +802,21 @@ class IDs, and confidences of the predictions. detections = sv.Detections.from_detectron2(result) ``` """ + instances = cast(_TypeDetectron2Instances, detectron2_results["instances"]) return cls( - xyxy=detectron2_results["instances"].pred_boxes.tensor.cpu().numpy(), - confidence=detectron2_results["instances"].scores.cpu().numpy(), + xyxy=instances.pred_boxes.tensor.cpu().numpy().astype(np.float32), + confidence=instances.scores.cpu().numpy().astype(np.float32), mask=( - detectron2_results["instances"].pred_masks.cpu().numpy() - if hasattr(detectron2_results["instances"], "pred_masks") + cast(npt.NDArray[np.bool_], instances.pred_masks.cpu().numpy()) + if instances.pred_masks is not None else None ), - class_id=detectron2_results["instances"] - .pred_classes.cpu() - .numpy() - .astype(int), + class_id=instances.pred_classes.cpu().numpy().astype(np.int64), ) @classmethod - def from_inference(cls, roboflow_result: dict[str, Any] | Any) -> Detections: + def from_inference(cls, roboflow_result: object) -> Detections: """ Create a `sv.Detections` object from the [Roboflow](https://roboflow.com/) API inference result or the [Inference](https://inference.roboflow.com/) @@ -645,12 +850,12 @@ def from_inference(cls, roboflow_result: dict[str, Any] | Any) -> Detections: elif hasattr(roboflow_result, "json"): roboflow_result = roboflow_result.json() xyxy, confidence, class_id, masks, trackers, data = process_roboflow_result( - roboflow_result=roboflow_result + roboflow_result=cast(_TypeRoboflowResult, roboflow_result) ) if np.asarray(xyxy).shape[0] == 0: empty_detection = cls.empty() - empty_detection.data = {CLASS_NAME_DATA_FIELD: np.empty(0)} + empty_detection.data = {CLASS_NAME_DATA_FIELD: np.empty(0, dtype=str)} return empty_detection return cls( @@ -663,7 +868,7 @@ def from_inference(cls, roboflow_result: dict[str, Any] | Any) -> Detections: ) @classmethod - def from_sam(cls, sam_result: list[dict[str, Any]]) -> Detections: + def from_sam(cls, sam_result: list[dict[str, object]]) -> Detections: """ Creates a Detections instance from [Segment Anything Model](https://github.com/facebookresearch/segment-anything) @@ -692,11 +897,29 @@ def from_sam(cls, sam_result: list[dict[str, Any]]) -> Detections: """ sorted_generated_masks = sorted( - sam_result, key=lambda x: x["area"], reverse=True + sam_result, key=lambda x: cast(float, x["area"]), reverse=True ) - xywh = np.array([mask["bbox"] for mask in sorted_generated_masks]) - mask = np.array([mask["segmentation"] for mask in sorted_generated_masks]) + xywh = cast( + npt.NDArray[np.float32], + np.array( + [ + cast(Sequence[float], mask["bbox"]) + for mask in sorted_generated_masks + ], + dtype=np.float32, + ), + ) + mask = cast( + npt.NDArray[np.bool_], + np.array( + [ + cast(npt.NDArray[np.bool_], mask["segmentation"]) + for mask in sorted_generated_masks + ], + dtype=bool, + ), + ) if np.asarray(xywh).shape[0] == 0: return cls.empty() @@ -706,7 +929,7 @@ def from_sam(cls, sam_result: list[dict[str, Any]]) -> Detections: @classmethod def from_sam3( - cls, sam3_result: dict[str, Any] | Any, resolution_wh: tuple[int, int] + cls, sam3_result: dict[str, object] | object, resolution_wh: tuple[int, int] ) -> Detections: """ Creates a Detections instance from @@ -758,15 +981,18 @@ def from_sam3( """ width, height = validate_resolution(resolution_wh) - masks = [] - confidences = [] - class_ids = [] + masks: list[npt.NDArray[np.bool_]] = [] + confidences: list[float] = [] + class_ids: list[int] = [] if isinstance(sam3_result, dict): - prompt_results = sam3_result.get("prompt_results", []) + prompt_results = cast(list[object], sam3_result.get("prompt_results", [])) if not prompt_results and "predictions" in sam3_result: prompt_results = [ - {"predictions": sam3_result["predictions"], "prompt_index": 0} + { + "predictions": sam3_result["predictions"], + "prompt_index": 0, + } ] else: prompt_results = getattr(sam3_result, "prompt_results", []) @@ -780,25 +1006,34 @@ def from_sam3( for i, prompt_result in enumerate(prompt_results): if isinstance(prompt_result, dict): - predictions = prompt_result.get("predictions", []) - prompt_index = prompt_result.get("prompt_index", i) + predictions = cast(list[object], prompt_result.get("predictions", [])) + prompt_index = cast(int, prompt_result.get("prompt_index", i)) else: - predictions = getattr(prompt_result, "predictions", []) - prompt_index = getattr(prompt_result, "prompt_index", i) + predictions = cast( + list[object], getattr(prompt_result, "predictions", []) + ) + prompt_index = cast(int, getattr(prompt_result, "prompt_index", i)) for prediction in predictions: if isinstance(prediction, dict): - prediction_format = prediction.get("format") + prediction_format = cast(str | None, prediction.get("format")) if prediction_format and prediction_format != "polygon": continue - pred_masks = prediction.get("masks", []) - confidence = prediction.get("confidence", 1.0) + pred_masks = cast( + list[Sequence[Sequence[float]]], prediction.get("masks", []) + ) + confidence = cast(float, prediction.get("confidence", 1.0)) else: - prediction_format = getattr(prediction, "format", None) + prediction_format = cast( + str | None, getattr(prediction, "format", None) + ) if prediction_format and prediction_format != "polygon": continue - pred_masks = getattr(prediction, "masks", []) - confidence = getattr(prediction, "confidence", 1.0) + pred_masks = cast( + list[Sequence[Sequence[float]]], + getattr(prediction, "masks", []), + ) + confidence = cast(float, getattr(prediction, "confidence", 1.0)) if not pred_masks: continue @@ -820,18 +1055,18 @@ def from_sam3( return cls.empty() masks_np = np.stack(masks, axis=0) - xyxy = mask_to_xyxy(masks_np) + xyxy = cast(npt.NDArray[np.float32], mask_to_xyxy(masks_np)) return cls( - xyxy=xyxy.astype(np.float32), + xyxy=xyxy, mask=masks_np, confidence=np.array(confidences, dtype=np.float32), - class_id=np.array(class_ids, dtype=int), + class_id=np.array(class_ids, dtype=np.int64), ) @classmethod def from_azure_analyze_image( - cls, azure_result: dict[str, Any], class_map: dict[int, str] | None = None + cls, azure_result: dict[str, object], class_map: dict[int, str] | None = None ) -> Detections: """ Creates a Detections instance from [Azure Image Analysis 4.0]( @@ -871,11 +1106,12 @@ def from_azure_analyze_image( ``` """ if "error" in azure_result: - raise ValueError( - f"Azure API returned an error {azure_result['error']['message']}" - ) + error_message = cast(dict[str, object], azure_result["error"])["message"] + raise ValueError(f"Azure API returned an error {error_message}") - xyxy, confidences, class_ids = [], [], [] + xyxy: list[list[float]] = [] + confidences: list[float] = [] + class_ids: list[int] = [] is_dynamic_mapping = class_map is None if class_map is None: @@ -883,10 +1119,11 @@ def from_azure_analyze_image( inverted_map: dict[str, int] = {value: key for key, value in class_map.items()} - for detection in azure_result["objectsResult"]["values"]: - bbox = detection["boundingBox"] + objects_result = cast(dict[str, object], azure_result["objectsResult"]) + for detection in cast(list[dict[str, object]], objects_result["values"]): + bbox = cast(dict[str, float], detection["boundingBox"]) - tags = detection["tags"] + tags = cast(list[dict[str, object]], detection["tags"]) x0 = bbox["x"] y0 = bbox["y"] @@ -894,8 +1131,8 @@ def from_azure_analyze_image( y1 = y0 + bbox["h"] for tag in tags: - confidence = tag["confidence"] - class_name: str = tag["name"] + confidence = cast(float, tag["confidence"]) + class_name = cast(str, tag["name"]) class_id_val: int | None = inverted_map.get(class_name, None) if is_dynamic_mapping and class_id_val is None: @@ -911,13 +1148,15 @@ def from_azure_analyze_image( return Detections.empty() return cls( - xyxy=np.array(xyxy), - class_id=np.array(class_ids), - confidence=np.array(confidences), + xyxy=np.array(xyxy, dtype=np.float32), + class_id=np.array(class_ids, dtype=np.int64), + confidence=np.array(confidences, dtype=np.float32), ) @classmethod - def from_paddledet(cls, paddledet_result: Any) -> Detections: + def from_paddledet( + cls, paddledet_result: dict[str, npt.NDArray[np.generic]] + ) -> Detections: """ Creates a Detections instance from [PaddleDetection](https://github.com/PaddlePaddle/PaddleDetection) @@ -953,14 +1192,17 @@ def from_paddledet(cls, paddledet_result: Any) -> Detections: return cls.empty() return cls( - xyxy=paddledet_result["bbox"][:, 2:6], - confidence=paddledet_result["bbox"][:, 1], - class_id=paddledet_result["bbox"][:, 0].astype(int), + xyxy=cast(npt.NDArray[np.float32], paddledet_result["bbox"][:, 2:6]), + confidence=cast(npt.NDArray[np.float32], paddledet_result["bbox"][:, 1]), + class_id=cast( + npt.NDArray[np.int64], + paddledet_result["bbox"][:, 0].astype(np.int64), + ), ) @classmethod def from_lmm( - cls, lmm: LMM | str, result: str | dict[str, Any], **kwargs: Any + cls, lmm: LMM | str, result: str | dict[str, object], **kwargs: object ) -> Detections: """ !!! deprecated "Deprecated" @@ -1449,7 +1691,7 @@ def from_lmm( @classmethod def from_vlm( - cls, vlm: VLM | str, result: str | dict[str, Any], **kwargs: Any + cls, vlm: VLM | str, result: str | dict[str, object], **kwargs: object ) -> Detections: """ @@ -1874,79 +2116,165 @@ def from_vlm( if vlm == VLM.PALIGEMMA: assert isinstance(result, str) - xyxy, class_id, class_name = from_paligemma(result, **kwargs) - data: dict[str, npt.NDArray[np.generic] | list[Any]] = { - CLASS_NAME_DATA_FIELD: class_name, + resolution_wh = cast(tuple[int, int], kwargs["resolution_wh"]) + classes = cast(list[str] | None, kwargs.get("classes")) + paligemma_xyxy, paligemma_class_id, paligemma_class_name = from_paligemma( + result, + resolution_wh=resolution_wh, + classes=classes, + ) + paligemma_data: DetectionDataType = { + CLASS_NAME_DATA_FIELD: cast( + npt.NDArray[np.generic], paligemma_class_name + ), } - return cls(xyxy=xyxy, class_id=class_id, data=data) + return cls( + xyxy=paligemma_xyxy, + class_id=paligemma_class_id, + data=paligemma_data, + ) if vlm == VLM.QWEN_2_5_VL: assert isinstance(result, str) - xyxy, class_id, class_name = from_qwen_2_5_vl(result, **kwargs) - data = {CLASS_NAME_DATA_FIELD: class_name} - confidence_arr: npt.NDArray[np.floating[Any]] = np.ones( - len(xyxy), dtype=float + input_wh = cast(tuple[int, int], kwargs["input_wh"]) + resolution_wh = cast(tuple[int, int], kwargs["resolution_wh"]) + classes = cast(list[str] | None, kwargs.get("classes")) + qwen_xyxy, qwen_class_id, qwen_class_name = from_qwen_2_5_vl( + result, + input_wh=input_wh, + resolution_wh=resolution_wh, + classes=classes, + ) + qwen_data: DetectionDataType = { + CLASS_NAME_DATA_FIELD: cast(npt.NDArray[np.generic], qwen_class_name) + } + confidence_arr: npt.NDArray[np.float32] = np.ones( + len(qwen_xyxy), dtype=np.float32 ) return cls( - xyxy=xyxy, class_id=class_id, confidence=confidence_arr, data=data + xyxy=qwen_xyxy, + class_id=qwen_class_id, + confidence=confidence_arr, + data=qwen_data, ) if vlm == VLM.QWEN_3_VL: assert isinstance(result, str) - xyxy, class_id, class_name = from_qwen_3_vl(result, **kwargs) - data = {CLASS_NAME_DATA_FIELD: class_name} - confidence_arr = np.ones(len(xyxy), dtype=float) + resolution_wh = cast(tuple[int, int], kwargs["resolution_wh"]) + classes = cast(list[str] | None, kwargs.get("classes")) + qwen3_xyxy, qwen3_class_id, qwen3_class_name = from_qwen_3_vl( + result, + resolution_wh=resolution_wh, + classes=classes, + ) + qwen3_data: DetectionDataType = { + CLASS_NAME_DATA_FIELD: cast(npt.NDArray[np.generic], qwen3_class_name) + } + confidence_arr = np.ones(len(qwen3_xyxy), dtype=float) return cls( - xyxy=xyxy, class_id=class_id, confidence=confidence_arr, data=data + xyxy=qwen3_xyxy, + class_id=qwen3_class_id, + confidence=confidence_arr, + data=qwen3_data, ) if vlm == VLM.DEEPSEEK_VL_2: assert isinstance(result, str) - xyxy, class_id, class_name = from_deepseek_vl_2(result, **kwargs) - data = {CLASS_NAME_DATA_FIELD: class_name} - return cls(xyxy=xyxy, class_id=class_id, data=data) + resolution_wh = cast(tuple[int, int], kwargs["resolution_wh"]) + classes = cast(list[str] | None, kwargs.get("classes")) + deepseek_xyxy, deepseek_class_id, deepseek_class_name = from_deepseek_vl_2( + result, + resolution_wh=resolution_wh, + classes=classes, + ) + deepseek_data: DetectionDataType = { + CLASS_NAME_DATA_FIELD: cast( + npt.NDArray[np.generic], deepseek_class_name + ) + } + return cls( + xyxy=deepseek_xyxy, class_id=deepseek_class_id, data=deepseek_data + ) if vlm == VLM.FLORENCE_2: assert isinstance(result, dict) - xyxy, labels, mask, xyxyxyxy = from_florence_2(result, **kwargs) - if len(xyxy) == 0: + resolution_wh = cast(tuple[int, int], kwargs["resolution_wh"]) + florence_xyxy, florence_labels, florence_mask, florence_xyxyxyxy = ( + from_florence_2(result, resolution_wh=resolution_wh) + ) + if len(florence_xyxy) == 0: return cls.empty() - data = {} - if labels is not None: - data[CLASS_NAME_DATA_FIELD] = labels - if xyxyxyxy is not None: - data[ORIENTED_BOX_COORDINATES] = xyxyxyxy + florence_data: DetectionDataType = {} + if florence_labels is not None: + florence_data[CLASS_NAME_DATA_FIELD] = cast( + npt.NDArray[np.generic], florence_labels + ) + if florence_xyxyxyxy is not None: + florence_data[ORIENTED_BOX_COORDINATES] = cast( + npt.NDArray[np.generic], florence_xyxyxyxy + ) - return cls(xyxy=xyxy, mask=mask, data=data) + return cls( + xyxy=florence_xyxy, + mask=florence_mask, + data=florence_data, + ) if vlm == VLM.GOOGLE_GEMINI_2_0: assert isinstance(result, str) - xyxy, class_id, class_name = from_google_gemini_2_0(result, **kwargs) - data = {CLASS_NAME_DATA_FIELD: class_name} - return cls(xyxy=xyxy, class_id=class_id, data=data) + resolution_wh = cast(tuple[int, int], kwargs["resolution_wh"]) + classes = cast(list[str] | None, kwargs.get("classes")) + gemini20_xyxy, gemini20_class_id, gemini20_class_name = ( + from_google_gemini_2_0( + result, + resolution_wh=resolution_wh, + classes=classes, + ) + ) + gemini20_data: DetectionDataType = { + CLASS_NAME_DATA_FIELD: cast( + npt.NDArray[np.generic], gemini20_class_name + ) + } + return cls( + xyxy=gemini20_xyxy, + class_id=gemini20_class_id, + data=gemini20_data, + ) if vlm == VLM.MOONDREAM: assert isinstance(result, dict) - xyxy = from_moondream(result, **kwargs) - return cls(xyxy=xyxy) + resolution_wh = cast(tuple[int, int], kwargs["resolution_wh"]) + moondream_xyxy = from_moondream(result, resolution_wh=resolution_wh) + return cls(xyxy=moondream_xyxy) if vlm == VLM.GOOGLE_GEMINI_2_5: assert isinstance(result, str) - gemini_result = from_google_gemini_2_5(result, **kwargs) - data = {CLASS_NAME_DATA_FIELD: gemini_result[2]} + resolution_wh = cast(tuple[int, int], kwargs["resolution_wh"]) + classes = cast(list[str] | None, kwargs.get("classes")) + gemini25_result = from_google_gemini_2_5( + result, + resolution_wh=resolution_wh, + classes=classes, + ) + gemini25_data: DetectionDataType = { + CLASS_NAME_DATA_FIELD: cast(npt.NDArray[np.generic], gemini25_result[2]) + } return cls( - xyxy=gemini_result[0], - class_id=gemini_result[1], - mask=gemini_result[4], - confidence=gemini_result[3], - data=data, + xyxy=gemini25_result[0], + class_id=gemini25_result[1], + mask=gemini25_result[4], + confidence=gemini25_result[3], + data=gemini25_data, ) return cls.empty() @classmethod - def from_easyocr(cls, easyocr_results: list[Any]) -> Detections: + def from_easyocr( + cls, easyocr_results: list[tuple[Sequence[Sequence[float]], str, float | None]] + ) -> Detections: """ Create a Detections object from the [EasyOCR](https://github.com/JaidedAI/EasyOCR) result. @@ -1992,7 +2320,7 @@ def from_easyocr(cls, easyocr_results: list[Any]) -> Detections: ) @classmethod - def from_ncnn(cls, ncnn_results: Any) -> Detections: + def from_ncnn(cls, ncnn_results: Sequence[_TypeNcnnResult]) -> Detections: """ Creates a Detections instance from the [ncnn](https://github.com/Tencent/ncnn) inference result. @@ -2165,7 +2493,10 @@ def merge(cls, detections_list: list[Detections]) -> Detections: data=detections.data, ) - xyxy = np.vstack([d.xyxy for d in detections_list]) + xyxy = cast( + npt.NDArray[np.number], + np.vstack([np.asarray(d.xyxy) for d in detections_list]), + ) def stack_or_none( name: str, @@ -2177,15 +2508,21 @@ def stack_or_none( if name == "mask": masks = [d.__getattribute__(name) for d in detections_list] if all(isinstance(m, CompactMask) for m in masks): - return CompactMask.merge(masks) + return CompactMask.merge(cast(list[CompactMask], masks)) # Mixed or all-ndarray: __array__ auto-converts any CompactMask. - return np.vstack([np.asarray(m) for m in masks]) - return np.hstack([d.__getattribute__(name) for d in detections_list]) + return cast( + npt.NDArray[np.generic], + np.vstack([np.asarray(m) for m in masks]), + ) + return cast( + npt.NDArray[np.generic], + np.hstack([d.__getattribute__(name) for d in detections_list]), + ) - mask = stack_or_none("mask") - confidence = stack_or_none("confidence") - class_id = stack_or_none("class_id") - tracker_id = stack_or_none("tracker_id") + mask = cast(npt.NDArray[np.bool_] | CompactMask | None, stack_or_none("mask")) + confidence = cast(npt.NDArray[np.floating] | None, stack_or_none("confidence")) + class_id = cast(npt.NDArray[np.integer] | None, stack_or_none("class_id")) + tracker_id = cast(npt.NDArray[np.integer] | None, stack_or_none("tracker_id")) data = merge_data([d.data for d in detections_list]) @@ -2202,7 +2539,7 @@ def stack_or_none( metadata=metadata, ) - def get_anchors_coordinates(self, anchor: Position) -> npt.NDArray[np.generic]: + def get_anchors_coordinates(self, anchor: Position) -> npt.NDArray[np.number]: """ Calculates and returns the coordinates of a specific anchor point within the bounding boxes defined by the `xyxy` attribute. The anchor @@ -2221,55 +2558,102 @@ def get_anchors_coordinates(self, anchor: Position) -> npt.NDArray[np.generic]: Raises: ValueError: If the provided `anchor` is not supported. """ + xyxy = self.xyxy if anchor == Position.CENTER: - return np.array( - [ - (self.xyxy[:, 0] + self.xyxy[:, 2]) / 2, - (self.xyxy[:, 1] + self.xyxy[:, 3]) / 2, - ] - ).transpose() + return cast( + npt.NDArray[np.number], + np.array( + [ + (xyxy[:, 0] + xyxy[:, 2]) / 2, + (xyxy[:, 1] + xyxy[:, 3]) / 2, + ] + ).transpose(), + ) elif anchor == Position.CENTER_OF_MASS: if self.mask is None: raise ValueError( "Cannot use `Position.CENTER_OF_MASS` without a detection mask." ) - return calculate_masks_centroids(masks=self.mask) + return cast( + npt.NDArray[np.number], calculate_masks_centroids(masks=self.mask) + ) elif anchor == Position.CENTER_LEFT: - return np.array( - [ - self.xyxy[:, 0], - (self.xyxy[:, 1] + self.xyxy[:, 3]) / 2, - ] - ).transpose() + return cast( + npt.NDArray[np.number], + np.array( + [ + xyxy[:, 0], + (xyxy[:, 1] + xyxy[:, 3]) / 2, + ] + ).transpose(), + ) elif anchor == Position.CENTER_RIGHT: - return np.array( - [ - self.xyxy[:, 2], - (self.xyxy[:, 1] + self.xyxy[:, 3]) / 2, - ] - ).transpose() + return cast( + npt.NDArray[np.number], + np.array( + [ + xyxy[:, 2], + (xyxy[:, 1] + xyxy[:, 3]) / 2, + ] + ).transpose(), + ) elif anchor == Position.BOTTOM_CENTER: - return np.array( - [(self.xyxy[:, 0] + self.xyxy[:, 2]) / 2, self.xyxy[:, 3]] - ).transpose() + return cast( + npt.NDArray[np.number], + np.array([(xyxy[:, 0] + xyxy[:, 2]) / 2, xyxy[:, 3]]).transpose(), + ) elif anchor == Position.BOTTOM_LEFT: - return np.array([self.xyxy[:, 0], self.xyxy[:, 3]]).transpose() + return cast( + npt.NDArray[np.number], + np.array([xyxy[:, 0], xyxy[:, 3]]).transpose(), + ) elif anchor == Position.BOTTOM_RIGHT: - return np.array([self.xyxy[:, 2], self.xyxy[:, 3]]).transpose() + return cast( + npt.NDArray[np.number], + np.array([xyxy[:, 2], xyxy[:, 3]]).transpose(), + ) elif anchor == Position.TOP_CENTER: - return np.array( - [(self.xyxy[:, 0] + self.xyxy[:, 2]) / 2, self.xyxy[:, 1]] - ).transpose() + return cast( + npt.NDArray[np.number], + np.array([(xyxy[:, 0] + xyxy[:, 2]) / 2, xyxy[:, 1]]).transpose(), + ) elif anchor == Position.TOP_LEFT: - return np.array([self.xyxy[:, 0], self.xyxy[:, 1]]).transpose() + return cast( + npt.NDArray[np.number], + np.array([xyxy[:, 0], xyxy[:, 1]]).transpose(), + ) elif anchor == Position.TOP_RIGHT: - return np.array([self.xyxy[:, 2], self.xyxy[:, 1]]).transpose() + return cast( + npt.NDArray[np.number], + np.array([xyxy[:, 2], xyxy[:, 1]]).transpose(), + ) raise ValueError(f"{anchor} is not supported.") + @overload + def __getitem__(self, index: str) -> object: ... + + @overload def __getitem__( - self, index: int | slice | list[int] | npt.NDArray[np.generic] | str - ) -> Detections | list[Any] | npt.NDArray[np.generic] | None: + self, + index: int + | slice + | list[int] + | list[bool] + | npt.NDArray[np.int_] + | npt.NDArray[np.bool_], + ) -> Detections: ... + + def __getitem__( + self, + index: int + | slice + | list[int] + | list[bool] + | npt.NDArray[np.int_] + | npt.NDArray[np.bool_] + | str, + ) -> Detections | object | None: """ Get a subset of the Detections object or access an item from its data field. @@ -2306,23 +2690,29 @@ def __getitem__( return self if isinstance(index, int): index = [index] + mask = self.mask[index] if self.mask is not None else None + confidence = self.confidence[index] if self.confidence is not None else None + class_id = self.class_id[index] if self.class_id is not None else None + tracker_id = self.tracker_id[index] if self.tracker_id is not None else None return Detections( xyxy=self.xyxy[index], - mask=self.mask[index] if self.mask is not None else None, - confidence=self.confidence[index] if self.confidence is not None else None, - class_id=self.class_id[index] if self.class_id is not None else None, - tracker_id=self.tracker_id[index] if self.tracker_id is not None else None, + mask=mask, + confidence=confidence, + class_id=class_id, + tracker_id=tracker_id, data=get_data_item(self.data, index), metadata=self.metadata, ) - def __setitem__(self, key: str, value: npt.NDArray[np.generic] | list[Any]) -> None: + def __setitem__( + self, key: str, value: npt.NDArray[np.generic] | list[object] + ) -> None: """ Set a value in the data dictionary of the Detections object. Args: key: The key in the data dictionary to set. - value: The value to set for the key. + value: The value to set for the key. Must be a np.ndarray or list. Example: ```python @@ -2352,7 +2742,7 @@ def __setitem__(self, key: str, value: npt.NDArray[np.generic] | list[Any]) -> N self.data[key] = value @property - def area(self) -> npt.NDArray[np.generic]: + def area(self) -> npt.NDArray[np.number]: """ Calculate the area of each detection in the set of object detections. If masks field is defined property returns are of each mask. @@ -2367,11 +2757,10 @@ def area(self) -> npt.NDArray[np.generic]: if isinstance(self.mask, CompactMask): return self.mask.area return np.array([np.sum(mask) for mask in self.mask]) - else: - return self.box_area + return self.box_area @property - def box_area(self) -> npt.NDArray[np.generic]: + def box_area(self) -> npt.NDArray[np.number]: """ Calculate the area of each bounding box in the set of object detections. @@ -2380,10 +2769,11 @@ def box_area(self) -> npt.NDArray[np.generic]: box in the format of `(area_1, area_2, ..., area_n)`, where n is the number of detections. """ - return (self.xyxy[:, 3] - self.xyxy[:, 1]) * (self.xyxy[:, 2] - self.xyxy[:, 0]) + xyxy = self.xyxy + return (xyxy[:, 3] - xyxy[:, 1]) * (xyxy[:, 2] - xyxy[:, 0]) @property - def box_aspect_ratio(self) -> npt.NDArray[np.generic]: + def box_aspect_ratio(self) -> npt.NDArray[np.floating]: """ Compute the aspect ratio (width divided by height) for each bounding box. @@ -2412,10 +2802,13 @@ def box_aspect_ratio(self) -> npt.NDArray[np.generic]: # array([[10., 10., 50., 50.]]) ``` """ - widths = self.xyxy[:, 2] - self.xyxy[:, 0] - heights = self.xyxy[:, 3] - self.xyxy[:, 1] + xyxy = self.xyxy + widths = xyxy[:, 2] - xyxy[:, 0] + heights = xyxy[:, 3] - xyxy[:, 1] - aspect_ratios = np.full_like(widths, np.nan, dtype=np.float64) + aspect_ratios: npt.NDArray[np.float64] = np.full_like( + widths, np.nan, dtype=np.float64 + ) np.divide(widths, heights, out=aspect_ratios, where=heights != 0) return aspect_ratios @@ -2483,7 +2876,7 @@ def with_nms( overlap_metric=overlap_metric, ) - return cast(Detections, self[indices]) + return self[indices] def with_nmm( self, @@ -2551,7 +2944,7 @@ def with_nmm( result: list[Detections] = [] for merge_group in merge_groups: - unmerged_detections = [cast(Detections, self[i]) for i in merge_group] + unmerged_detections = [self[i] for i in merge_group] merged_detections = merge_inner_detections_objects_without_iou( unmerged_detections ) @@ -2630,7 +3023,9 @@ def merge_inner_detection_object_pair( if detections_1.mask is None and detections_2.mask is None: merged_mask = None else: - merged_mask = np.logical_or(detections_1.mask, detections_2.mask) + merged_mask = np.logical_or( + np.asarray(detections_1.mask), np.asarray(detections_2.mask) + ) if detections_1.confidence is None or detections_2.confidence is None: winning_detection = detections_1 @@ -2672,7 +3067,11 @@ def merge_inner_detections_objects( 0 ] else: - iou = box_iou_batch(detections_1.xyxy, detections_2.xyxy, overlap_metric)[0] + iou = box_iou_batch( + detections_1.xyxy, + detections_2.xyxy, + overlap_metric, + )[0] if iou < threshold: break detections_1 = merge_inner_detection_object_pair(detections_1, detections_2) diff --git a/src/supervision/detection/line_zone.py b/src/supervision/detection/line_zone.py index 3178fca24a..6d869521c4 100644 --- a/src/supervision/detection/line_zone.py +++ b/src/supervision/detection/line_zone.py @@ -5,7 +5,7 @@ from collections import Counter, defaultdict, deque from collections.abc import Iterable from functools import lru_cache -from typing import Any, Literal, cast +from typing import Literal import cv2 import numpy as np @@ -99,7 +99,7 @@ def __init__( Position.BOTTOM_RIGHT, ), minimum_crossing_threshold: int = 1, - ): + ) -> None: """ Args: start: The starting point of the line. @@ -344,7 +344,7 @@ def __init__( display_text_box: bool = True, text_orient_to_line: bool = False, text_centered: bool = True, - ): + ) -> None: """ A class for drawing the `LineZone` and its detected object count on an image. @@ -673,29 +673,33 @@ def _make_label_image( annotation_shape = (annotation_dim, annotation_dim) annotation_center = Point(annotation_dim // 2, annotation_dim // 2) - annotation = np.zeros((*annotation_shape, 3), dtype=np.uint8) - annotation_alpha = np.zeros((*annotation_shape, 1), dtype=np.uint8) - - text_args: dict[str, Any] = dict( + annotation: npt.NDArray[np.uint8] = np.zeros( + (*annotation_shape, 3), dtype=np.uint8 + ) + annotation_alpha: npt.NDArray[np.uint8] = np.zeros( + (*annotation_shape, 1), dtype=np.uint8 + ) + draw_text( + scene=annotation, text=text, text_anchor=annotation_center, text_scale=text_scale, text_thickness=text_thickness, text_padding=text_padding, - ) - draw_text( - scene=annotation, text_color=text_color, background_color=text_box_color if text_box_show else None, - **text_args, ) draw_text( scene=annotation_alpha, + text=text, + text_anchor=annotation_center, + text_scale=text_scale, + text_thickness=text_thickness, + text_padding=text_padding, text_color=Color.WHITE, background_color=Color.WHITE if text_box_show else None, - **text_args, ) - annotation = np.dstack((annotation, annotation_alpha)) + annotation = np.dstack((annotation, annotation_alpha)).astype(np.uint8) # Make sure text is displayed upright if 90 < line_angle_degrees % 360 < 270: @@ -705,9 +709,11 @@ def _make_label_image( rotation_matrix = cv2.getRotationMatrix2D( annotation_center.as_xy_float_tuple(), rotation_angle, scale=1 ) - annotation = cv2.warpAffine(annotation, rotation_matrix, annotation_shape) + annotation = cv2.warpAffine( + annotation, rotation_matrix, annotation_shape + ).astype(np.uint8) - return cast(npt.NDArray[np.uint8], annotation) + return annotation class LineZoneAnnotatorMulticlass: @@ -728,7 +734,7 @@ def __init__( text_scale: float = 0.75, text_thickness: int = 1, force_draw_class_ids: bool = False, - ): + ) -> None: """ Draw a table showing how many items of each class crossed each line. diff --git a/src/supervision/detection/tools/inference_slicer.py b/src/supervision/detection/tools/inference_slicer.py index c26a7584ab..897c7c85fa 100644 --- a/src/supervision/detection/tools/inference_slicer.py +++ b/src/supervision/detection/tools/inference_slicer.py @@ -4,10 +4,11 @@ import warnings from collections.abc import Callable from concurrent.futures import ThreadPoolExecutor, as_completed -from typing import Any +from typing import TypeAlias, cast import numpy as np import numpy.typing as npt +from PIL import Image as PILImage from supervision.config import ORIENTED_BOX_COORDINATES from supervision.detection.compact_mask import CompactMask @@ -19,10 +20,18 @@ from supervision.utils.image import crop_image, get_image_resolution_wh from supervision.utils.internal import SupervisionWarnings +# Concrete union for callback storage: the callback may receive either an +# 8-bit ndarray or a PIL image. ``ImageType`` is a constrained ``TypeVar`` +# (see ``supervision.draw.base``) and cannot be stored on a non-generic class +# as an attribute annotation — using the underlying union keeps the public +# API contract intact while satisfying static type checking. +_TypeSliceCallbackInput: TypeAlias = npt.NDArray[np.uint8] | PILImage.Image +_TypeSliceCallback = Callable[[_TypeSliceCallbackInput], Detections] + def move_detections( detections: Detections, - offset: npt.NDArray[Any], + offset: npt.NDArray[np.int_], resolution_wh: tuple[int, int] | None = None, ) -> Detections: """ @@ -36,10 +45,16 @@ def move_detections( Returns: Repositioned Detections object. """ - detections.xyxy = move_boxes(xyxy=detections.xyxy, offset=offset) + detections.xyxy = move_boxes( + xyxy=cast(npt.NDArray[np.float32], detections.xyxy), offset=offset + ) if ORIENTED_BOX_COORDINATES in detections.data: + oriented_boxes = cast( + npt.NDArray[np.float32], + detections.data[ORIENTED_BOX_COORDINATES], + ) detections.data[ORIENTED_BOX_COORDINATES] = move_oriented_boxes( - xyxyxyxy=detections.data[ORIENTED_BOX_COORDINATES], offset=offset + xyxyxyxy=oriented_boxes, offset=offset ) if detections.mask is not None: if resolution_wh is None: @@ -56,7 +71,9 @@ def move_detections( ) else: detections.mask = move_masks( - masks=detections.mask, offset=offset, resolution_wh=resolution_wh + masks=detections.mask, + offset=offset, + resolution_wh=resolution_wh, ) return detections @@ -142,7 +159,7 @@ def callback(tile): def __init__( self, - callback: Callable[[ImageType], Detections], + callback: _TypeSliceCallback, slice_wh: int | tuple[int, int] = 640, overlap_wh: int | tuple[int, int] = 100, overlap_filter: OverlapFilter | str = OverlapFilter.NON_MAX_SUPPRESSION, @@ -150,7 +167,7 @@ def __init__( overlap_metric: OverlapMetric | str = OverlapMetric.IOU, thread_workers: int = 1, compact_masks: bool = False, - ): + ) -> None: slice_wh_norm = self._normalize_slice_wh(slice_wh) overlap_wh_norm = self._normalize_overlap_wh(overlap_wh) @@ -167,7 +184,7 @@ def __init__( self.iou_threshold = iou_threshold self.overlap_metric = OverlapMetric.from_value(overlap_metric) self.overlap_filter = OverlapFilter.from_value(overlap_filter) - self.callback: Callable[[ImageType], Detections] = callback + self.callback: _TypeSliceCallback = callback self.thread_workers = thread_workers self.compact_masks = compact_masks self._out_of_slice_bounds_warned: bool = False @@ -272,7 +289,9 @@ def __call__(self, image: ImageType) -> Detections: ) return merged - def _run_callback(self, image: ImageType, offset: npt.NDArray[Any]) -> Detections: + def _run_callback( + self, image: ImageType, offset: npt.NDArray[np.int_] + ) -> Detections: """ Run detection callback on a sliced portion of the image and adjust coordinates. @@ -285,7 +304,10 @@ def _run_callback(self, image: ImageType, offset: npt.NDArray[Any]) -> Detection Detections adjusted to the full image coordinate system. """ image_slice = crop_image(image=image, xyxy=offset) - detections = self.callback(image_slice) + # ``image_slice`` matches the input ``ImageType`` (TypeVar); the callback + # is typed against the underlying union for storage, so a narrowing cast + # is required to bridge the two at the call site. + detections = self.callback(cast(_TypeSliceCallbackInput, image_slice)) if ( self.compact_masks @@ -312,10 +334,11 @@ def _run_callback(self, image: ImageType, offset: npt.NDArray[Any]) -> Detection if not self._out_of_slice_bounds_warned and len(detections) > 0: slice_width = offset[2] - offset[0] slice_height = offset[3] - offset[1] - x_exceeds = np.any(detections.xyxy[:, [0, 2]] > slice_width) - y_exceeds = np.any(detections.xyxy[:, [1, 3]] > slice_height) - x_negative = np.any(detections.xyxy[:, [0, 2]] < 0) - y_negative = np.any(detections.xyxy[:, [1, 3]] < 0) + xyxy_num = detections.xyxy + x_exceeds = np.any(xyxy_num[:, [0, 2]] > slice_width) + y_exceeds = np.any(xyxy_num[:, [1, 3]] > slice_height) + x_negative = np.any(xyxy_num[:, [0, 2]] < 0) + y_negative = np.any(xyxy_num[:, [1, 3]] < 0) if x_exceeds or y_exceeds or x_negative or y_negative: self._out_of_slice_bounds_warned = True msg = ( @@ -390,7 +413,7 @@ def _generate_offset( resolution_wh: tuple[int, int], slice_wh: tuple[int, int], overlap_wh: tuple[int, int], - ) -> npt.NDArray[Any]: + ) -> npt.NDArray[np.int_]: """ Generate bounding boxes defining the coordinates of image slices with overlap. @@ -442,7 +465,7 @@ def _compute_axis_starts( x_max = np.clip(x_min + slice_width, 0, image_width) y_max = np.clip(y_min + slice_height, 0, image_height) - offsets: npt.NDArray[Any] = np.stack( + offsets: npt.NDArray[np.int_] = np.stack( [x_min, y_min, x_max, y_max], axis=-1, ).reshape(-1, 4) diff --git a/src/supervision/detection/tools/polygon_zone.py b/src/supervision/detection/tools/polygon_zone.py index 58e979f9b1..8556dfbc38 100644 --- a/src/supervision/detection/tools/polygon_zone.py +++ b/src/supervision/detection/tools/polygon_zone.py @@ -62,7 +62,7 @@ def __init__( self, polygon: npt.NDArray[np.int64], triggering_anchors: Iterable[Position] = (Position.BOTTOM_CENTER,), - ): + ) -> None: self.polygon = polygon.astype(int) self.triggering_anchors = triggering_anchors if not list(self.triggering_anchors): @@ -94,7 +94,7 @@ def trigger(self, detections: Detections) -> npt.NDArray[np.bool_]: """ if len(detections) == 0: self.current_count = 0 - return np.array([], dtype=bool) + return cast(npt.NDArray[np.bool_], np.array([], dtype=bool)) all_anchors = np.array( [ @@ -110,7 +110,7 @@ def trigger(self, detections: Detections) -> npt.NDArray[np.bool_]: y_safe = np.clip(y, 0, mask_h - 1) is_in_zone = np.all(in_bounds & self.mask[y_safe, x_safe], axis=0) self.current_count = int(np.sum(is_in_zone)) - return is_in_zone.astype(bool) + return cast(npt.NDArray[np.bool_], is_in_zone.astype(bool)) class PolygonZoneAnnotator: @@ -144,7 +144,7 @@ def __init__( text_padding: int = 10, display_in_zone_count: bool = True, opacity: float = 0, - ): + ) -> None: self.zone = zone self.color = color self.thickness = thickness diff --git a/src/supervision/detection/tools/smoother.py b/src/supervision/detection/tools/smoother.py index 52282f2de6..18d04c2542 100644 --- a/src/supervision/detection/tools/smoother.py +++ b/src/supervision/detection/tools/smoother.py @@ -3,7 +3,6 @@ import warnings from collections import defaultdict, deque from copy import deepcopy -from typing import cast import numpy as np @@ -115,7 +114,7 @@ def update_with_detections(self, detections: Detections) -> Detections: tracker_id_value = detections.tracker_id[detection_idx] tracker_id = int(tracker_id_value) - self.tracks[tracker_id].append(cast(Detections, detections[detection_idx])) + self.tracks[tracker_id].append(detections[detection_idx]) for track_id in self.tracks.keys(): if track_id not in detections.tracker_id: @@ -137,8 +136,16 @@ def get_track(self, track_id: int) -> Detections | None: return None ret = deepcopy(valid[0]) - ret.xyxy = np.mean([d.xyxy for d in valid], axis=0) - ret.confidence = np.mean([d.confidence for d in valid], axis=0) + xyxy_stack = np.stack([d.xyxy for d in valid], axis=0) + ret.xyxy = xyxy_stack.mean(axis=0) + + if all(d.confidence is not None for d in valid): + confidence_stack = np.stack( + [d.confidence for d in valid if d.confidence is not None], axis=0 + ) + ret.confidence = confidence_stack.mean(axis=0) + else: + ret.confidence = None return ret diff --git a/src/supervision/detection/utils/_typing.py b/src/supervision/detection/utils/_typing.py new file mode 100644 index 0000000000..8f7a4103ce --- /dev/null +++ b/src/supervision/detection/utils/_typing.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from typing import TypeAlias + +import numpy as np +import numpy.typing as npt + +DetectionDataValueType: TypeAlias = npt.NDArray[np.generic] | list[object] +DetectionDataType: TypeAlias = dict[str, DetectionDataValueType] +MetadataType: TypeAlias = dict[str, object] diff --git a/src/supervision/detection/utils/boxes.py b/src/supervision/detection/utils/boxes.py index a19b05d445..eefab11eca 100644 --- a/src/supervision/detection/utils/boxes.py +++ b/src/supervision/detection/utils/boxes.py @@ -42,7 +42,7 @@ def clip_boxes( ``` """ - result: npt.NDArray[np.number] = np.copy(xyxy) + result = np.copy(xyxy) width, height = resolution_wh result[:, [0, 2]] = result[:, [0, 2]].clip(0, width) result[:, [1, 3]] = result[:, [1, 3]].clip(0, height) @@ -90,8 +90,8 @@ def pad_boxes( py = px result = xyxy.copy() - result[:, [0, 1]] -= [px, py] - result[:, [2, 3]] += [px, py] + result[:, :2] = result[:, :2] - [px, py] + result[:, 2:] = result[:, 2:] + [px, py] return result @@ -125,7 +125,8 @@ def denormalize_boxes( Returns: Array of shape `(N, 4)` with absolute coordinates in - `(x_min, y_min, x_max, y_max)` format. + `(x_min, y_min, x_max, y_max)` format. Preserves the caller's + floating-point dtype; integer inputs are upcast to float64. Examples: ```pycon @@ -153,7 +154,7 @@ def denormalize_boxes( ``` """ width, height = resolution_wh - result = xyxy.copy() + result: npt.NDArray[np.number] = np.array(xyxy, copy=True) result[:, [0, 2]] = (result[:, [0, 2]] * width) / normalization_factor result[:, [1, 3]] = (result[:, [1, 3]] * height) / normalization_factor @@ -162,8 +163,8 @@ def denormalize_boxes( def move_boxes( - xyxy: npt.NDArray[np.float64], offset: npt.NDArray[np.int32] -) -> npt.NDArray[np.float64]: + xyxy: npt.NDArray[np.number], offset: npt.NDArray[np.integer] +) -> npt.NDArray[np.number]: """ Args: xyxy: An array of shape `(n, 4)` containing the @@ -193,8 +194,8 @@ def move_boxes( def move_oriented_boxes( - xyxyxyxy: npt.NDArray[np.float64], offset: npt.NDArray[np.int32] -) -> npt.NDArray[np.float64]: + xyxyxyxy: npt.NDArray[np.number], offset: npt.NDArray[np.integer] +) -> npt.NDArray[np.number]: """ Args: xyxyxyxy: An array of shape `(n, 4, 2)` containing the @@ -241,9 +242,7 @@ def move_oriented_boxes( return xyxyxyxy + offset -def scale_boxes( - xyxy: npt.NDArray[np.float64], factor: float -) -> npt.NDArray[np.float64]: +def scale_boxes(xyxy: npt.NDArray[np.number], factor: float) -> npt.NDArray[np.number]: """ Scale the dimensions of bounding boxes. @@ -322,7 +321,7 @@ def spread_out_boxes( # NxNx2 delta_centers = centers[:, np.newaxis, :] - centers[np.newaxis, :, :] - delta_centers *= overlap_mask[:, :, np.newaxis] + delta_centers = delta_centers * overlap_mask[:, :, np.newaxis] # Nx2 delta_sum = np.sum(delta_centers, axis=1) @@ -337,13 +336,13 @@ def spread_out_boxes( force_vectors = np.sum(iou, axis=1) force_vectors = force_vectors[:, np.newaxis] * direction_vectors - force_vectors *= 10 + force_vectors = force_vectors * 10 force_vectors[(force_vectors > 0) & (force_vectors < 2)] = 2 force_vectors[(force_vectors < 0) & (force_vectors > -2)] = -2 force_vectors = force_vectors.astype(int) - xyxy_padded[:, [0, 1]] += force_vectors - xyxy_padded[:, [2, 3]] += force_vectors + xyxy_padded[:, :2] = xyxy_padded[:, :2] + force_vectors + xyxy_padded[:, 2:] = xyxy_padded[:, 2:] + force_vectors return pad_boxes(xyxy_padded, px=-1) diff --git a/src/supervision/detection/utils/converters.py b/src/supervision/detection/utils/converters.py index 9626daf6dd..b40c9991a9 100644 --- a/src/supervision/detection/utils/converters.py +++ b/src/supervision/detection/utils/converters.py @@ -79,7 +79,7 @@ def xywh_to_xyxy(xywh: npt.NDArray[np.number]) -> npt.NDArray[np.number]: xyxy = xywh.copy() xyxy[:, 2] = xywh[:, 0] + xywh[:, 2] xyxy[:, 3] = xywh[:, 1] + xywh[:, 3] - return xyxy + return cast(npt.NDArray[np.number], np.asarray(xyxy)) def xyxy_to_xywh(xyxy: npt.NDArray[np.number]) -> npt.NDArray[np.number]: @@ -113,7 +113,7 @@ def xyxy_to_xywh(xyxy: npt.NDArray[np.number]) -> npt.NDArray[np.number]: xywh = xyxy.copy() xywh[:, 2] = xyxy[:, 2] - xyxy[:, 0] xywh[:, 3] = xyxy[:, 3] - xyxy[:, 1] - return xywh + return cast(npt.NDArray[np.number], np.asarray(xywh)) def xcycwh_to_xyxy(xcycwh: npt.NDArray[np.number]) -> npt.NDArray[np.number]: @@ -149,7 +149,7 @@ def xcycwh_to_xyxy(xcycwh: npt.NDArray[np.number]) -> npt.NDArray[np.number]: xyxy[:, 1] = xcycwh[:, 1] - xcycwh[:, 3] / 2 xyxy[:, 2] = xcycwh[:, 0] + xcycwh[:, 2] / 2 xyxy[:, 3] = xcycwh[:, 1] + xcycwh[:, 3] / 2 - return xyxy + return cast(npt.NDArray[np.number], np.asarray(xyxy)) def xyxy_to_xcycarh(xyxy: npt.NDArray[np.number]) -> npt.NDArray[np.floating]: diff --git a/src/supervision/detection/utils/internal.py b/src/supervision/detection/utils/internal.py index 1d0d3371fa..7e684a1c09 100644 --- a/src/supervision/detection/utils/internal.py +++ b/src/supervision/detection/utils/internal.py @@ -2,27 +2,92 @@ import logging from itertools import chain -from typing import Any, cast +from typing import Protocol, TypedDict, cast import cv2 import numpy as np import numpy.typing as npt from supervision.config import CLASS_NAME_DATA_FIELD +from supervision.detection.utils._typing import ( + DetectionDataType, + DetectionDataValueType, + MetadataType, +) from supervision.detection.utils.converters import polygon_to_mask, rle_to_mask from supervision.geometry.core import Vector logger = logging.getLogger(__name__) -def extract_ultralytics_masks(yolov8_results: Any) -> npt.NDArray[np.bool_] | None: - if not yolov8_results.masks: +class _TypeUltralyticsMaskData(Protocol): + shape: tuple[int, ...] + + def cpu(self) -> _TypeUltralyticsMaskData: + """Return the mask data on CPU.""" + + def numpy(self) -> npt.NDArray[np.generic]: + """Convert the mask data to a NumPy array.""" + + +class _TypeUltralyticsMasks(Protocol): + data: _TypeUltralyticsMaskData + + +class _TypeUltralyticsResult(Protocol): + masks: _TypeUltralyticsMasks | None + orig_shape: tuple[int, int] + + +class _TypeRoboflowImage(TypedDict): + width: int + height: int + + +class _TypeRoboflowPoint(TypedDict): + x: float + y: float + + +class _TypeRoboflowRLE(TypedDict): + size: tuple[int, int] | list[int] + counts: npt.NDArray[np.integer] | list[int] | str | bytes + + +_TypeRoboflowPrediction = TypedDict( + "_TypeRoboflowPrediction", + { + "x": float, + "y": float, + "width": float, + "height": float, + "class_id": int, + "class": str, + "confidence": float, + "tracker_id": int, + "rle": _TypeRoboflowRLE, + "rle_mask": _TypeRoboflowRLE, + "points": list[_TypeRoboflowPoint], + }, + total=False, +) + + +class _TypeRoboflowResult(TypedDict): + predictions: list[_TypeRoboflowPrediction] + image: _TypeRoboflowImage + + +def extract_ultralytics_masks( + yolov8_results: _TypeUltralyticsResult, +) -> npt.NDArray[np.bool_] | None: + if yolov8_results.masks is None: return None orig_shape = yolov8_results.orig_shape inference_shape = tuple(yolov8_results.masks.data.shape[1:]) - pad = (0, 0) + pad: tuple[float, float] = (0.0, 0.0) if inference_shape != orig_shape: gain = min( @@ -52,14 +117,14 @@ def extract_ultralytics_masks(yolov8_results: Any) -> npt.NDArray[np.bool_] | No def process_roboflow_result( - roboflow_result: dict[str, Any], + roboflow_result: _TypeRoboflowResult, ) -> tuple[ - npt.NDArray[np.floating], - npt.NDArray[np.floating], - npt.NDArray[np.integer], + npt.NDArray[np.float64], + npt.NDArray[np.float64], + npt.NDArray[np.int64], npt.NDArray[np.bool_] | None, - npt.NDArray[np.integer] | None, - dict[str, npt.NDArray[np.generic]], + npt.NDArray[np.int64] | None, + DetectionDataType, ]: if not roboflow_result["predictions"]: return ( @@ -133,36 +198,47 @@ def process_roboflow_result( polygon = np.array( [[point["x"], point["y"]] for point in prediction["points"]], dtype=int ) - mask = polygon_to_mask(polygon, resolution_wh=(image_width, image_height)) + polygon_mask = cast( + npt.NDArray[np.bool_], + polygon_to_mask( + polygon, resolution_wh=(image_width, image_height) + ).astype(bool), + ) xyxy.append([x_min, y_min, x_max, y_max]) class_id.append(prediction["class_id"]) class_name.append(prediction["class"]) confidence.append(prediction["confidence"]) - masks.append(mask.astype(bool)) + masks.append(polygon_mask) if "tracker_id" in prediction: tracker_ids.append(prediction["tracker_id"]) - xyxy_arr: npt.NDArray[np.floating] = ( - np.array(xyxy, dtype=np.float64) if len(xyxy) > 0 else np.empty((0, 4)) + xyxy_arr: npt.NDArray[np.float64] = ( + np.array(xyxy, dtype=np.float64) + if len(xyxy) > 0 + else np.empty((0, 4), dtype=np.float64) ) - confidence_arr: npt.NDArray[np.floating] = ( - np.array(confidence, dtype=np.float64) if len(confidence) > 0 else np.empty(0) + confidence_arr: npt.NDArray[np.float64] = ( + np.array(confidence, dtype=np.float64) + if len(confidence) > 0 + else np.empty(0, dtype=np.float64) ) - class_id_arr: npt.NDArray[np.integer] = ( + class_id_arr: npt.NDArray[np.int64] = ( np.array(class_id, dtype=np.int64) if len(class_id) > 0 else np.empty(0, dtype=np.int64) ) class_name_arr: npt.NDArray[np.str_] = ( - np.array(class_name) if len(class_name) > 0 else np.empty(0, dtype=str) + np.array(class_name) + if len(class_name) > 0 + else cast(npt.NDArray[np.str_], np.empty(0, dtype=str)) ) masks_arr: npt.NDArray[np.bool_] | None = ( - np.array(masks, dtype=bool) if len(masks) > 0 else None + cast(npt.NDArray[np.bool_], np.stack(masks, axis=0)) if len(masks) > 0 else None ) - tracker_id_arr: npt.NDArray[np.integer] | None = ( + tracker_id_arr: npt.NDArray[np.int64] | None = ( np.array(tracker_ids, dtype=np.int64) if len(tracker_ids) > 0 else None ) - data: dict[str, npt.NDArray[np.generic]] = {CLASS_NAME_DATA_FIELD: class_name_arr} + data: DetectionDataType = {CLASS_NAME_DATA_FIELD: class_name_arr} return ( xyxy_arr, @@ -175,8 +251,8 @@ def process_roboflow_result( def is_data_equal( - data_a: dict[str, npt.NDArray[np.generic]], - data_b: dict[str, npt.NDArray[np.generic]], + data_a: DetectionDataType, + data_b: DetectionDataType, ) -> bool: """ Compares the data payloads of two Detections instances. @@ -187,12 +263,25 @@ def is_data_equal( Returns: True if the data payloads are equal, False otherwise. """ - return set(data_a.keys()) == set(data_b.keys()) and all( - np.array_equal(data_a[key], data_b[key]) for key in data_a - ) + if set(data_a.keys()) != set(data_b.keys()): + return False + + for key in data_a: + value_a = data_a[key] + value_b = data_b[key] + + if isinstance(value_a, np.ndarray) or isinstance(value_b, np.ndarray): + array_a = cast(npt.NDArray[np.generic], np.asarray(value_a)) + array_b = cast(npt.NDArray[np.generic], np.asarray(value_b)) + if not np.array_equal(array_a, array_b): + return False + elif value_a != value_b: + return False + + return True -def is_metadata_equal(metadata_a: dict[str, Any], metadata_b: dict[str, Any]) -> bool: +def is_metadata_equal(metadata_a: MetadataType, metadata_b: MetadataType) -> bool: """ Compares the metadata payloads of two Detections instances. @@ -202,20 +291,25 @@ def is_metadata_equal(metadata_a: dict[str, Any], metadata_b: dict[str, Any]) -> Returns: True if the metadata payloads are equal, False otherwise. """ - return set(metadata_a.keys()) == set(metadata_b.keys()) and all( - np.array_equal(metadata_a[key], metadata_b[key]) - if ( - isinstance(metadata_a[key], np.ndarray) - and isinstance(metadata_b[key], np.ndarray) - ) - else metadata_a[key] == metadata_b[key] - for key in metadata_a - ) + if set(metadata_a.keys()) != set(metadata_b.keys()): + return False + + for key in metadata_a: + value_a = metadata_a[key] + value_b = metadata_b[key] + + if isinstance(value_a, np.ndarray) or isinstance(value_b, np.ndarray): + array_a = cast(npt.NDArray[np.generic], np.asarray(value_a)) + array_b = cast(npt.NDArray[np.generic], np.asarray(value_b)) + if not np.array_equal(array_a, array_b): + return False + elif value_a != value_b: + return False + return True -def merge_data( - data_list: list[dict[str, npt.NDArray[np.generic] | list[Any]]], -) -> dict[str, npt.NDArray[np.generic] | list[Any]]: + +def merge_data(data_list: list[DetectionDataType]) -> DetectionDataType: """ Merges the data payloads of a list of Detections instances. @@ -249,20 +343,25 @@ def merge_data( "All data values within a single object must have equal length." ) - merged_data: dict[str, list[Any]] = {key: [] for key in all_keys_sets[0]} + merged_data_values: dict[str, list[DetectionDataValueType]] = { + key: [] for key in all_keys_sets[0] + } for data in data_list: - for key in data: - merged_data[key].append(data[key]) - - for key in merged_data: - if all(isinstance(item, list) for item in merged_data[key]): - merged_data[key] = list(chain.from_iterable(merged_data[key])) - elif all(isinstance(item, np.ndarray) for item in merged_data[key]): - ndim = merged_data[key][0].ndim + for key, value in data.items(): + merged_data_values[key].append(value) + + merged_data: DetectionDataType = {} + for key, values in merged_data_values.items(): + if all(isinstance(item, list) for item in values): + list_values = cast(list[list[object]], values) + merged_data[key] = list(chain.from_iterable(list_values)) + elif all(isinstance(item, np.ndarray) for item in values): + array_values = cast(list[npt.NDArray[np.generic]], values) + ndim = array_values[0].ndim if ndim == 1: - merged_data[key] = np.hstack(merged_data[key]) + merged_data[key] = np.hstack(array_values) elif ndim > 1: - merged_data[key] = np.vstack(merged_data[key]) + merged_data[key] = np.vstack(array_values) else: raise ValueError(f"Unexpected array dimension for key '{key}'.") else: @@ -274,7 +373,7 @@ def merge_data( return merged_data -def merge_metadata(metadata_list: list[dict[str, Any]]) -> dict[str, Any]: +def merge_metadata(metadata_list: list[MetadataType]) -> MetadataType: """ Merge metadata from a list of metadata dictionaries. @@ -301,7 +400,7 @@ def merge_metadata(metadata_list: list[dict[str, Any]]) -> dict[str, Any]: if not all(keys_set == all_keys_sets[0] for keys_set in all_keys_sets): raise ValueError("All metadata dictionaries must have the same keys to merge.") - merged_metadata: dict[str, Any] = {} + merged_metadata: MetadataType = {} for metadata in metadata_list: for key, value in metadata.items(): if key not in merged_metadata: @@ -310,7 +409,9 @@ def merge_metadata(metadata_list: list[dict[str, Any]]) -> dict[str, Any]: other_value = merged_metadata[key] if isinstance(value, np.ndarray) and isinstance(other_value, np.ndarray): - if not np.array_equal(merged_metadata[key], value): + array_a = cast(npt.NDArray[np.generic], merged_metadata[key]) + array_b = cast(npt.NDArray[np.generic], value) + if not np.array_equal(array_a, array_b): raise ValueError( f"Conflicting metadata for key: '{key}': " "{type(value)}, {type(other_value)}." @@ -329,9 +430,14 @@ def merge_metadata(metadata_list: list[dict[str, Any]]) -> dict[str, Any]: def get_data_item( - data: dict[str, npt.NDArray[np.generic] | list[Any]], - index: int | slice | list[int] | npt.NDArray[np.integer | np.bool_], -) -> dict[str, npt.NDArray[np.generic] | list[Any]]: + data: DetectionDataType, + index: int + | slice + | list[int] + | list[bool] + | npt.NDArray[np.int_] + | npt.NDArray[np.bool_], +) -> DetectionDataType: """ Retrieve a subset of the data dictionary based on the given index. @@ -342,12 +448,14 @@ def get_data_item( Returns: A subset of the data dictionary corresponding to the specified index. """ - subset_data: dict[str, npt.NDArray[np.generic] | list[Any]] = {} + subset_data: DetectionDataType = {} for key, value in data.items(): if isinstance(value, np.ndarray): subset_data[key] = value[index] elif isinstance(value, list): - if isinstance(index, slice): + if isinstance(index, int): + subset_data[key] = [value[index]] + elif isinstance(index, slice): subset_data[key] = value[index] elif isinstance(index, list): subset_data[key] = [value[i] for i in index] @@ -358,8 +466,6 @@ def get_data_item( ] else: subset_data[key] = [value[i] for i in index] - elif isinstance(index, int): - subset_data[key] = [value[index]] else: raise TypeError(f"Unsupported index type: {type(index)}") else: diff --git a/src/supervision/detection/utils/iou_and_nms.py b/src/supervision/detection/utils/iou_and_nms.py index c2aa230f06..ea8b52b519 100644 --- a/src/supervision/detection/utils/iou_and_nms.py +++ b/src/supervision/detection/utils/iou_and_nms.py @@ -1,7 +1,7 @@ from __future__ import annotations from enum import Enum -from typing import Any, cast +from typing import cast import numpy as np import numpy.typing as npt @@ -359,7 +359,8 @@ def box_iou_batch_with_jaccard( def oriented_box_iou_batch( - boxes_true: npt.NDArray[np.number], boxes_detection: npt.NDArray[np.number] + boxes_true: npt.NDArray[np.number], + boxes_detection: npt.NDArray[np.number], ) -> npt.NDArray[np.floating]: """ Compute Intersection over Union (IoU) of two sets of oriented bounding boxes - @@ -385,27 +386,29 @@ def oriented_box_iou_batch( # adding 1 because we are 0-indexed max_width = int(max(boxes_true[:, :, 1].max(), boxes_detection[:, :, 1].max()) + 1) - mask_true = np.zeros((boxes_true.shape[0], max_height, max_width), dtype=np.uint8) + mask_true = np.zeros((boxes_true.shape[0], max_height, max_width), dtype=bool) for box_idx, box_true in enumerate(boxes_true): - mask_true[box_idx] = polygon_to_mask(box_true, (max_width, max_height)) + mask_true[box_idx] = polygon_to_mask(box_true, (max_width, max_height)).astype( + bool + ) mask_detection = np.zeros( - (boxes_detection.shape[0], max_height, max_width), dtype=np.uint8 + (boxes_detection.shape[0], max_height, max_width), dtype=bool ) for box_idx, box_detection in enumerate(boxes_detection): mask_detection[box_idx] = polygon_to_mask( box_detection, (max_width, max_height) - ) + ).astype(bool) ious = mask_iou_batch(mask_true, mask_detection) return ious def compact_mask_iou_batch( - masks_true: Any, - masks_detection: Any, + masks_true: CompactMask, + masks_detection: CompactMask, overlap_metric: OverlapMetric = OverlapMetric.IOU, -) -> npt.NDArray[np.floating]: +) -> npt.NDArray[np.float64]: """Compute pairwise overlap between two :class:`CompactMask` collections. Avoids materialising full ``(N, H, W)`` arrays by: @@ -433,7 +436,7 @@ def compact_mask_iou_batch( """ n1: int = len(masks_true) n2: int = len(masks_detection) - result: npt.NDArray[np.floating] = np.zeros((n1, n2), dtype=float) + result: npt.NDArray[np.float64] = np.zeros((n1, n2), dtype=float) if n1 == 0 or n2 == 0: return result @@ -505,10 +508,10 @@ def compact_mask_iou_batch( def _mask_iou_batch_split( - masks_true: npt.NDArray[Any], - masks_detection: npt.NDArray[Any], + masks_true: npt.NDArray[np.bool_], + masks_detection: npt.NDArray[np.bool_], overlap_metric: OverlapMetric = OverlapMetric.IOU, -) -> npt.NDArray[np.floating]: +) -> npt.NDArray[np.float64]: """ Internal function. Compute Intersection over Union (IoU) of two sets of masks - @@ -554,15 +557,15 @@ def _mask_iou_batch_split( ) ious = np.nan_to_num(ious) - return cast(npt.NDArray[np.floating], ious) + return cast(npt.NDArray[np.float64], ious) def mask_iou_batch( - masks_true: npt.NDArray[Any], - masks_detection: npt.NDArray[Any], + masks_true: npt.NDArray[np.bool_] | CompactMask, + masks_detection: npt.NDArray[np.bool_] | CompactMask, overlap_metric: OverlapMetric = OverlapMetric.IOU, memory_limit: int = 1024 * 5, -) -> npt.NDArray[np.floating]: +) -> npt.NDArray[np.float64]: """ Compute Intersection over Union (IoU) of two sets of masks - `masks_true` and `masks_detection`. @@ -626,12 +629,12 @@ def mask_iou_batch( ) ) - return cast(npt.NDArray[np.floating], np.vstack(ious)) + return cast(npt.NDArray[np.float64], np.vstack(ious)) def mask_non_max_suppression( predictions: npt.NDArray[np.floating], - masks: npt.NDArray[Any], + masks: npt.NDArray[np.bool_] | CompactMask, iou_threshold: float = 0.5, overlap_metric: OverlapMetric = OverlapMetric.IOU, mask_dimension: int = 640, @@ -738,7 +741,7 @@ def box_non_max_suppression( boxes = predictions[:, :4] categories = predictions[:, 5] ious = box_iou_batch(boxes, boxes, overlap_metric) - ious = ious - np.eye(rows) + ious = ious - np.eye(rows, dtype=ious.dtype) keep = np.ones(rows, dtype=bool) @@ -755,8 +758,8 @@ def box_non_max_suppression( def _group_overlapping_masks( - predictions: npt.NDArray[np.float64], - masks: npt.NDArray[np.float64], + predictions: npt.NDArray[np.floating], + masks: npt.NDArray[np.bool_], iou_threshold: float = 0.5, overlap_metric: OverlapMetric = OverlapMetric.IOU, ) -> list[list[int]]: @@ -813,7 +816,7 @@ def _group_overlapping_masks( def mask_non_max_merge( predictions: npt.NDArray[np.floating], - masks: npt.NDArray[Any], + masks: npt.NDArray[np.bool_] | CompactMask, iou_threshold: float = 0.5, mask_dimension: int = 640, overlap_metric: OverlapMetric = OverlapMetric.IOU, diff --git a/src/supervision/detection/utils/masks.py b/src/supervision/detection/utils/masks.py index e8dd43227d..f6dc42e1cf 100644 --- a/src/supervision/detection/utils/masks.py +++ b/src/supervision/detection/utils/masks.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Literal, cast +from typing import Literal, cast import cv2 import numpy as np @@ -11,7 +11,7 @@ def move_masks( masks: npt.NDArray[np.bool_], - offset: npt.NDArray[np.int32], + offset: npt.NDArray[np.integer], resolution_wh: tuple[int, int], ) -> npt.NDArray[np.bool_]: """ @@ -88,7 +88,7 @@ def move_masks( def calculate_masks_centroids( - masks: npt.NDArray[Any] | CompactMask, + masks: npt.NDArray[np.bool_] | CompactMask, ) -> npt.NDArray[np.int_]: """ Calculate the centroids of binary masks in a tensor. @@ -260,7 +260,9 @@ def contains_multiple_segments( return bool(number_of_labels > 2) -def resize_masks(masks: npt.NDArray[Any], max_dimension: int = 640) -> npt.NDArray[Any]: +def resize_masks( + masks: npt.NDArray[np.bool_], max_dimension: int = 640 +) -> npt.NDArray[np.bool_]: """ Resize all masks in the array to have a maximum dimension of max_dimension, maintaining aspect ratio. @@ -376,14 +378,12 @@ def filter_segments_by_distance( if not np.any(mask): return mask.copy() - image = mask.astype(np.uint8) - num_labels: int - labels: npt.NDArray[np.int32] - stats: npt.NDArray[np.int32] - centroids: npt.NDArray[np.float64] - num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats( - image, connectivity=connectivity - ) + image = cast(npt.NDArray[np.uint8], mask.astype(np.uint8)) + components = cv2.connectedComponentsWithStats(image, connectivity=connectivity) + num_labels = int(components[0]) + labels = cast(npt.NDArray[np.int32], components[1]) + stats = cast(npt.NDArray[np.int32], components[2]) + centroids = cast(npt.NDArray[np.float64], components[3]) if num_labels <= 1: return mask.copy() diff --git a/src/supervision/detection/utils/polygons.py b/src/supervision/detection/utils/polygons.py index 041e43f614..6d2ccf151e 100644 --- a/src/supervision/detection/utils/polygons.py +++ b/src/supervision/detection/utils/polygons.py @@ -34,7 +34,9 @@ def filter_polygons_by_area( """ if min_area is None and max_area is None: return polygons - ares = [cv2.contourArea(polygon) for polygon in polygons] + ares = [ + cv2.contourArea(np.asarray(polygon, dtype=np.float32)) for polygon in polygons + ] return [ polygon for polygon, area in zip(polygons, ares) @@ -71,16 +73,20 @@ def approximate_polygon( if percentage < 0 or percentage >= 1: raise ValueError("Percentage must be in the range [0, 1).") - target_points = max(int(len(polygon) * (1 - percentage)), 3) + polygon_f32 = np.asarray(polygon, dtype=np.float32) + target_points = max(int(len(polygon_f32) * (1 - percentage)), 3) - if len(polygon) <= target_points: - return polygon + if len(polygon_f32) <= target_points: + return cast(npt.NDArray[np.number], polygon_f32) epsilon: float = 0 - approximated_points = polygon + approximated_points: npt.NDArray[np.float32] = polygon_f32 while True: epsilon += epsilon_step - new_approximated_points = cv2.approxPolyDP(polygon, epsilon, closed=True) + new_approximated_points = cast( + npt.NDArray[np.float32], + cv2.approxPolyDP(polygon_f32, epsilon, closed=True), + ) if len(new_approximated_points) > target_points: approximated_points = new_approximated_points else: diff --git a/src/supervision/detection/vlm.py b/src/supervision/detection/vlm.py index de53548d04..6bc055ad9c 100644 --- a/src/supervision/detection/vlm.py +++ b/src/supervision/detection/vlm.py @@ -6,7 +6,7 @@ import json import re from enum import Enum -from typing import Any, cast +from typing import cast import numpy as np import numpy.typing as npt @@ -159,7 +159,9 @@ def from_value(cls, value: VLM | str) -> VLM: ] -def validate_vlm_parameters(vlm: VLM | str, result: Any, kwargs: dict[str, Any]) -> VLM: +def validate_vlm_parameters( + vlm: VLM | str, result: object, kwargs: dict[str, object] +) -> VLM: """ Validates the parameters and result type for a given Vision-Language Model (VLM). @@ -202,7 +204,7 @@ def validate_vlm_parameters(vlm: VLM | str, result: Any, kwargs: dict[str, Any]) def from_paligemma( result: str, resolution_wh: tuple[int, int], classes: list[str] | None = None -) -> tuple[npt.NDArray[Any], npt.NDArray[Any], npt.NDArray[Any]]: +) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.int64] | None, npt.NDArray[np.str_]]: """ Parse bounding boxes from paligemma-formatted text, scale them to the specified resolution, and optionally filter by classes. @@ -226,26 +228,43 @@ def from_paligemma( r"(?) ([\w\s\-]+)" ) matches = pattern.findall(result) - matches = np.array(matches) if matches else np.empty((0, 5)) + matches_arr = ( + np.array(matches, dtype=object) if matches else np.empty((0, 5), dtype=object) + ) - if matches.shape[0] == 0: - return np.empty((0, 4)), np.empty((0,), dtype=int), np.empty(0, dtype=str) + if matches_arr.shape[0] == 0: + return ( + np.empty((0, 4), dtype=np.float32), + np.empty((0,), dtype=np.int64), + np.empty(0, dtype=str), + ) - xyxy, class_name = matches[:, [1, 0, 3, 2]], matches[:, 4] - xyxy = xyxy.astype(int) / 1024 * np.array([w, h, w, h]) - class_name = np.char.strip(class_name.astype(str)) - class_id = None + xyxy_arr: npt.NDArray[np.float32] = np.asarray( + matches_arr[:, [1, 0, 3, 2]].astype(np.float32) + / np.float32(1024) + * np.array([w, h, w, h], dtype=np.float32), + dtype=np.float32, + ) + class_name_arr: npt.NDArray[np.str_] = np.asarray( + np.char.strip(matches_arr[:, 4].astype(str)), + dtype=str, + ) + class_id_arr: npt.NDArray[np.int64] | None = None if classes is not None: - mask = np.array([name in classes for name in class_name], dtype=bool) - xyxy = xyxy[mask] - class_name = class_name[mask] - class_id = np.array([classes.index(name) for name in class_name]) + mask_arr = np.array([name in classes for name in class_name_arr], dtype=bool) + xyxy_arr = xyxy_arr[mask_arr] + class_name_arr = class_name_arr[mask_arr] + class_id_arr = np.array( + [classes.index(name) for name in class_name_arr], dtype=np.int64 + ) - return xyxy, class_id, class_name + return xyxy_arr, class_id_arr, class_name_arr -def recover_truncated_qwen_2_5_vl_response(text: str) -> Any | None: +def recover_truncated_qwen_2_5_vl_response( + text: str, +) -> list[dict[str, object]] | None: """ Attempt to recover and parse a truncated or malformed JSON snippet from Qwen-2.5-VL output. @@ -285,7 +304,10 @@ def recover_truncated_qwen_2_5_vl_response(text: str) -> Any | None: repaired = prefix + body + "]" - return json.loads(repaired) + recovered = json.loads(repaired) + if isinstance(recovered, list): + return cast(list[dict[str, object]], recovered) + return None except Exception: return None @@ -295,7 +317,7 @@ def from_qwen_2_5_vl( input_wh: tuple[int, int], resolution_wh: tuple[int, int], classes: list[str] | None = None, -) -> tuple[npt.NDArray[Any], npt.NDArray[Any] | None, npt.NDArray[Any]]: +) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.int64] | None, npt.NDArray[np.str_]]: """ Parse and rescale bounding boxes and class labels from Qwen-2.5-VL JSON output. @@ -334,6 +356,7 @@ def from_qwen_2_5_vl( if start != -1 and end != -1 and end > start: text = text[start : end + 1].strip() + data: object try: data = json.loads(text) except json.JSONDecodeError: @@ -346,24 +369,33 @@ def from_qwen_2_5_vl( except (ValueError, SyntaxError, TypeError): return ( np.empty((0, 4)), - np.empty((0,), dtype=int), + np.empty((0,), dtype=np.int64), np.empty((0,), dtype=str), ) if not isinstance(data, list): - return (np.empty((0, 4)), np.empty((0,), dtype=int), np.empty((0,), dtype=str)) + return ( + np.empty((0, 4)), + np.empty((0,), dtype=np.int64), + np.empty((0,), dtype=str), + ) - boxes_list = [] - labels_list = [] + data = cast(list[dict[str, object]], data) + boxes_list: list[list[float]] = [] + labels_list: list[str] = [] for item in data: if "bbox_2d" not in item or "label" not in item: continue - boxes_list.append(item["bbox_2d"]) - labels_list.append(item["label"]) + boxes_list.append(cast(list[float], item["bbox_2d"])) + labels_list.append(cast(str, item["label"])) if not boxes_list: - return (np.empty((0, 4)), np.empty((0,), dtype=int), np.empty((0,), dtype=str)) + return ( + np.empty((0, 4)), + np.empty((0,), dtype=np.int64), + np.empty((0,), dtype=str), + ) xyxy = np.array(boxes_list, dtype=float) class_name = np.array(labels_list, dtype=str) @@ -377,7 +409,9 @@ def from_qwen_2_5_vl( mask = np.array([label in classes for label in class_name], dtype=bool) xyxy = xyxy[mask] class_name = class_name[mask] - class_id = np.array([classes.index(label) for label in class_name], dtype=int) + class_id = np.array( + [classes.index(label) for label in class_name], dtype=np.int64 + ) return xyxy, class_id, class_name @@ -386,7 +420,7 @@ def from_qwen_3_vl( result: str, resolution_wh: tuple[int, int], classes: list[str] | None = None, -) -> tuple[npt.NDArray[Any], npt.NDArray[Any] | None, npt.NDArray[Any]]: +) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.int64] | None, npt.NDArray[np.str_]]: """ Parse and scale bounding boxes from Qwen-3-VL style JSON output. @@ -411,7 +445,7 @@ def from_qwen_3_vl( def from_deepseek_vl_2( result: str, resolution_wh: tuple[int, int], classes: list[str] | None = None -) -> tuple[npt.NDArray[Any], npt.NDArray[Any] | None, npt.NDArray[Any]]: +) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.int64] | None, npt.NDArray[np.str_]]: """ Parse bounding boxes from deepseek-vl2-formatted text, scale them to the specified resolution, and optionally filter by classes. @@ -448,12 +482,13 @@ def from_deepseek_vl_2( f"and det tags ({len(detection_segments)}) in the result must be equal." ) - xyxy, class_name_list = [], [] + xyxy_list: list[list[float]] = [] + class_name_list: list[str] = [] for label, detection_blob in zip(label_segments, detection_segments): current_class_name = label.strip() for box in re.findall(r"\[(.*?)\]", detection_blob): x1, y1, x2, y2 = map(float, box.strip("[]").split(",")) - xyxy.append( + xyxy_list.append( [ (x1 / 999 * width), (y1 / 999 * height), @@ -463,29 +498,33 @@ def from_deepseek_vl_2( ) class_name_list.append(current_class_name) - xyxy = np.array(xyxy, dtype=np.float32) - class_name = np.array(class_name_list) + xyxy_arr = np.array(xyxy_list, dtype=np.float32) + class_name_arr = np.array(class_name_list, dtype=str) if classes is not None: - mask = np.array([name in classes for name in class_name], dtype=bool) - xyxy = xyxy[mask] - class_name = class_name[mask] - class_id = np.array([classes.index(name) for name in class_name]) + mask_arr = np.array([name in classes for name in class_name_arr], dtype=bool) + xyxy_arr = xyxy_arr[mask_arr] + class_name_arr = class_name_arr[mask_arr] + class_id_arr = np.array( + [classes.index(name) for name in class_name_arr], dtype=np.int64 + ) else: - unique_classes = sorted(list(set(class_name))) + unique_classes = sorted(list(set(class_name_arr))) class_to_id = {name: i for i, name in enumerate(unique_classes)} - class_id = np.array([class_to_id[name] for name in class_name]) + class_id_arr = np.array( + [class_to_id[name] for name in class_name_arr], dtype=np.int64 + ) - return xyxy, class_id, class_name + return xyxy_arr, class_id_arr, class_name_arr def from_florence_2( - result: dict[str, Any], resolution_wh: tuple[int, int] + result: dict[str, object], resolution_wh: tuple[int, int] ) -> tuple[ - npt.NDArray[Any], - npt.NDArray[Any] | None, - npt.NDArray[Any] | None, - npt.NDArray[Any] | None, + npt.NDArray[np.float32], + npt.NDArray[np.str_] | None, + npt.NDArray[np.bool_] | None, + npt.NDArray[np.float32] | None, ]: """ Parse results from the Florence 2 multi-model model. @@ -509,66 +548,94 @@ def from_florence_2( raise ValueError( f"{task} not supported. Supported tasks are: {SUPPORTED_TASKS_FLORENCE_2}" ) - result = result[task] + task_result = result[task] if task in ["", "", ""]: - xyxy = np.array(result["bboxes"], dtype=np.float32) - labels = np.array(result["labels"]) + task_result = cast(dict[str, object], task_result) + xyxy = np.array( + cast(list[list[float]], task_result["bboxes"]), dtype=np.float32 + ) + labels = np.array(cast(list[str], task_result["labels"]), dtype=str) return xyxy, labels, None, None if task == "": - xyxy = np.array(result["bboxes"], dtype=np.float32) + task_result = cast(dict[str, object], task_result) + xyxy = np.array( + cast(list[list[float]], task_result["bboxes"]), dtype=np.float32 + ) # provides labels, but they are ["", "", "", ...] return xyxy, None, None, None if task == "": - xyxyxyxy = np.array(result["quad_boxes"], dtype=np.float32) + task_result = cast(dict[str, object], task_result) + xyxyxyxy = np.array( + cast(list[list[float]], task_result["quad_boxes"]), dtype=np.float32 + ) xyxyxyxy = xyxyxyxy.reshape(-1, 4, 2) - xyxy = np.array([polygon_to_xyxy(polygon) for polygon in xyxyxyxy]) - labels = np.array(result["labels"]) + xyxy = np.array( + [polygon_to_xyxy(polygon) for polygon in xyxyxyxy], dtype=np.float32 + ) + labels = np.array(cast(list[str], task_result["labels"]), dtype=str) return xyxy, labels, None, xyxyxyxy if task in ["", ""]: - xyxy_list = [] - masks_list = [] - for polygons_of_same_class in result["polygons"]: + task_result = cast(dict[str, object], task_result) + xyxy_list: list[npt.NDArray[np.float32]] = [] + masks_list: list[npt.NDArray[np.bool_]] = [] + for polygons_of_same_class in cast( + list[list[list[float]]], task_result["polygons"] + ): for polygon in polygons_of_same_class: - polygon = np.reshape(polygon, (-1, 2)).astype(np.int32) - mask = polygon_to_mask(polygon, resolution_wh).astype(bool) + polygon_arr = np.reshape(polygon, (-1, 2)).astype(np.int32) + mask = cast( + npt.NDArray[np.bool_], + polygon_to_mask(polygon_arr, resolution_wh).astype(bool), + ) masks_list.append(mask) - xyxy = polygon_to_xyxy(polygon) - xyxy_list.append(xyxy) + xyxy_box = cast( + npt.NDArray[np.float32], + np.asarray(polygon_to_xyxy(polygon_arr), dtype=np.float32), + ) + xyxy_list.append(xyxy_box) # per-class labels also provided, but they are ["", "", "", ...] # when we figure out how to set class names, we can do # zip(result["labels"], result["polygons"]) xyxy = np.array(xyxy_list, dtype=np.float32) - masks = np.array(masks_list) + masks = np.stack(masks_list, axis=0) return xyxy, None, masks, None if task == "": - xyxy = np.array(result["bboxes"], dtype=np.float32) - labels = np.array(result["bboxes_labels"]) + task_result = cast(dict[str, object], task_result) + xyxy = np.array( + cast(list[list[float]], task_result["bboxes"]), dtype=np.float32 + ) + labels = np.array(cast(list[str], task_result["bboxes_labels"]), dtype=str) # Also has "polygons" and "polygons_labels", but they don't seem to be used return xyxy, labels, None, None if task in ["", ""]: - assert isinstance(result, str), ( - f"Expected string as result, got {type(result)}" + assert isinstance(task_result, str), ( + f"Expected string as result, got {type(task_result)}" ) - if result == "No object detected.": - return np.empty((0, 4), dtype=np.float32), np.array([]), None, None + if task_result == "No object detected.": + return ( + np.empty((0, 4), dtype=np.float32), + np.empty(0, dtype=str), + None, + None, + ) pattern = re.compile(r"") - match = pattern.search(result) + match = pattern.search(task_result) assert match is not None, ( - f"Expected string to end in location tags, but got {result}" + f"Expected string to end in location tags, but got {task_result}" ) w, h = validate_resolution(resolution_wh) xyxy = np.array([match.groups()], dtype=np.float32) xyxy *= np.array([w, h, w, h]) / 1000 - result_string = result[: match.start()] + result_string = task_result[: match.start()] labels = np.array([result_string]) return xyxy, labels, None, None @@ -579,7 +646,7 @@ def from_google_gemini_2_0( result: str, resolution_wh: tuple[int, int], classes: list[str] | None = None, -) -> tuple[npt.NDArray[Any], npt.NDArray[Any] | None, npt.NDArray[Any]]: +) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.int64] | None, npt.NDArray[np.str_]]: """ Parse and scale bounding boxes from Google Gemini style [JSON output](https://ai.google.dev/gemini-api/docs/vision?lang=python). @@ -626,37 +693,57 @@ def from_google_gemini_2_0( try: data = json.loads(result) except json.JSONDecodeError: - return np.empty((0, 4)), np.empty((0,), dtype=int), np.empty((0,), dtype=str) + return ( + np.empty((0, 4), dtype=np.float64), + np.empty((0,), dtype=np.int64), + np.empty((0,), dtype=str), + ) + if not isinstance(data, list): + return ( + np.empty((0, 4), dtype=np.float64), + np.empty((0,), dtype=np.int64), + np.empty((0,), dtype=str), + ) - labels = [] - xyxy = [] + labels: list[str] = [] + xyxy_list: list[list[float]] = [] for item in data: - if "box_2d" not in item or "label" not in item: + if not isinstance(item, dict): continue - labels.append(item["label"]) - box = item["box_2d"] + label = item.get("label") + box_2d = item.get("box_2d") + if not isinstance(label, str) or not isinstance(box_2d, list): + continue + labels.append(label) + box = [float(value) for value in box_2d] # Gemini bbox order is [y_min, x_min, y_max, x_max] - xyxy.append([box[1], box[0], box[3], box[2]]) + xyxy_list.append([box[1], box[0], box[3], box[2]]) - if len(xyxy) == 0: - return np.empty((0, 4)), np.empty((0,), dtype=int), np.empty((0,), dtype=str) + if len(xyxy_list) == 0: + return ( + np.empty((0, 4), dtype=np.float64), + np.empty((0,), dtype=np.int64), + np.empty((0,), dtype=str), + ) - xyxy = denormalize_boxes( - np.array(xyxy, dtype=np.float64), + xyxy_arr: npt.NDArray[np.float64] = denormalize_boxes( + np.array(xyxy_list, dtype=np.float64), resolution_wh=(w, h), normalization_factor=1000, ) - class_name = np.array(labels) - class_id = None + class_name_arr: npt.NDArray[np.str_] = np.array(labels, dtype=str) + class_id_arr: npt.NDArray[np.int64] | None = None if classes is not None: - mask = np.array([name in classes for name in class_name], dtype=bool) - xyxy = xyxy[mask] - class_name = class_name[mask] - class_id = np.array([classes.index(name) for name in class_name]) + mask = np.array([name in classes for name in class_name_arr], dtype=bool) + xyxy_arr = xyxy_arr[mask] + class_name_arr = class_name_arr[mask] + class_id_arr = np.array( + [classes.index(name) for name in class_name_arr], dtype=np.int64 + ) - return xyxy, class_id, class_name + return xyxy_arr, class_id_arr, class_name_arr def from_google_gemini_2_5( @@ -664,11 +751,11 @@ def from_google_gemini_2_5( resolution_wh: tuple[int, int], classes: list[str] | None = None, ) -> tuple[ - npt.NDArray[Any], - npt.NDArray[Any] | None, - npt.NDArray[Any], - npt.NDArray[Any] | None, - npt.NDArray[Any] | None, + npt.NDArray[np.float64], + npt.NDArray[np.int64] | None, + npt.NDArray[np.str_], + npt.NDArray[np.float64] | None, + npt.NDArray[np.bool_] | None, ]: """ Parse and scale bounding boxes and masks from Google Gemini 2.5 style @@ -712,44 +799,45 @@ def from_google_gemini_2_5( break try: - data = json.loads(result) + data = cast(list[dict[str, object]], json.loads(result)) except json.JSONDecodeError: return ( - np.empty((0, 4)), - np.array([], dtype=int), + np.empty((0, 4), dtype=np.float64), + np.array([], dtype=np.int64), np.array([], dtype=str), - np.array([], dtype=float), + np.array([], dtype=np.float64), None, ) - boxes_list: list[Any] = [] + boxes_list: list[npt.NDArray[np.float64]] = [] labels_list: list[str] = [] confidence_list: list[float] | None = [] - masks_list: list[npt.NDArray[Any]] | None = [] + masks_list: list[npt.NDArray[np.bool_]] | None = [] for item in data: if "box_2d" not in item or "label" not in item: continue - labels_list.append(item["label"]) - box = item["box_2d"] + labels_list.append(cast(str, item["label"])) + box = cast(list[float], item["box_2d"]) # Gemini bbox order is [y_min, x_min, y_max, x_max] absolute_bbox = denormalize_boxes( np.array([[box[1], box[0], box[3], box[2]]]).astype(np.float64), resolution_wh=(w, h), normalization_factor=1000, )[0] + absolute_bbox = cast(npt.NDArray[np.float64], absolute_bbox) boxes_list.append(absolute_bbox) if "mask" in item: if masks_list is not None: - png_str = item["mask"] + png_str = cast(str, item["mask"]) if not png_str.startswith("data:image/png;base64,"): masks_list.append(np.zeros((h, w), dtype=bool)) continue - png_str = png_str.removeprefix("data:image/png;base64,") - png_str = base64.b64decode(png_str) - mask_img = Image.open(io.BytesIO(png_str)) + png_payload = png_str.removeprefix("data:image/png;base64,") + png_bytes = base64.b64decode(png_payload) + mask_img = Image.open(io.BytesIO(png_bytes)) y_min, y_max = int(absolute_bbox[1]), int(absolute_bbox[3]) x_min, x_max = int(absolute_bbox[0]), int(absolute_bbox[2]) @@ -758,11 +846,11 @@ def from_google_gemini_2_5( bbox_width = x_max - x_min if bbox_height > 0 and bbox_width > 0: - mask_img = mask_img.resize( + mask_img_resized = mask_img.resize( (bbox_width, bbox_height), resample=Image.Resampling.BILINEAR ) np_mask: npt.NDArray[np.bool_] = np.zeros((h, w), dtype=bool) - np_mask[y_min:y_max, x_min:x_max] = np.array(mask_img) > 0 + np_mask[y_min:y_max, x_min:x_max] = np.array(mask_img_resized) > 0 masks_list.append(np_mask) else: masks_list.append(np.zeros((h, w), dtype=bool)) @@ -771,28 +859,30 @@ def from_google_gemini_2_5( if "confidence" in item: if confidence_list is not None: - confidence_list.append(item["confidence"]) + confidence_list.append(cast(float, item["confidence"])) else: confidence_list = None if not boxes_list: return ( - np.empty((0, 4)), - np.array([], dtype=int), + np.empty((0, 4), dtype=np.float64), + np.array([], dtype=np.int64), np.array([], dtype=str), - np.array([], dtype=float), + np.array([], dtype=np.float64), None, ) - xyxy = np.array(boxes_list, dtype=float) - class_name = np.array(labels_list) - class_id: npt.NDArray[Any] + xyxy = np.array(boxes_list, dtype=np.float64) + class_name = np.array(labels_list, dtype=str) + class_id: npt.NDArray[np.int64] if classes is not None: mask = np.array([name in classes for name in class_name], dtype=bool) xyxy = xyxy[mask] class_name = class_name[mask] - class_id = np.array([classes.index(name) for name in class_name]) + class_id = np.array( + [classes.index(name) for name in class_name], dtype=np.int64 + ) if masks_list is not None: masks_list = [m for m, keep in zip(masks_list, mask) if keep] @@ -801,12 +891,14 @@ def from_google_gemini_2_5( else: unique_labels = sorted(list(set(class_name))) label_to_id = {label: i for i, label in enumerate(unique_labels)} - class_id = np.array([label_to_id[name] for name in class_name]) + class_id = np.array([label_to_id[name] for name in class_name], dtype=np.int64) confidence = ( - np.array(confidence_list, dtype=float) if confidence_list is not None else None + np.array(confidence_list, dtype=np.float64) + if confidence_list is not None + else None ) - masks = np.array(masks_list) if masks_list is not None else None + masks = np.array(masks_list, dtype=bool) if masks_list is not None else None return ( xyxy, @@ -818,9 +910,9 @@ def from_google_gemini_2_5( def from_moondream( - result: dict[str, Any], + result: dict[str, object], resolution_wh: tuple[int, int], -) -> npt.NDArray[Any]: +) -> npt.NDArray[np.float64]: """ Parse and scale bounding boxes from moondream JSON output. @@ -856,28 +948,27 @@ def from_moondream( ) if "objects" not in result or not isinstance(result["objects"], list): - return np.empty((0, 4), dtype=float) + return np.empty((0, 4), dtype=np.float64) - xyxy = [] + xyxy: list[list[float]] = [] + objects = cast(list[object], result["objects"]) - for item in result["objects"]: + for item in objects: + item = cast(dict[str, object], item) if not all(k in item for k in ["x_min", "y_min", "x_max", "y_max"]): continue - x_min = item["x_min"] - y_min = item["y_min"] - x_max = item["x_max"] - y_max = item["y_max"] + x_min = cast(float, item["x_min"]) + y_min = cast(float, item["y_min"]) + x_max = cast(float, item["x_max"]) + y_max = cast(float, item["y_max"]) xyxy.append([x_min, y_min, x_max, y_max]) if len(xyxy) == 0: - return cast(npt.NDArray[Any], np.empty((0, 4))) + return np.empty((0, 4), dtype=np.float64) return cast( - npt.NDArray[Any], - denormalize_boxes( - np.array(xyxy).astype(np.float64), - resolution_wh=(w, h), - ), + npt.NDArray[np.float64], + denormalize_boxes(np.array(xyxy, dtype=np.float64), resolution_wh=(w, h)), ) diff --git a/src/supervision/draw/utils.py b/src/supervision/draw/utils.py index 37ea37b61d..4d9fff0479 100644 --- a/src/supervision/draw/utils.py +++ b/src/supervision/draw/utils.py @@ -318,6 +318,7 @@ def draw_image( Raises: FileNotFoundError: If the image path does not exist. + OSError: If the image path exists but cannot be decoded. ValueError: For invalid opacity or rectangle dimensions. """ @@ -325,7 +326,12 @@ def draw_image( if isinstance(image, str): if not os.path.exists(image): raise FileNotFoundError(f"Image path ('{image}') does not exist.") - image = cv2.imread(image, cv2.IMREAD_UNCHANGED) + loaded_image = cv2.imread(image, cv2.IMREAD_UNCHANGED) + if loaded_image is None: + raise OSError(f"Could not decode image path ('{image}').") + image_np = cast(npt.NDArray[np.uint8], loaded_image) + else: + image_np = image # Validate opacity if not 0.0 <= opacity <= 1.0: @@ -345,12 +351,13 @@ def draw_image( raise ValueError("Invalid rectangle dimensions.") # Resize and isolate alpha channel - image = cv2.resize(image, (rect_width, rect_height)) - image = cast(npt.NDArray[np.uint8], image) + image_np = cast( + npt.NDArray[np.uint8], cv2.resize(image_np, (rect_width, rect_height)) + ) alpha_channel = ( - image[:, :, 3] - if image.shape[2] == 4 - else np.ones((rect_height, rect_width), dtype=image.dtype) * 255 + image_np[:, :, 3] + if image_np.shape[2] == 4 + else np.ones((rect_height, rect_width), dtype=image_np.dtype) * 255 ) alpha_scaled = cv2.convertScaleAbs(alpha_channel * opacity) @@ -359,7 +366,7 @@ def draw_image( alpha_float = alpha_scaled.astype(np.float32) / 255.0 blended_roi = cv2.convertScaleAbs( (1 - alpha_float[..., np.newaxis]) * scene_roi - + alpha_float[..., np.newaxis] * image[:, :, :3] + + alpha_float[..., np.newaxis] * image_np[:, :, :3] ) # Update the scene diff --git a/src/supervision/key_points/annotators.py b/src/supervision/key_points/annotators.py index d4dfb73013..7b1ba37e3a 100644 --- a/src/supervision/key_points/annotators.py +++ b/src/supervision/key_points/annotators.py @@ -2,7 +2,6 @@ from abc import ABC, abstractmethod from collections.abc import Sequence -from typing import Any import cv2 import numpy as np @@ -207,7 +206,7 @@ def __init__( text_padding: int = 10, border_radius: int = 0, smart_position: bool = False, - ): + ) -> None: """ Args: color: The color to use for each keypoint label. If a list is provided, @@ -331,7 +330,9 @@ def annotate( if skeletons_count == 0: return scene - anchors = key_points.xy.reshape(points_count * skeletons_count, 2).astype(int) + anchors: npt.NDArray[np.int_] = np.asarray( + key_points.xy.reshape(points_count * skeletons_count, 2), dtype=int + ) mask = np.all(anchors != 0, axis=1) if not np.any(mask): @@ -358,7 +359,7 @@ def annotate( text_colors = text_colors[mask] filtered_labels = processed_labels[mask] - xyxy = np.array( + xyxy: npt.NDArray[np.float32] = np.array( [ self.get_text_bounding_box( text=label, @@ -368,13 +369,16 @@ def annotate( center_coordinates=tuple(anchor), ) for anchor, label in zip(anchors, filtered_labels) - ] + ], + dtype=np.float32, ) - xyxy_padded = pad_boxes(xyxy=xyxy, px=self.text_padding) + xyxy_padded: npt.NDArray[np.float32] = pad_boxes( + xyxy=xyxy, px=self.text_padding + ).astype(np.float32) if self.smart_position: - xyxy_padded = spread_out_boxes(xyxy_padded) - xyxy = pad_boxes(xyxy=xyxy_padded, px=-self.text_padding) + xyxy_padded = spread_out_boxes(xyxy_padded).astype(np.float32) + xyxy = pad_boxes(xyxy=xyxy_padded, px=-self.text_padding).astype(np.float32) for text, color, text_color, box, box_padded in zip( filtered_labels, colors, text_colors, xyxy, xyxy_padded @@ -388,7 +392,7 @@ def annotate( cv2.putText( img=scene, text=text, - org=(box[0], box[3]), + org=(int(box[0]), int(box[3])), fontFace=font, fontScale=self.text_scale, color=text_color.as_bgr(), @@ -439,7 +443,7 @@ def preprocess_and_validate_colors( colors: Color | list[Color] | None, points_count: int, skeletons_count: int, - ) -> npt.NDArray[Any]: + ) -> npt.NDArray[np.object_]: if isinstance(colors, list) and len(colors) != points_count: raise ValueError( f"Number of colors ({len(colors)}) must match number of key points " diff --git a/src/supervision/key_points/core.py b/src/supervision/key_points/core.py index cd8190ae15..279fe16497 100644 --- a/src/supervision/key_points/core.py +++ b/src/supervision/key_points/core.py @@ -1,26 +1,114 @@ from __future__ import annotations -from collections.abc import Iterable, Iterator +from collections.abc import Iterable, Iterator, Sequence from dataclasses import dataclass, field -from typing import Any, Union, cast +from typing import Protocol, TypedDict, cast, overload import numpy as np import numpy.typing as npt from supervision.config import CLASS_NAME_DATA_FIELD from supervision.detection.core import Detections -from supervision.detection.utils.internal import get_data_item, is_data_equal +from supervision.detection.utils._typing import DetectionDataType +from supervision.detection.utils.internal import ( + get_data_item, + is_data_equal, +) from supervision.validators import validate_key_points_fields -Index1D = Union[ - int, - slice, - list[int], - list[bool], - npt.NDArray[np.int_], - npt.NDArray[np.bool_], -] -Index2D = tuple[Index1D, Index1D] + +class _TypeTensorLike(Protocol): + def cpu(self) -> _TypeTensorLike: + """Return the tensor on CPU.""" + + def numpy(self) -> npt.NDArray[np.generic]: + """Convert the tensor to a NumPy array.""" + + def numel(self) -> int: + """Return the number of elements in the tensor.""" + + +class _TypeInferenceKeypoint(TypedDict): + x: float + y: float + confidence: float + + +_TypeInferencePrediction = TypedDict( + "_TypeInferencePrediction", + { + "keypoints": list[_TypeInferenceKeypoint], + "class_id": int, + "class": str, + }, +) + + +class _TypeInferenceResult(TypedDict): + predictions: list[_TypeInferencePrediction] + + +class _TypeMediapipeLandmark(Protocol): + x: float + y: float + visibility: float + + +class _TypeMediapipeLandmarkList(Protocol): + landmark: list[_TypeMediapipeLandmark] + + +class _TypeMediapipeResults(Protocol): + pose_landmarks: ( + list[list[_TypeMediapipeLandmark]] | _TypeMediapipeLandmarkList | None + ) + face_landmarks: list[list[_TypeMediapipeLandmark]] + multi_face_landmarks: list[_TypeMediapipeLandmarkList] | None + + +class _TypeUltralyticsKeypoints(Protocol): + xy: _TypeTensorLike + conf: _TypeTensorLike + + +class _TypeUltralyticsBoxes(Protocol): + cls: _TypeTensorLike + + +class _TypeUltralyticsResults(Protocol): + keypoints: _TypeUltralyticsKeypoints + boxes: _TypeUltralyticsBoxes + names: dict[int, str] + + +class _TypeYoloNasPrediction(Protocol): + poses: npt.NDArray[np.float32] + labels: npt.NDArray[np.int_] | None + + +class _TypeYoloNasResults(Protocol): + prediction: _TypeYoloNasPrediction + class_names: Sequence[str] | None + + +class _TypeDetectron2Instances(Protocol): + pred_keypoints: _TypeTensorLike + pred_classes: _TypeTensorLike + + +class _TypeDetectron2Results(Protocol): + instances: _TypeDetectron2Instances + + +class _TypeTransformersResult(Protocol): + keypoints: _TypeTensorLike + scores: _TypeTensorLike + + +_TypeIndex1D = ( + int | slice | list[int] | list[bool] | npt.NDArray[np.int_] | npt.NDArray[np.bool_] +) +_TypeIndex2D = tuple[_TypeIndex1D, _TypeIndex1D] @dataclass @@ -167,7 +255,7 @@ class simplifies data manipulation and filtering, providing a uniform API for xy: npt.NDArray[np.float32] class_id: npt.NDArray[np.int_] | None = None confidence: npt.NDArray[np.float32] | None = None - data: dict[str, npt.NDArray[np.generic] | list[Any]] = field(default_factory=dict) + data: DetectionDataType = field(default_factory=dict) def __post_init__(self) -> None: validate_key_points_fields( @@ -202,9 +290,9 @@ def __iter__( ) -> Iterator[ tuple[ npt.NDArray[np.float32], - npt.NDArray[np.float32] | None, - npt.NDArray[np.int_] | None, - dict[str, npt.NDArray[np.generic] | list[Any]], + np.float32 | None, + np.integer | None, + DetectionDataType, ] ]: """ @@ -212,27 +300,42 @@ def __iter__( `(xy, confidence, class_id, data)` for each object detection. """ for i in range(len(self.xy)): + confidence = ( + cast(np.float32, self.confidence[i]) + if self.confidence is not None + else None + ) + class_id = ( + cast(np.integer, self.class_id[i]) + if self.class_id is not None + else None + ) yield ( self.xy[i], - self.confidence[i] if self.confidence is not None else None, - self.class_id[i] if self.class_id is not None else None, + confidence, + class_id, get_data_item(self.data, i), ) def __eq__(self, other: object) -> bool: if not isinstance(other, KeyPoints): return NotImplemented - return all( - [ - np.array_equal(self.xy, other.xy), - np.array_equal(self.class_id, other.class_id), - np.array_equal(self.confidence, other.confidence), - is_data_equal(self.data, other.data), - ] - ) + if not np.array_equal(self.xy, other.xy): + return False + if self.class_id is None or other.class_id is None: + if self.class_id is not other.class_id: + return False + elif not np.array_equal(self.class_id, other.class_id): + return False + if self.confidence is None or other.confidence is None: + if self.confidence is not other.confidence: + return False + elif not np.array_equal(self.confidence, other.confidence): + return False + return is_data_equal(self.data, other.data) @classmethod - def from_inference(cls, inference_result: Any) -> KeyPoints: + def from_inference(cls, inference_result: object) -> KeyPoints: """ Create a `sv.KeyPoints` object from the [Roboflow](https://roboflow.com/) API inference result or the [Inference](https://inference.roboflow.com/) @@ -281,20 +384,26 @@ def from_inference(cls, inference_result: Any) -> KeyPoints: ) if hasattr(inference_result, "dict"): - inference_result = inference_result.dict(exclude_none=True, by_alias=True) + inference_result = cast( + _TypeInferenceResult, + inference_result.dict(exclude_none=True, by_alias=True), + ) elif hasattr(inference_result, "json"): - inference_result = inference_result.json() + inference_result = cast(_TypeInferenceResult, inference_result.json()) + else: + inference_result = cast(_TypeInferenceResult, inference_result) + if not inference_result.get("predictions"): return cls.empty() - xy = [] - confidence = [] - class_id = [] - class_names = [] + xy: list[list[list[float]]] = [] + confidence: list[list[float]] = [] + class_id: list[int] = [] + class_names: list[str] = [] for prediction in inference_result["predictions"]: - prediction_xy = [] - prediction_confidence = [] + prediction_xy: list[list[float]] = [] + prediction_confidence: list[float] = [] for keypoint in prediction["keypoints"]: prediction_xy.append([keypoint["x"], keypoint["y"]]) prediction_confidence.append(keypoint["confidence"]) @@ -304,18 +413,20 @@ def from_inference(cls, inference_result: Any) -> KeyPoints: class_id.append(prediction["class_id"]) class_names.append(prediction["class"]) - data = {CLASS_NAME_DATA_FIELD: np.array(class_names)} + data: DetectionDataType = { + CLASS_NAME_DATA_FIELD: np.array(class_names, dtype=str) + } return cls( xy=np.array(xy, dtype=np.float32), confidence=np.array(confidence, dtype=np.float32), - class_id=np.array(class_id, dtype=int), + class_id=np.array(class_id, dtype=np.int_), data=data, ) @classmethod def from_mediapipe( - cls, mediapipe_results: Any, resolution_wh: tuple[int, int] + cls, mediapipe_results: _TypeMediapipeResults, resolution_wh: tuple[int, int] ) -> KeyPoints: """ Creates a `sv.KeyPoints` instance from a @@ -392,18 +503,15 @@ def from_mediapipe( ``` """ + results: Sequence[Sequence[_TypeMediapipeLandmark]] = [] if hasattr(mediapipe_results, "pose_landmarks"): - results = mediapipe_results.pose_landmarks - if not isinstance(mediapipe_results.pose_landmarks, list): - if mediapipe_results.pose_landmarks is None: - results = [] - else: - results = [ - [ - landmark - for landmark in mediapipe_results.pose_landmarks.landmark - ] - ] + pose_landmarks = mediapipe_results.pose_landmarks + if pose_landmarks is None: + results = [] + elif isinstance(pose_landmarks, list): + results = pose_landmarks + else: + results = [pose_landmarks.landmark] elif hasattr(mediapipe_results, "face_landmarks"): results = mediapipe_results.face_landmarks elif hasattr(mediapipe_results, "multi_face_landmarks"): @@ -418,11 +526,11 @@ def from_mediapipe( if len(results) == 0: return cls.empty() - xy = [] - confidence = [] + xy: list[list[list[float]]] = [] + confidence: list[list[float]] = [] for pose in results: - prediction_xy = [] - prediction_confidence = [] + prediction_xy: list[list[float]] = [] + prediction_confidence: list[float] = [] for landmark in pose: keypoint_xy = [ landmark.x * resolution_wh[0], @@ -440,7 +548,9 @@ def from_mediapipe( ) @classmethod - def from_ultralytics(cls, ultralytics_results: Any) -> KeyPoints: + def from_ultralytics( + cls, ultralytics_results: _TypeUltralyticsResults + ) -> KeyPoints: """ Creates a `sv.KeyPoints` instance from a [YOLOv8](https://github.com/ultralytics/ultralytics) pose inference result. @@ -468,16 +578,18 @@ def from_ultralytics(cls, ultralytics_results: Any) -> KeyPoints: if ultralytics_results.keypoints.xy.numel() == 0: return cls.empty() - xy = ultralytics_results.keypoints.xy.cpu().numpy() - class_id = ultralytics_results.boxes.cls.cpu().numpy().astype(int) - class_names = np.array([ultralytics_results.names[i] for i in class_id]) + xy = ultralytics_results.keypoints.xy.cpu().numpy().astype(np.float32) + class_id = ultralytics_results.boxes.cls.cpu().numpy().astype(np.int_) + class_names = np.array( + [ultralytics_results.names[int(i)] for i in class_id], dtype=str + ) - confidence = ultralytics_results.keypoints.conf.cpu().numpy() - data = {CLASS_NAME_DATA_FIELD: class_names} + confidence = ultralytics_results.keypoints.conf.cpu().numpy().astype(np.float32) + data: DetectionDataType = {CLASS_NAME_DATA_FIELD: class_names} return cls(xy, class_id, confidence, data) @classmethod - def from_yolo_nas(cls, yolo_nas_results: Any) -> KeyPoints: + def from_yolo_nas(cls, yolo_nas_results: _TypeYoloNasResults) -> KeyPoints: """ Create a `sv.KeyPoints` instance from a [YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS-POSE.md) pose inference results. @@ -519,7 +631,7 @@ def from_yolo_nas(cls, yolo_nas_results: Any) -> KeyPoints: else: class_id = None - data = {} + data: DetectionDataType = {} if class_id is not None and yolo_nas_results.class_names is not None: class_names = [] for c_id in class_id: @@ -535,7 +647,7 @@ def from_yolo_nas(cls, yolo_nas_results: Any) -> KeyPoints: ) @classmethod - def from_detectron2(cls, detectron2_results: Any) -> KeyPoints: + def from_detectron2(cls, detectron2_results: dict[str, object]) -> KeyPoints: """ Create a `sv.KeyPoints` object from the [Detectron2](https://github.com/facebookresearch/detectron2) inference result. @@ -567,26 +679,24 @@ def from_detectron2(cls, detectron2_results: Any) -> KeyPoints: ``` """ - if hasattr(detectron2_results["instances"], "pred_keypoints"): - if detectron2_results["instances"].pred_keypoints.cpu().numpy().size == 0: + instances = cast(_TypeDetectron2Instances, detectron2_results["instances"]) + if hasattr(instances, "pred_keypoints"): + if instances.pred_keypoints.cpu().numpy().size == 0: return cls.empty() return cls( - xy=detectron2_results["instances"] - .pred_keypoints.cpu() - .numpy()[:, :, :2], - confidence=detectron2_results["instances"] - .pred_keypoints.cpu() - .numpy()[:, :, 2], - class_id=detectron2_results["instances"] - .pred_classes.cpu() - .numpy() - .astype(int), + xy=instances.pred_keypoints.cpu().numpy()[:, :, :2].astype(np.float32), + confidence=instances.pred_keypoints.cpu() + .numpy()[:, :, 2] + .astype(np.float32), + class_id=instances.pred_classes.cpu().numpy().astype(np.int_), ) else: return cls.empty() @classmethod - def from_transformers(cls, transformers_results: Any) -> KeyPoints: + def from_transformers( + cls, transformers_results: list[dict[str, _TypeTensorLike]] + ) -> KeyPoints: """ Create a `sv.KeyPoints` object from the [Transformers](https://github.com/huggingface/transformers) inference result. @@ -665,7 +775,7 @@ def from_transformers(cls, transformers_results: Any) -> KeyPoints: return cls( xy=np.stack(xy).astype(np.float32), confidence=np.stack(scores).astype(np.float32), - class_id=np.arange(len(xy)).astype(int), + class_id=np.arange(len(xy)).astype(np.int_), ) else: return cls.empty() @@ -743,15 +853,20 @@ def _get_by_2d_bool_mask(self, mask: npt.NDArray[np.bool_]) -> KeyPoints: data=get_data_item(self.data, slice(None)), ) + @overload + def __getitem__(self, index: str) -> object: ... + + @overload + def __getitem__(self, index: _TypeIndex1D | _TypeIndex2D) -> KeyPoints: ... + def __getitem__( - self, - index: Index1D | Index2D | str, - ) -> KeyPoints | npt.NDArray[np.generic] | list[Any] | None: + self, index: _TypeIndex1D | _TypeIndex2D | str + ) -> KeyPoints | object | None: if isinstance(index, str): return self.data.get(index) if isinstance(index, np.ndarray) and index.ndim == 2 and index.dtype == bool: - return self._get_by_2d_bool_mask(index) + return self._get_by_2d_bool_mask(cast(npt.NDArray[np.bool_], index)) if not isinstance(index, tuple): index = (index, slice(None)) @@ -777,7 +892,7 @@ def __getitem__( and not np.isscalar(i) and not np.isscalar(j) ): - i, j = np.ix_(i, j) + i, j = np.ix_(np.asarray(i), np.asarray(j)) xy_selected = self.xy[i, j] @@ -812,13 +927,15 @@ def __getitem__( data=data_selected, ) - def __setitem__(self, key: str, value: npt.NDArray[np.generic] | list[Any]) -> None: + def __setitem__( + self, key: str, value: npt.NDArray[np.generic] | list[object] + ) -> None: """ Set a value in the data dictionary of the `sv.KeyPoints` object. Args: key: The key in the data dictionary to set. - value: The value to set for the key. + value: The value to set for the key. Must be a np.ndarray or list. Examples: ```python @@ -953,6 +1070,7 @@ def as_detections( detections = Detections.merge(detections_list) detections.class_id = self.class_id detections.data = self.data - detections = cast(Detections, detections[detections.area > 0]) + area = detections.area + detections = detections[area > 0] return detections diff --git a/src/supervision/metrics/core.py b/src/supervision/metrics/core.py index ad6a17818e..c9287666e8 100644 --- a/src/supervision/metrics/core.py +++ b/src/supervision/metrics/core.py @@ -2,16 +2,19 @@ from abc import ABC, abstractmethod from enum import Enum -from typing import Any +from typing import Any, Generic, ParamSpec, TypeVar +P = ParamSpec("P") +R = TypeVar("R") -class Metric(ABC): + +class Metric(ABC, Generic[P, R]): """ The base class for all supervision metrics. """ @abstractmethod - def update(self, *args: Any, **kwargs: Any) -> Metric: + def update(self, *args: P.args, **kwargs: P.kwargs) -> Metric[P, R]: """ Add data to the metric, without computing the result. Return the metric itself to allow method chaining. @@ -26,7 +29,7 @@ def reset(self) -> None: raise NotImplementedError @abstractmethod - def compute(self, *args: Any, **kwargs: Any) -> Any: + def compute(self, *args: Any, **kwargs: Any) -> R: """ Compute the metric from the internal state and return the result. """ diff --git a/src/supervision/metrics/detection.py b/src/supervision/metrics/detection.py index ccebf3e513..b6342ca0ef 100644 --- a/src/supervision/metrics/detection.py +++ b/src/supervision/metrics/detection.py @@ -2,6 +2,7 @@ from collections.abc import Callable from dataclasses import dataclass +from typing import cast import matplotlib import matplotlib.pyplot as plt @@ -143,8 +144,8 @@ def from_detections( ... classes=['person'] ... ) >>> confusion_matrix.matrix - array([[1., 0.], - [0., 0.]]) + array([[1, 0], + [0, 0]], dtype=int32) ``` """ @@ -217,16 +218,18 @@ def from_tensors( ... classes=['person', 'dog'] ... ) >>> confusion_matrix.matrix - array([[1., 0., 1.], - [0., 1., 0.], - [1., 0., 0.]]) + array([[1, 0, 1], + [0, 1, 0], + [1, 0, 0]], dtype=int32) ``` """ validate_input_tensors(predictions, targets) num_classes = len(classes) - matrix = np.zeros((num_classes + 1, num_classes + 1)) + matrix: npt.NDArray[np.int32] = np.zeros( + (num_classes + 1, num_classes + 1), dtype=np.int32 + ) for true_batch, detection_batch in zip(targets, predictions): matrix += cls.evaluate_detection_batch( predictions=detection_batch, @@ -271,7 +274,9 @@ def evaluate_detection_batch( Returns: Confusion matrix based on a single image. """ - result_matrix = np.zeros((num_classes + 1, num_classes + 1)) + result_matrix: npt.NDArray[np.int32] = np.zeros( + (num_classes + 1, num_classes + 1), dtype=np.int32 + ) # Filter predictions by confidence threshold conf_idx = 5 @@ -472,7 +477,7 @@ def plot( Confusion matrix plot. """ - array = self.matrix.copy() + array = self.matrix.astype(np.float32, copy=True) if normalize: eps = 1e-8 @@ -495,7 +500,7 @@ def plot( im = ax.imshow(array, cmap="Blues") cbar = ax.figure.colorbar(im, ax=ax) - cbar.mappable.set_clim(vmin=0, vmax=np.nanmax(array)) + cbar.mappable.set_clim(vmin=0, vmax=float(np.nanmax(array))) if x_tick_labels is None: tick_interval = 2 @@ -572,7 +577,7 @@ class MeanAveragePrecision: map50_95: float map50: float map75: float - per_class_ap50_95: npt.NDArray[np.float64] + per_class_ap50_95: npt.NDArray[np.float32] @classmethod def from_detections( @@ -742,7 +747,9 @@ def from_tensors( if true_objs.shape[0]: matches = cls._match_detection_batch( - predicted_objs, true_objs, iou_thresholds + predicted_objs, + true_objs, + iou_thresholds.astype(np.float32, copy=False), ) stats.append( ( @@ -756,7 +763,12 @@ def from_tensors( # Compute average precisions if any matches exist if stats: concatenated_stats = [np.concatenate(items, 0) for items in zip(*stats)] - average_precisions = cls._average_precisions_per_class(*concatenated_stats) + average_precisions = cls._average_precisions_per_class( + cast(npt.NDArray[np.bool_], concatenated_stats[0]), + cast(npt.NDArray[np.float32], concatenated_stats[1]), + cast(npt.NDArray[np.int32], concatenated_stats[2]), + cast(npt.NDArray[np.int32], concatenated_stats[3]), + ) map50 = average_precisions[:, 0].mean() map75 = average_precisions[:, 5].mean() map50_95 = average_precisions.mean() @@ -861,7 +873,7 @@ def _average_precisions_per_class( prediction_class_ids: npt.NDArray[np.int32], true_class_ids: npt.NDArray[np.int32], eps: float = 1e-16, - ) -> npt.NDArray[np.float64]: + ) -> npt.NDArray[np.float32]: """ Compute the average precision, given the recall and precision curves. Source: https://github.com/rafaelpadilla/Object-Detection-Metrics. @@ -883,8 +895,8 @@ def _average_precisions_per_class( unique_classes, class_counts = np.unique(true_class_ids, return_counts=True) num_classes = unique_classes.shape[0] - average_precisions: npt.NDArray[np.float64] = np.zeros( - (num_classes, matches.shape[1]), dtype=np.float64 + average_precisions: npt.NDArray[np.float32] = np.zeros( + (num_classes, matches.shape[1]), dtype=np.float32 ) for class_idx, class_id in enumerate(unique_classes): @@ -907,5 +919,5 @@ def _average_precisions_per_class( ) ) - result: npt.NDArray[np.float64] = average_precisions + result: npt.NDArray[np.float32] = average_precisions return result diff --git a/src/supervision/metrics/f1_score.py b/src/supervision/metrics/f1_score.py index 9525a6dde5..eaabb0345d 100644 --- a/src/supervision/metrics/f1_score.py +++ b/src/supervision/metrics/f1_score.py @@ -2,7 +2,7 @@ from copy import deepcopy from dataclasses import dataclass -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, cast import numpy as np import numpy.typing as npt @@ -26,8 +26,20 @@ if TYPE_CHECKING: import pandas as pd +F1Stats = tuple[ + npt.NDArray[np.bool_], + npt.NDArray[np.float32], + npt.NDArray[np.int32], + npt.NDArray[np.int32], +] -class F1Score(Metric): + +class F1Score( + Metric[ + [Detections | list[Detections], Detections | list[Detections]], + "F1ScoreResult", + ] +): """ F1 Score is a metric used to evaluate object detection models. It is the harmonic mean of precision and recall, calculated at different IoU thresholds. @@ -67,7 +79,7 @@ def __init__( self, metric_target: MetricTarget = MetricTarget.BOXES, averaging_method: AveragingMethod = AveragingMethod.WEIGHTED, - ): + ) -> None: """ Initialize the F1Score metric. @@ -152,32 +164,61 @@ def compute(self) -> F1ScoreResult: def _compute( self, predictions_list: list[Detections], targets_list: list[Detections] ) -> F1ScoreResult: - iou_thresholds = np.linspace(0.5, 0.95, 10) - stats: list[Any] = [] + iou_thresholds = np.linspace(0.5, 0.95, 10, dtype=np.float32) + stats: list[F1Stats] = [] for predictions, targets in zip(predictions_list, targets_list): prediction_contents = self._detections_content(predictions) target_contents = self._detections_content(targets) if len(targets) > 0: + if predictions.class_id is None or targets.class_id is None: + raise ValueError( + "F1Score metric requires `class_id` on both predictions " + "and targets." + ) if len(predictions) == 0: stats.append( ( np.zeros((0, iou_thresholds.size), dtype=bool), np.zeros((0,), dtype=np.float32), - np.zeros((0,), dtype=int), - targets.class_id, + np.zeros((0,), dtype=np.int32), + np.asarray(targets.class_id, dtype=np.int32), ) ) else: + if predictions.confidence is None: + raise ValueError( + "F1Score metric requires `confidence` on predictions." + ) + prediction_class_ids = np.asarray( + predictions.class_id, dtype=np.int32 + ) + target_class_ids = np.asarray(targets.class_id, dtype=np.int32) if self._metric_target == MetricTarget.BOXES: - iou = box_iou_batch(target_contents, prediction_contents) + iou: npt.NDArray[np.float64] = np.asarray( + box_iou_batch( + np.asarray(target_contents, dtype=np.float32), + np.asarray(prediction_contents, dtype=np.float32), + ), + dtype=np.float64, + ) elif self._metric_target == MetricTarget.MASKS: - iou = mask_iou_batch(target_contents, prediction_contents) + iou = np.asarray( + mask_iou_batch( + np.asarray(target_contents, dtype=bool), + np.asarray(prediction_contents, dtype=bool), + ), + dtype=np.float64, + ) elif self._metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES: - iou = oriented_box_iou_batch( - target_contents, prediction_contents + iou = np.asarray( + oriented_box_iou_batch( + np.asarray(target_contents, dtype=np.float32), + np.asarray(prediction_contents, dtype=np.float32), + ), + dtype=np.float64, ) else: raise ValueError( @@ -185,21 +226,17 @@ def _compute( ) matches = self._match_detection_batch( - predictions.class_id - if predictions.class_id is not None - else np.array([]), - targets.class_id - if targets.class_id is not None - else np.array([]), + prediction_class_ids, + target_class_ids, iou, iou_thresholds, ) stats.append( ( matches, - predictions.confidence, - predictions.class_id, - targets.class_id, + np.asarray(predictions.confidence, dtype=np.float32), + prediction_class_ids, + target_class_ids, ) ) @@ -210,7 +247,7 @@ def _compute( f1_scores=np.zeros(iou_thresholds.shape[0]), f1_per_class=np.zeros((0, iou_thresholds.shape[0])), iou_thresholds=iou_thresholds, - matched_classes=np.array([], dtype=int), + matched_classes=np.array([], dtype=np.int32), small_objects=None, medium_objects=None, large_objects=None, @@ -248,6 +285,8 @@ def _compute_f1_for_classes( matches = matches[sorted_indices] prediction_class_ids = prediction_class_ids[sorted_indices] unique_classes, class_counts = np.unique(true_class_ids, return_counts=True) + unique_classes = np.asarray(unique_classes, dtype=np.int32) + class_counts = np.asarray(class_counts, dtype=np.int32) # Shape: PxTh,P,C,C -> CxThx3 confusion_matrix = self._compute_confusion_matrix( @@ -273,7 +312,7 @@ def _compute_f1_for_classes( def _match_detection_batch( predictions_classes: npt.NDArray[np.int32], target_classes: npt.NDArray[np.int32], - iou: npt.NDArray[np.float32], + iou: npt.NDArray[np.floating], iou_thresholds: npt.NDArray[np.float32], ) -> npt.NDArray[np.bool_]: num_predictions, num_iou_levels = ( @@ -304,9 +343,9 @@ def _match_detection_batch( @staticmethod def _compute_confusion_matrix( sorted_matches: npt.NDArray[np.bool_], - sorted_prediction_class_ids: npt.NDArray[np.int32], - unique_classes: npt.NDArray[np.int32], - class_counts: npt.NDArray[np.int32], + sorted_prediction_class_ids: npt.NDArray[np.integer], + unique_classes: npt.NDArray[np.integer], + class_counts: npt.NDArray[np.integer], ) -> npt.NDArray[np.float64]: """ Compute the confusion matrix for each class and IoU threshold. @@ -341,13 +380,15 @@ class ids. num_predictions = is_class.sum() if num_predictions == 0: - true_positives = np.zeros(num_thresholds) - false_positives = np.zeros(num_thresholds) - false_negatives = np.full(num_thresholds, num_true) + true_positives = np.zeros(num_thresholds, dtype=np.float64) + false_positives = np.zeros(num_thresholds, dtype=np.float64) + false_negatives = np.full(num_thresholds, num_true, dtype=np.float64) elif num_true == 0: - true_positives = np.zeros(num_thresholds) - false_positives = np.full(num_thresholds, num_predictions) - false_negatives = np.zeros(num_thresholds) + true_positives = np.zeros(num_thresholds, dtype=np.float64) + false_positives = np.full( + num_thresholds, num_predictions, dtype=np.float64 + ) + false_negatives = np.zeros(num_thresholds, dtype=np.float64) else: true_positives = sorted_matches[is_class].sum(0) false_positives = (1 - sorted_matches[is_class]).sum(0) @@ -389,14 +430,18 @@ def _compute_f1( result_f1_score: npt.NDArray[np.float64] = f1_score return result_f1_score - def _detections_content(self, detections: Detections) -> npt.NDArray[Any]: + def _detections_content(self, detections: Detections) -> npt.NDArray[np.generic]: """Return boxes, masks or oriented bounding boxes from detections.""" if self._metric_target == MetricTarget.BOXES: - result_boxes: npt.NDArray[np.float32] = detections.xyxy + result_boxes: npt.NDArray[np.float32] = np.asarray( + detections.xyxy, dtype=np.float32 + ) return result_boxes if self._metric_target == MetricTarget.MASKS: if detections.mask is not None: - result_masks: npt.NDArray[np.bool_] = detections.mask + result_masks: npt.NDArray[np.bool_] = np.asarray( + detections.mask, dtype=bool + ) return result_masks return self._make_empty_content() if self._metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES: @@ -407,7 +452,7 @@ def _detections_content(self, detections: Detections) -> npt.NDArray[Any]: return self._make_empty_content() raise ValueError(f"Invalid metric target: {self._metric_target}") - def _make_empty_content(self) -> npt.NDArray[Any]: + def _make_empty_content(self) -> npt.NDArray[np.generic]: if self._metric_target == MetricTarget.BOXES: empty_boxes: npt.NDArray[np.float32] = np.empty((0, 4), dtype=np.float32) return empty_boxes @@ -430,18 +475,30 @@ def _filter_detections_by_size( sizes = get_detection_size_category(new_detections, self._metric_target) size_mask = sizes == size_category.value - new_detections.xyxy = new_detections.xyxy[size_mask] + new_detections.xyxy = cast( + npt.NDArray[np.number], new_detections.xyxy[size_mask] + ) if new_detections.mask is not None: - new_detections.mask = new_detections.mask[size_mask] + new_detections.mask = cast( + npt.NDArray[np.bool_], new_detections.mask[size_mask] + ) if new_detections.class_id is not None: - new_detections.class_id = new_detections.class_id[size_mask] + new_detections.class_id = cast( + npt.NDArray[np.int32], new_detections.class_id[size_mask] + ) if new_detections.confidence is not None: - new_detections.confidence = new_detections.confidence[size_mask] + new_detections.confidence = cast( + npt.NDArray[np.float32], new_detections.confidence[size_mask] + ) if new_detections.tracker_id is not None: - new_detections.tracker_id = new_detections.tracker_id[size_mask] + new_detections.tracker_id = cast( + npt.NDArray[np.int32], new_detections.tracker_id[size_mask] + ) if new_detections.data is not None: for key, value in new_detections.data.items(): - new_detections.data[key] = np.array(value)[size_mask] + new_detections.data[key] = cast( + npt.NDArray[np.generic], np.asarray(value)[size_mask] + ) return new_detections @@ -593,7 +650,7 @@ def to_pandas(self) -> pd.DataFrame: ensure_pandas_installed() import pandas as pd - pandas_data = { + pandas_data: dict[str, object] = { "F1@50": self.f1_50, "F1@75": self.f1_75, } diff --git a/src/supervision/metrics/mean_average_precision.py b/src/supervision/metrics/mean_average_precision.py index 8d598da9c2..0db410b4a6 100644 --- a/src/supervision/metrics/mean_average_precision.py +++ b/src/supervision/metrics/mean_average_precision.py @@ -7,7 +7,7 @@ from copy import deepcopy from dataclasses import dataclass from enum import Enum -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, TypeAlias, TypedDict import numpy as np import numpy.typing as npt @@ -26,6 +26,40 @@ import pandas as pd +class _TypeCocoDict(TypedDict, total=False): + id: int + image_id: int + category_id: int + bbox: list[float] + area: float + iscrowd: int + ignore: int + _ignore: int + score: float + segmentation: list[list[float]] + name: str + supercategory: str + caption: str + keypoints: list[float] + + +_TypeCocoDataset: TypeAlias = dict[str, list[_TypeCocoDict]] + + +class _TypeEvaluationImageResult(TypedDict): + image_id: int + category_id: int + area_range: list[float] | tuple[float, float] + max_det: int + dt_ids: list[int] + gt_ids: list[int] + dtMatches: npt.NDArray[np.int64] + gtMatches: npt.NDArray[np.int64] + dtScores: list[float] + gtIgnore: npt.NDArray[np.int64] + dtIgnore: npt.NDArray[np.bool_] + + @dataclass class MeanAveragePrecisionResult: """ @@ -152,7 +186,7 @@ def to_pandas(self) -> pd.DataFrame: ensure_pandas_installed() import pandas as pd - pandas_data = { + pandas_data: dict[str, object] = { "mAP@50:95": self.map50_95, "mAP@50": self.map50, "mAP@75": self.map75, @@ -252,7 +286,7 @@ class EvaluationDataset: `COCOEvaluator` class. """ - def __init__(self, targets: dict[str, Any] | None = None): + def __init__(self, targets: _TypeCocoDataset | None = None) -> None: """ Constructor of EvaluationDataset object used to evaluate models with Mean Average Precision. @@ -263,11 +297,11 @@ def __init__(self, targets: dict[str, Any] | None = None): """ # Initialize members # Initialize members - self.dataset: dict[str, Any] = dict() - self.anns: dict[int, Any] = dict() - self.cats: dict[int, Any] = dict() - self.imgs: dict[int, Any] = dict() - self.img_to_anns: dict[int, list[Any]] = defaultdict(list) + self.dataset: _TypeCocoDataset = {} + self.anns: dict[int, _TypeCocoDict] = {} + self.cats: dict[int, _TypeCocoDict] = {} + self.imgs: dict[int, _TypeCocoDict] = {} + self.img_to_anns: dict[int, list[_TypeCocoDict]] = defaultdict(list) self.cat_to_imgs: dict[int, list[int]] = defaultdict(list) if targets is None: @@ -285,8 +319,11 @@ def create_class_members(self) -> None: """ Create index elements for the dataset. """ - anns, cats, imgs = {}, {}, {} - img_to_anns, cat_to_imgs = defaultdict(list), defaultdict(list) + anns: dict[int, _TypeCocoDict] = {} + cats: dict[int, _TypeCocoDict] = {} + imgs: dict[int, _TypeCocoDict] = {} + img_to_anns: dict[int, list[_TypeCocoDict]] = defaultdict(list) + cat_to_imgs: dict[int, list[int]] = defaultdict(list) if "annotations" in self.dataset: for ann in self.dataset["annotations"]: img_to_anns[ann["image_id"]].append(ann) @@ -442,7 +479,7 @@ def get_image_ids( return list(ids_set) - def get_annotations(self, ids: list[int] | None = None) -> list[dict[str, Any]]: + def get_annotations(self, ids: list[int] | None = None) -> list[_TypeCocoDict]: """ Get annotations with the specified ids. @@ -456,7 +493,7 @@ def get_annotations(self, ids: list[int] | None = None) -> list[dict[str, Any]]: return [] return [self.anns[idx] for idx in ids] - def load_predictions(self, predictions: list[dict[str, Any]]) -> EvaluationDataset: + def load_predictions(self, predictions: list[_TypeCocoDict]) -> EvaluationDataset: """ Load prediction result into an EvaluationDataset object. @@ -468,7 +505,7 @@ def load_predictions(self, predictions: list[dict[str, Any]]) -> EvaluationDatas """ # Create an empty EvaluationDataset object for the predictions predictions_dataset = EvaluationDataset.empty() - predictions_dataset.dataset["images"] = [img for img in self.dataset["images"]] + predictions_dataset.dataset["images"] = list(self.dataset["images"]) if not isinstance(predictions, list): raise ValueError("results must be a list") @@ -588,7 +625,7 @@ class COCOEvaluator: def __init__( self, coco_targets: EvaluationDataset, coco_predictions: EvaluationDataset - ): + ) -> None: """ Constructor of COCOEvaluator object. @@ -606,18 +643,22 @@ def __init__( # List of dictionaries containing the evaluation results # len(eval_imgs) = (categories) * (area_ranges) * (images) # For COCO 2017: len(eval_images) = 80 * 4 * 5000 = 1600000 - self.eval_imgs: Any = defaultdict(list) + self.eval_imgs: list[_TypeEvaluationImageResult | None] = [] # Dictionary of accumulated results - self.results: dict[str, Any] = {} + self.results: dict[str, object] = {} # Dictionary of targets for evaluation - self._targets: defaultdict[tuple[int, int], list[Any]] = defaultdict(list) - self._predictions: defaultdict[tuple[int, int], list[Any]] = defaultdict(list) + self._targets: defaultdict[tuple[int, int], list[_TypeCocoDict]] = defaultdict( + list + ) + self._predictions: defaultdict[tuple[int, int], list[_TypeCocoDict]] = ( + defaultdict(list) + ) # Parameters for evaluation self.params = COCOEvaluatorParameters() # List of results summarization - self.stats: list[Any] = [] + self.stats: list[object] = [] # Dictionary of IOUs between all targets and predictions - self.ious: dict[tuple[int, int], Any] = {} + self.ious: dict[tuple[int, int], npt.NDArray[np.float32]] = {} # Set image and category ids self.params.img_ids = sorted(self.coco_targets.get_image_ids()) self.params.cat_ids = sorted(self.coco_targets.get_category_ids()) @@ -653,7 +694,7 @@ def _prepare_targets_and_predictions(self) -> None: self._predictions[dt["image_id"], dt["category_id"]].append(dt) # Initialize evaluation results - self.eval_imgs = defaultdict(list) + self.eval_imgs = [] self.results = {} def _compute_iou(self, img_id: int, cat_id: int) -> npt.NDArray[np.float32]: @@ -703,7 +744,7 @@ def _evaluate_image( cat_id: int, area_range: list[float] | tuple[float, float], max_det: int, - ) -> dict[str, Any] | None: + ) -> _TypeEvaluationImageResult | None: """ Perform evaluation for single category and image. Args: @@ -716,8 +757,8 @@ def _evaluate_image( The evaluation results. """ # Get targets (gt) and predictions (dt) for the given image and category - gt: list[dict[str, Any]] = self._targets[img_id, cat_id] - dt: list[dict[str, Any]] = self._predictions[img_id, cat_id] + gt: list[_TypeCocoDict] = self._targets[img_id, cat_id] + dt: list[_TypeCocoDict] = self._predictions[img_id, cat_id] # If there is nothing to evaluate if len(gt) == 0 and len(dt) == 0: @@ -754,11 +795,11 @@ def _evaluate_image( num_detections = len(dt) # Initialize matches: 0 means no match - gt_matches = np.zeros((num_thresholds, num_ground_truths)) - dt_matches = np.zeros((num_thresholds, num_detections)) + gt_matches = np.zeros((num_thresholds, num_ground_truths), dtype=np.int64) + dt_matches = np.zeros((num_thresholds, num_detections), dtype=np.int64) # Initialize ignore flags: 0 means no ignore - gt_ignore = np.array([g["_ignore"] for g in gt]) - dt_ignore = np.zeros((num_thresholds, num_detections)) + gt_ignore = np.array([g["_ignore"] for g in gt], dtype=np.int64) + dt_ignore = np.zeros((num_thresholds, num_detections), dtype=np.bool_) if len(ious) != 0: # Go through the iou thresholds for tresh_idx, thresh in enumerate(self.params.iou_thrs): @@ -772,7 +813,7 @@ def _evaluate_image( for g_idx, g in enumerate(gt): # If current gt is already matched, and not a crowd, continue # if gt_matches[tresh_idx, g_idx] > 0 and not iscrowd[g_idx]: - iscrowd = int(g.get("iscrowd", 0)) + iscrowd = g["iscrowd"] if gt_matches[tresh_idx, g_idx] > 0 and not iscrowd: continue # Stop searching the ground truths @@ -901,10 +942,12 @@ def _accumulate(self) -> None: # Loop through max detections for max_det_idx, max_det in enumerate(selected_max_detections): - eval_img_data = [ + eval_img_data_raw = [ self.eval_imgs[cat_offset + area_offset + i] for i in image_inds ] - eval_img_data = [e for e in eval_img_data if e is not None] + eval_img_data: list[_TypeEvaluationImageResult] = [ + e for e in eval_img_data_raw if e is not None + ] # No image to evaluate if len(eval_img_data) == 0: @@ -1009,23 +1052,23 @@ def _accumulate(self) -> None: # Helper function to compute average precision while handling -1 sentinel values def compute_average_precision( precision_slice: npt.NDArray[np.float32], - ) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.float32]]: + ) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]: """Compute average precision while handling -1 sentinel values.""" valid_mask = precision_slice != -1 valid_precision = np.where(valid_mask, precision_slice, np.float32(0.0)) def mean_with_mask( axis: int | tuple[int, ...], - ) -> npt.NDArray[np.float32]: + ) -> npt.NDArray[np.float64]: sums = valid_precision.sum(axis=axis, dtype=np.float64) counts = valid_mask.sum(axis=axis) - means = np.divide( + means: npt.NDArray[np.float64] = np.divide( sums, counts, out=np.full(sums.shape, -1.0, dtype=np.float64), where=counts > 0, ) - return means.astype(np.float32) + return means mAP_scores = mean_with_mask((1, 2)) ap_per_class = mean_with_mask(1).transpose(1, 0) @@ -1119,7 +1162,9 @@ def _summarize( if use_ap: # Dimension of precision: # threshold x recall x classes x areas x max detections - s = self.results["precision"] + s: npt.NDArray[np.float32] = np.asarray( + self.results["precision"], dtype=np.float32 + ) # IOU if iou_thr is not None: t = np.where(iou_thr == self.params.iou_thrs)[0] @@ -1128,7 +1173,7 @@ def _summarize( else: # Dimension of recall: # threshold x classes x areas x max detections - s = self.results["recall"] + s = np.asarray(self.results["recall"], dtype=np.float32) if iou_thr is not None: t = np.where(iou_thr == self.params.iou_thrs)[0] s = s[t] @@ -1223,7 +1268,12 @@ def evaluate(self) -> None: self._accumulate() -class MeanAveragePrecision(Metric): +class MeanAveragePrecision( + Metric[ + [Detections | list[Detections], Detections | list[Detections]], + MeanAveragePrecisionResult, + ] +): """ Mean Average Precision (mAP) is a metric used to evaluate object detection models. It is the average of the precision-recall curves at different IoU thresholds. @@ -1267,7 +1317,7 @@ def __init__( class_agnostic: bool = False, class_mapping: dict[int, int] | None = None, image_indices: list[int] | None = None, - ): + ) -> None: """ Initialize the Mean Average Precision metric. @@ -1336,13 +1386,13 @@ def update( def _prepare_targets( self, targets: list[Detections] - ) -> dict[str, list[dict[str, Any]]]: + ) -> dict[str, list[_TypeCocoDict]]: """Transform targets into a dictionary that can be used by the COCO evaluator""" - images = [{"id": img_id} for img_id in range(len(targets))] + images: list[_TypeCocoDict] = [{"id": img_id} for img_id in range(len(targets))] if self._image_indices is not None: images = [{"id": self._image_indices[img["id"]]} for img in images] # Annotations list - annotations: list[dict[str, Any]] = [] + annotations: list[_TypeCocoDict] = [] for image_id, image_targets in enumerate(targets): if self._image_indices is not None: image_id = self._image_indices[image_id] @@ -1367,16 +1417,22 @@ def _prepare_targets( # Use area from data if available, otherwise calculate from bbox area = None if image_targets.data is not None and "area" in image_targets.data: - area = float(image_targets.data["area"][target_idx]) + area_data: npt.NDArray[np.float32] = np.asarray( + image_targets.data["area"], dtype=np.float32 + ) + area = float(area_data[target_idx]) if area is None: area = xywh[2] * xywh[3] iscrowd = 0 if image_targets.data is not None and "iscrowd" in image_targets.data: - iscrowd = int(image_targets.data["iscrowd"][target_idx]) + iscrowd_data: npt.NDArray[np.int64] = np.asarray( + image_targets.data["iscrowd"], dtype=np.int64 + ) + iscrowd = int(iscrowd_data[target_idx]) - dict_annotation = { + dict_annotation: _TypeCocoDict = { "area": area, "iscrowd": iscrowd, "image_id": image_id, @@ -1387,8 +1443,8 @@ def _prepare_targets( } annotations.append(dict_annotation) # Category list - all_cat_ids = {annotation.get("category_id") for annotation in annotations} - categories = [{"id": cat_id} for cat_id in all_cat_ids] + all_cat_ids = {annotation["category_id"] for annotation in annotations} + categories: list[_TypeCocoDict] = [{"id": cat_id} for cat_id in all_cat_ids] # Create coco dictionary return { "images": images, @@ -1398,10 +1454,10 @@ def _prepare_targets( def _prepare_predictions( self, predictions: list[Detections] - ) -> list[dict[str, Any]]: + ) -> list[_TypeCocoDict]: """Transform predictions into a list of predictions that can be used by the COCO evaluator.""" - coco_predictions: list[dict[str, Any]] = [] + coco_predictions: list[_TypeCocoDict] = [] for image_id, image_predictions in enumerate(predictions): if self._image_indices is not None: image_id = self._image_indices[image_id] @@ -1431,12 +1487,15 @@ def _prepare_predictions( image_predictions.data is not None and "area" in image_predictions.data ): - area = float(image_predictions.data["area"][pred_idx]) + area_data: npt.NDArray[np.float32] = np.asarray( + image_predictions.data["area"], dtype=np.float32 + ) + area = float(area_data[pred_idx]) if area is None: area = xywh[2] * xywh[3] - dict_prediction = { + dict_prediction: _TypeCocoDict = { "image_id": image_id, "bbox": xywh, "score": score, @@ -1481,38 +1540,54 @@ def compute(self) -> MeanAveragePrecisionResult: mAP_small = MeanAveragePrecisionResult( metric_target=self._metric_target, is_class_agnostic=self._class_agnostic, - mAP_scores=cocoEval.results["mAP_scores_small"], - ap_per_class=cocoEval.results["ap_per_class_small"], - iou_thresholds=cocoEval.params.iou_thrs, - matched_classes=np.array(cocoEval.params.cat_ids), + mAP_scores=np.asarray( + cocoEval.results["mAP_scores_small"], dtype=np.float64 + ), + ap_per_class=np.asarray( + cocoEval.results["ap_per_class_small"], dtype=np.float64 + ), + iou_thresholds=np.asarray(cocoEval.params.iou_thrs, dtype=np.float64), + matched_classes=np.asarray(cocoEval.params.cat_ids, dtype=np.int32), ) # Create MeanAveragePrecisionResult object for medium objects mAP_medium = MeanAveragePrecisionResult( metric_target=self._metric_target, is_class_agnostic=self._class_agnostic, - mAP_scores=cocoEval.results["mAP_scores_medium"], - ap_per_class=cocoEval.results["ap_per_class_medium"], - iou_thresholds=cocoEval.params.iou_thrs, - matched_classes=np.array(cocoEval.params.cat_ids), + mAP_scores=np.asarray( + cocoEval.results["mAP_scores_medium"], dtype=np.float64 + ), + ap_per_class=np.asarray( + cocoEval.results["ap_per_class_medium"], dtype=np.float64 + ), + iou_thresholds=np.asarray(cocoEval.params.iou_thrs, dtype=np.float64), + matched_classes=np.asarray(cocoEval.params.cat_ids, dtype=np.int32), ) # Create MeanAveragePrecisionResult object for large objects mAP_large = MeanAveragePrecisionResult( metric_target=self._metric_target, is_class_agnostic=self._class_agnostic, - mAP_scores=cocoEval.results["mAP_scores_large"], - ap_per_class=cocoEval.results["ap_per_class_large"], - iou_thresholds=cocoEval.params.iou_thrs, - matched_classes=np.array(cocoEval.params.cat_ids), + mAP_scores=np.asarray( + cocoEval.results["mAP_scores_large"], dtype=np.float64 + ), + ap_per_class=np.asarray( + cocoEval.results["ap_per_class_large"], dtype=np.float64 + ), + iou_thresholds=np.asarray(cocoEval.params.iou_thrs, dtype=np.float64), + matched_classes=np.asarray(cocoEval.params.cat_ids, dtype=np.int32), ) # Create the final MeanAveragePrecisionResult object mAP_result = MeanAveragePrecisionResult( metric_target=self._metric_target, is_class_agnostic=self._class_agnostic, - mAP_scores=cocoEval.results["mAP_scores_all_sizes"], - ap_per_class=cocoEval.results["ap_per_class_all_sizes"], - iou_thresholds=cocoEval.params.iou_thrs, - matched_classes=np.array(cocoEval.params.cat_ids), + mAP_scores=np.asarray( + cocoEval.results["mAP_scores_all_sizes"], dtype=np.float64 + ), + ap_per_class=np.asarray( + cocoEval.results["ap_per_class_all_sizes"], dtype=np.float64 + ), + iou_thresholds=np.asarray(cocoEval.params.iou_thrs, dtype=np.float64), + matched_classes=np.asarray(cocoEval.params.cat_ids, dtype=np.int32), small_objects=mAP_small, medium_objects=mAP_medium, large_objects=mAP_large, diff --git a/src/supervision/metrics/mean_average_recall.py b/src/supervision/metrics/mean_average_recall.py index 01b80fd5e9..dae7fe0c89 100644 --- a/src/supervision/metrics/mean_average_recall.py +++ b/src/supervision/metrics/mean_average_recall.py @@ -2,7 +2,7 @@ from copy import deepcopy from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, cast import numpy as np import numpy.typing as npt @@ -26,6 +26,13 @@ if TYPE_CHECKING: import pandas as pd +MeanAverageRecallStats = tuple[ + npt.NDArray[np.bool_], + npt.NDArray[np.int32], + npt.NDArray[np.int32], + npt.NDArray[np.int32], +] + @dataclass class MeanAverageRecallResult: @@ -161,7 +168,7 @@ def to_pandas(self) -> pd.DataFrame: ensure_pandas_installed() import pandas as pd - pandas_data = { + pandas_data: dict[str, object] = { "mAR @ 1": self.mAR_at_1, "mAR @ 10": self.mAR_at_10, "mAR @ 100": self.mAR_at_100, @@ -256,7 +263,12 @@ def plot(self) -> None: plt.show() -class MeanAverageRecall(Metric): +class MeanAverageRecall( + Metric[ + [Detections | list[Detections], Detections | list[Detections]], + MeanAverageRecallResult, + ] +): """ Mean Average Recall (mAR) measures how well the model detects and retrieves relevant objects by averaging recall over multiple @@ -297,7 +309,7 @@ class MeanAverageRecall(Metric): def __init__( self, metric_target: MetricTarget = MetricTarget.BOXES, - ): + ) -> None: """ Initialize the Mean Average Recall metric. @@ -381,32 +393,62 @@ def compute(self) -> MeanAverageRecallResult: def _compute( self, predictions_list: list[Detections], targets_list: list[Detections] ) -> MeanAverageRecallResult: - iou_thresholds = np.linspace(0.5, 0.95, 10) - stats: list[Any] = [] + iou_thresholds = np.linspace(0.5, 0.95, 10, dtype=np.float32) + stats: list[MeanAverageRecallStats] = [] for predictions, targets in zip(predictions_list, targets_list): prediction_contents = self._detections_content(predictions) target_contents = self._detections_content(targets) if len(targets) > 0: + if predictions.class_id is None or targets.class_id is None: + raise ValueError( + "MeanAverageRecall metric requires `class_id` on both " + "predictions and targets." + ) if len(predictions) == 0: stats.append( ( np.zeros((0, iou_thresholds.size), dtype=bool), - np.zeros((0,), dtype=int), - np.zeros((0,), dtype=int), - targets.class_id, + np.zeros((0,), dtype=np.int32), + np.zeros((0,), dtype=np.int32), + np.asarray(targets.class_id, dtype=np.int32), ) ) else: + if predictions.confidence is None: + raise ValueError( + "MeanAverageRecall metric requires `confidence` on " + "predictions." + ) + prediction_class_ids = np.asarray( + predictions.class_id, dtype=np.int32 + ) + target_class_ids = np.asarray(targets.class_id, dtype=np.int32) if self._metric_target == MetricTarget.BOXES: - iou = box_iou_batch(target_contents, prediction_contents) + iou: npt.NDArray[np.float64] = np.asarray( + box_iou_batch( + np.asarray(target_contents, dtype=np.float32), + np.asarray(prediction_contents, dtype=np.float32), + ), + dtype=np.float64, + ) elif self._metric_target == MetricTarget.MASKS: - iou = mask_iou_batch(target_contents, prediction_contents) + iou = np.asarray( + mask_iou_batch( + np.asarray(target_contents, dtype=bool), + np.asarray(prediction_contents, dtype=bool), + ), + dtype=np.float64, + ) elif self._metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES: - iou = oriented_box_iou_batch( - target_contents, prediction_contents + iou = np.asarray( + oriented_box_iou_batch( + np.asarray(target_contents, dtype=np.float32), + np.asarray(prediction_contents, dtype=np.float32), + ), + dtype=np.float64, ) else: raise ValueError( @@ -414,27 +456,21 @@ def _compute( ) matches = self._match_detection_batch( - predictions.class_id - if predictions.class_id is not None - else np.array([]), - targets.class_id - if targets.class_id is not None - else np.array([]), + prediction_class_ids, + target_class_ids, iou, iou_thresholds, ) sorted_indices = np.argsort( - -cast(npt.NDArray[np.float32], predictions.confidence) + -np.asarray(predictions.confidence, dtype=np.float32) ) stats.append( ( matches[sorted_indices], - np.arange(len(predictions)), - cast(npt.NDArray[np.int32], predictions.class_id)[ - sorted_indices - ], - cast(npt.NDArray[np.int32], targets.class_id), + np.arange(len(predictions), dtype=np.int32), + prediction_class_ids[sorted_indices], + target_class_ids, ) ) @@ -445,7 +481,7 @@ def _compute( recall_per_class=np.zeros((0, iou_thresholds.shape[0])), max_detections=self.max_detections, iou_thresholds=iou_thresholds, - matched_classes=np.array([], dtype=int), + matched_classes=np.array([], dtype=np.int32), small_objects=None, medium_objects=None, large_objects=None, @@ -480,8 +516,10 @@ def _compute_average_recall_for_classes( npt.NDArray[np.int32], ]: unique_classes, class_counts = np.unique(true_class_ids, return_counts=True) + unique_classes = np.asarray(unique_classes, dtype=np.int32) + class_counts = np.asarray(class_counts, dtype=np.int32) - recalls_at_k = [] + recalls_at_k_list: list[npt.NDArray[np.float64]] = [] for max_detections in self.max_detections: # Shape: PxTh,P,C,C -> CxThx3 confusion_matrix = self._compute_confusion_matrix( @@ -493,10 +531,10 @@ def _compute_average_recall_for_classes( # Shape: CxThx3 -> CxTh recall_per_class = self._compute_recall(confusion_matrix) - recalls_at_k.append(recall_per_class) + recalls_at_k_list.append(recall_per_class) # Shape: KxCxTh -> KxC - recalls_at_k = np.array(recalls_at_k) + recalls_at_k = np.array(recalls_at_k_list, dtype=np.float64) average_recall_per_class = np.mean(recalls_at_k, axis=2) # Shape: KxC -> K @@ -508,7 +546,7 @@ def _compute_average_recall_for_classes( def _match_detection_batch( predictions_classes: npt.NDArray[np.int32], target_classes: npt.NDArray[np.int32], - iou: npt.NDArray[np.float32], + iou: npt.NDArray[np.floating], iou_thresholds: npt.NDArray[np.float32], ) -> npt.NDArray[np.bool_]: num_predictions, num_iou_levels = ( @@ -538,9 +576,9 @@ def _match_detection_batch( @staticmethod def _compute_confusion_matrix( sorted_matches: npt.NDArray[np.bool_], - sorted_prediction_class_ids: npt.NDArray[np.int32], - unique_classes: npt.NDArray[np.int32], - class_counts: npt.NDArray[np.int32], + sorted_prediction_class_ids: npt.NDArray[np.integer], + unique_classes: npt.NDArray[np.integer], + class_counts: npt.NDArray[np.integer], ) -> npt.NDArray[np.float64]: """ Compute the confusion matrix for each class and IoU threshold. @@ -577,13 +615,15 @@ class ids. num_predictions = is_class.sum() if num_predictions == 0: - true_positives = np.zeros(num_thresholds) - false_positives = np.zeros(num_thresholds) - false_negatives = np.full(num_thresholds, num_true) + true_positives = np.zeros(num_thresholds, dtype=np.float64) + false_positives = np.zeros(num_thresholds, dtype=np.float64) + false_negatives = np.full(num_thresholds, num_true, dtype=np.float64) elif num_true == 0: - true_positives = np.zeros(num_thresholds) - false_positives = np.full(num_thresholds, num_predictions) - false_negatives = np.zeros(num_thresholds) + true_positives = np.zeros(num_thresholds, dtype=np.float64) + false_positives = np.full( + num_thresholds, num_predictions, dtype=np.float64 + ) + false_negatives = np.zeros(num_thresholds, dtype=np.float64) else: limited_matches = sorted_matches[is_class] true_positives = limited_matches.sum(0) @@ -626,14 +666,18 @@ def _compute_recall( result_recall: npt.NDArray[np.float64] = recall return result_recall - def _detections_content(self, detections: Detections) -> npt.NDArray[Any]: + def _detections_content(self, detections: Detections) -> npt.NDArray[np.generic]: """Return boxes, masks or oriented bounding boxes from detections.""" if self._metric_target == MetricTarget.BOXES: - result_boxes: npt.NDArray[np.float32] = detections.xyxy + result_boxes: npt.NDArray[np.float32] = np.asarray( + detections.xyxy, dtype=np.float32 + ) return result_boxes if self._metric_target == MetricTarget.MASKS: if detections.mask is not None: - result_masks: npt.NDArray[np.bool_] = detections.mask + result_masks: npt.NDArray[np.bool_] = np.asarray( + detections.mask, dtype=bool + ) return result_masks return self._make_empty_content() if self._metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES: @@ -644,7 +688,7 @@ def _detections_content(self, detections: Detections) -> npt.NDArray[Any]: return self._make_empty_content() raise ValueError(f"Invalid metric target: {self._metric_target}") - def _make_empty_content(self) -> npt.NDArray[Any]: + def _make_empty_content(self) -> npt.NDArray[np.generic]: if self._metric_target == MetricTarget.BOXES: empty_boxes: npt.NDArray[np.float32] = np.empty((0, 4), dtype=np.float32) return empty_boxes @@ -670,18 +714,30 @@ def _filter_detections_by_size( sizes = get_detection_size_category(new_detections, self._metric_target) size_mask = sizes == size_category.value - new_detections.xyxy = new_detections.xyxy[size_mask] + new_detections.xyxy = cast( + npt.NDArray[np.number], new_detections.xyxy[size_mask] + ) if new_detections.mask is not None: - new_detections.mask = new_detections.mask[size_mask] + new_detections.mask = cast( + npt.NDArray[np.bool_], new_detections.mask[size_mask] + ) if new_detections.class_id is not None: - new_detections.class_id = new_detections.class_id[size_mask] + new_detections.class_id = cast( + npt.NDArray[np.int32], new_detections.class_id[size_mask] + ) if new_detections.confidence is not None: - new_detections.confidence = new_detections.confidence[size_mask] + new_detections.confidence = cast( + npt.NDArray[np.float32], new_detections.confidence[size_mask] + ) if new_detections.tracker_id is not None: - new_detections.tracker_id = new_detections.tracker_id[size_mask] + new_detections.tracker_id = cast( + npt.NDArray[np.int32], new_detections.tracker_id[size_mask] + ) if new_detections.data is not None: for key, value in new_detections.data.items(): - new_detections.data[key] = np.array(value)[size_mask] + new_detections.data[key] = cast( + npt.NDArray[np.generic], np.array(value)[size_mask] + ) return new_detections diff --git a/src/supervision/metrics/precision.py b/src/supervision/metrics/precision.py index 223bfdbc3d..bebbd67049 100644 --- a/src/supervision/metrics/precision.py +++ b/src/supervision/metrics/precision.py @@ -2,7 +2,7 @@ from copy import deepcopy from dataclasses import dataclass -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, cast import numpy as np import numpy.typing as npt @@ -26,8 +26,20 @@ if TYPE_CHECKING: import pandas as pd +PrecisionStats = tuple[ + npt.NDArray[np.bool_], + npt.NDArray[np.float32], + npt.NDArray[np.int32], + npt.NDArray[np.int32], +] -class Precision(Metric): + +class Precision( + Metric[ + [Detections | list[Detections], Detections | list[Detections]], + "PrecisionResult", + ] +): """ Precision is a metric used to evaluate object detection models. It is the ratio of true positive detections to the total number of predicted detections. We calculate @@ -70,7 +82,7 @@ def __init__( self, metric_target: MetricTarget = MetricTarget.BOXES, averaging_method: AveragingMethod = AveragingMethod.WEIGHTED, - ): + ) -> None: """ Initialize the Precision metric. @@ -155,32 +167,61 @@ def compute(self) -> PrecisionResult: def _compute( self, predictions_list: list[Detections], targets_list: list[Detections] ) -> PrecisionResult: - iou_thresholds = np.linspace(0.5, 0.95, 10) - stats: list[Any] = [] + iou_thresholds = np.linspace(0.5, 0.95, 10, dtype=np.float32) + stats: list[PrecisionStats] = [] for predictions, targets in zip(predictions_list, targets_list): prediction_contents = self._detections_content(predictions) target_contents = self._detections_content(targets) if len(targets) > 0: + if predictions.class_id is None or targets.class_id is None: + raise ValueError( + "Precision metric requires `class_id` on both predictions " + "and targets." + ) if len(predictions) == 0: stats.append( ( np.zeros((0, iou_thresholds.size), dtype=bool), np.zeros((0,), dtype=np.float32), - np.zeros((0,), dtype=int), - targets.class_id, + np.zeros((0,), dtype=np.int32), + np.asarray(targets.class_id, dtype=np.int32), ) ) else: + if predictions.confidence is None: + raise ValueError( + "Precision metric requires `confidence` on predictions." + ) + prediction_class_ids = np.asarray( + predictions.class_id, dtype=np.int32 + ) + target_class_ids = np.asarray(targets.class_id, dtype=np.int32) if self._metric_target == MetricTarget.BOXES: - iou = box_iou_batch(target_contents, prediction_contents) + iou: npt.NDArray[np.float64] = np.asarray( + box_iou_batch( + np.asarray(target_contents, dtype=np.float32), + np.asarray(prediction_contents, dtype=np.float32), + ), + dtype=np.float64, + ) elif self._metric_target == MetricTarget.MASKS: - iou = mask_iou_batch(target_contents, prediction_contents) + iou = np.asarray( + mask_iou_batch( + np.asarray(target_contents, dtype=bool), + np.asarray(prediction_contents, dtype=bool), + ), + dtype=np.float64, + ) elif self._metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES: - iou = oriented_box_iou_batch( - target_contents, prediction_contents + iou = np.asarray( + oriented_box_iou_batch( + np.asarray(target_contents, dtype=np.float32), + np.asarray(prediction_contents, dtype=np.float32), + ), + dtype=np.float64, ) else: raise ValueError( @@ -188,21 +229,17 @@ def _compute( ) matches = self._match_detection_batch( - predictions.class_id - if predictions.class_id is not None - else np.array([]), - targets.class_id - if targets.class_id is not None - else np.array([]), + prediction_class_ids, + target_class_ids, iou, iou_thresholds, ) stats.append( ( matches, - predictions.confidence, - predictions.class_id, - targets.class_id, + np.asarray(predictions.confidence, dtype=np.float32), + prediction_class_ids, + target_class_ids, ) ) @@ -213,7 +250,7 @@ def _compute( precision_scores=np.zeros(iou_thresholds.shape[0]), precision_per_class=np.zeros((0, iou_thresholds.shape[0])), iou_thresholds=iou_thresholds, - matched_classes=np.array([], dtype=int), + matched_classes=np.array([], dtype=np.int32), small_objects=None, medium_objects=None, large_objects=None, @@ -251,6 +288,8 @@ def _compute_precision_for_classes( matches = matches[sorted_indices] prediction_class_ids = prediction_class_ids[sorted_indices] unique_classes, class_counts = np.unique(true_class_ids, return_counts=True) + unique_classes = np.asarray(unique_classes, dtype=np.int32) + class_counts = np.asarray(class_counts, dtype=np.int32) # Shape: PxTh,P,C,C -> CxThx3 confusion_matrix = self._compute_confusion_matrix( @@ -278,7 +317,7 @@ def _compute_precision_for_classes( def _match_detection_batch( predictions_classes: npt.NDArray[np.int32], target_classes: npt.NDArray[np.int32], - iou: npt.NDArray[np.float32], + iou: npt.NDArray[np.floating], iou_thresholds: npt.NDArray[np.float32], ) -> npt.NDArray[np.bool_]: num_predictions, num_iou_levels = ( @@ -308,9 +347,9 @@ def _match_detection_batch( @staticmethod def _compute_confusion_matrix( sorted_matches: npt.NDArray[np.bool_], - sorted_prediction_class_ids: npt.NDArray[np.int32], - unique_classes: npt.NDArray[np.int32], - class_counts: npt.NDArray[np.int32], + sorted_prediction_class_ids: npt.NDArray[np.integer], + unique_classes: npt.NDArray[np.integer], + class_counts: npt.NDArray[np.integer], ) -> npt.NDArray[np.float64]: """ Compute the confusion matrix for each class and IoU threshold. @@ -345,13 +384,15 @@ class ids. num_predictions = is_class.sum() if num_predictions == 0: - true_positives = np.zeros(num_thresholds) - false_positives = np.zeros(num_thresholds) - false_negatives = np.full(num_thresholds, num_true) + true_positives = np.zeros(num_thresholds, dtype=np.float64) + false_positives = np.zeros(num_thresholds, dtype=np.float64) + false_negatives = np.full(num_thresholds, num_true, dtype=np.float64) elif num_true == 0: - true_positives = np.zeros(num_thresholds) - false_positives = np.full(num_thresholds, num_predictions) - false_negatives = np.zeros(num_thresholds) + true_positives = np.zeros(num_thresholds, dtype=np.float64) + false_positives = np.full( + num_thresholds, num_predictions, dtype=np.float64 + ) + false_negatives = np.zeros(num_thresholds, dtype=np.float64) else: true_positives = sorted_matches[is_class].sum(0) false_positives = (1 - sorted_matches[is_class]).sum(0) @@ -395,14 +436,18 @@ def _compute_precision( result_precision: npt.NDArray[np.float64] = precision return result_precision - def _detections_content(self, detections: Detections) -> npt.NDArray[Any]: + def _detections_content(self, detections: Detections) -> npt.NDArray[np.generic]: """Return boxes, masks or oriented bounding boxes from detections.""" if self._metric_target == MetricTarget.BOXES: - result_boxes: npt.NDArray[np.float32] = detections.xyxy + result_boxes: npt.NDArray[np.float32] = np.asarray( + detections.xyxy, dtype=np.float32 + ) return result_boxes if self._metric_target == MetricTarget.MASKS: if detections.mask is not None: - result_masks: npt.NDArray[np.bool_] = detections.mask + result_masks: npt.NDArray[np.bool_] = np.asarray( + detections.mask, dtype=bool + ) return result_masks return self._make_empty_content() if self._metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES: @@ -413,7 +458,7 @@ def _detections_content(self, detections: Detections) -> npt.NDArray[Any]: return self._make_empty_content() raise ValueError(f"Invalid metric target: {self._metric_target}") - def _make_empty_content(self) -> npt.NDArray[Any]: + def _make_empty_content(self) -> npt.NDArray[np.generic]: if self._metric_target == MetricTarget.BOXES: empty_boxes: npt.NDArray[np.float32] = np.empty((0, 4), dtype=np.float32) return empty_boxes @@ -441,18 +486,30 @@ def _filter_detections_by_size( sizes = get_detection_size_category(new_detections, self._metric_target) size_mask = sizes == size_category.value - new_detections.xyxy = new_detections.xyxy[size_mask] + new_detections.xyxy = cast( + npt.NDArray[np.number], new_detections.xyxy[size_mask] + ) if new_detections.mask is not None: - new_detections.mask = new_detections.mask[size_mask] + new_detections.mask = cast( + npt.NDArray[np.bool_], new_detections.mask[size_mask] + ) if new_detections.class_id is not None: - new_detections.class_id = new_detections.class_id[size_mask] + new_detections.class_id = cast( + npt.NDArray[np.int32], new_detections.class_id[size_mask] + ) if new_detections.confidence is not None: - new_detections.confidence = new_detections.confidence[size_mask] + new_detections.confidence = cast( + npt.NDArray[np.float32], new_detections.confidence[size_mask] + ) if new_detections.tracker_id is not None: - new_detections.tracker_id = new_detections.tracker_id[size_mask] + new_detections.tracker_id = cast( + npt.NDArray[np.int32], new_detections.tracker_id[size_mask] + ) if new_detections.data is not None: for key, value in new_detections.data.items(): - new_detections.data[key] = np.array(value)[size_mask] + new_detections.data[key] = cast( + npt.NDArray[np.generic], np.asarray(value)[size_mask] + ) return new_detections @@ -608,7 +665,7 @@ def to_pandas(self) -> pd.DataFrame: ensure_pandas_installed() import pandas as pd - pandas_data = { + pandas_data: dict[str, object] = { "P@50": self.precision_at_50, "P@75": self.precision_at_75, } diff --git a/src/supervision/metrics/recall.py b/src/supervision/metrics/recall.py index 2d428cbbf5..409e5c4d4e 100644 --- a/src/supervision/metrics/recall.py +++ b/src/supervision/metrics/recall.py @@ -2,7 +2,7 @@ from copy import deepcopy from dataclasses import dataclass -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, cast import numpy as np import numpy.typing as npt @@ -26,8 +26,20 @@ if TYPE_CHECKING: import pandas as pd +RecallStats = tuple[ + npt.NDArray[np.bool_], + npt.NDArray[np.float32], + npt.NDArray[np.int32], + npt.NDArray[np.int32], +] -class Recall(Metric): + +class Recall( + Metric[ + [Detections | list[Detections], Detections | list[Detections]], + "RecallResult", + ] +): """ Recall is a metric used to evaluate object detection models. It is the ratio of true positive detections to the total number of ground truth instances. We calculate @@ -70,7 +82,7 @@ def __init__( self, metric_target: MetricTarget = MetricTarget.BOXES, averaging_method: AveragingMethod = AveragingMethod.WEIGHTED, - ): + ) -> None: """ Initialize the Recall metric. @@ -155,32 +167,61 @@ def compute(self) -> RecallResult: def _compute( self, predictions_list: list[Detections], targets_list: list[Detections] ) -> RecallResult: - iou_thresholds = np.linspace(0.5, 0.95, 10) - stats: list[Any] = [] + iou_thresholds = np.linspace(0.5, 0.95, 10, dtype=np.float32) + stats: list[RecallStats] = [] for predictions, targets in zip(predictions_list, targets_list): prediction_contents = self._detections_content(predictions) target_contents = self._detections_content(targets) if len(targets) > 0: + if predictions.class_id is None or targets.class_id is None: + raise ValueError( + "Recall metric requires `class_id` on both predictions " + "and targets." + ) if len(predictions) == 0: stats.append( ( np.zeros((0, iou_thresholds.size), dtype=bool), np.zeros((0,), dtype=np.float32), - np.zeros((0,), dtype=int), - targets.class_id, + np.zeros((0,), dtype=np.int32), + np.asarray(targets.class_id, dtype=np.int32), ) ) else: + if predictions.confidence is None: + raise ValueError( + "Recall metric requires `confidence` on predictions." + ) + prediction_class_ids = np.asarray( + predictions.class_id, dtype=np.int32 + ) + target_class_ids = np.asarray(targets.class_id, dtype=np.int32) if self._metric_target == MetricTarget.BOXES: - iou = box_iou_batch(target_contents, prediction_contents) + iou: npt.NDArray[np.float64] = np.asarray( + box_iou_batch( + np.asarray(target_contents, dtype=np.float32), + np.asarray(prediction_contents, dtype=np.float32), + ), + dtype=np.float64, + ) elif self._metric_target == MetricTarget.MASKS: - iou = mask_iou_batch(target_contents, prediction_contents) + iou = np.asarray( + mask_iou_batch( + np.asarray(target_contents, dtype=bool), + np.asarray(prediction_contents, dtype=bool), + ), + dtype=np.float64, + ) elif self._metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES: - iou = oriented_box_iou_batch( - target_contents, prediction_contents + iou = np.asarray( + oriented_box_iou_batch( + np.asarray(target_contents, dtype=np.float32), + np.asarray(prediction_contents, dtype=np.float32), + ), + dtype=np.float64, ) else: raise ValueError( @@ -188,21 +229,17 @@ def _compute( ) matches = self._match_detection_batch( - predictions.class_id - if predictions.class_id is not None - else np.array([]), - targets.class_id - if targets.class_id is not None - else np.array([]), + prediction_class_ids, + target_class_ids, iou, iou_thresholds, ) stats.append( ( matches, - predictions.confidence, - predictions.class_id, - targets.class_id, + np.asarray(predictions.confidence, dtype=np.float32), + prediction_class_ids, + target_class_ids, ) ) @@ -213,7 +250,7 @@ def _compute( recall_scores=np.zeros(iou_thresholds.shape[0]), recall_per_class=np.zeros((0, iou_thresholds.shape[0])), iou_thresholds=iou_thresholds, - matched_classes=np.array([], dtype=int), + matched_classes=np.array([], dtype=np.int32), small_objects=None, medium_objects=None, large_objects=None, @@ -251,6 +288,8 @@ def _compute_recall_for_classes( matches = matches[sorted_indices] prediction_class_ids = prediction_class_ids[sorted_indices] unique_classes, class_counts = np.unique(true_class_ids, return_counts=True) + unique_classes = np.asarray(unique_classes, dtype=np.int32) + class_counts = np.asarray(class_counts, dtype=np.int32) # Shape: PxTh,P,C,C -> CxThx3 confusion_matrix = self._compute_confusion_matrix( @@ -276,7 +315,7 @@ def _compute_recall_for_classes( def _match_detection_batch( predictions_classes: npt.NDArray[np.int32], target_classes: npt.NDArray[np.int32], - iou: npt.NDArray[np.float32], + iou: npt.NDArray[np.floating], iou_thresholds: npt.NDArray[np.float32], ) -> npt.NDArray[np.bool_]: num_predictions, num_iou_levels = ( @@ -306,9 +345,9 @@ def _match_detection_batch( @staticmethod def _compute_confusion_matrix( sorted_matches: npt.NDArray[np.bool_], - sorted_prediction_class_ids: npt.NDArray[np.int32], - unique_classes: npt.NDArray[np.int32], - class_counts: npt.NDArray[np.int32], + sorted_prediction_class_ids: npt.NDArray[np.integer], + unique_classes: npt.NDArray[np.integer], + class_counts: npt.NDArray[np.integer], ) -> npt.NDArray[np.float64]: """ Compute the confusion matrix for each class and IoU threshold. @@ -343,13 +382,15 @@ class ids. num_predictions = is_class.sum() if num_predictions == 0: - true_positives = np.zeros(num_thresholds) - false_positives = np.zeros(num_thresholds) - false_negatives = np.full(num_thresholds, num_true) + true_positives = np.zeros(num_thresholds, dtype=np.float64) + false_positives = np.zeros(num_thresholds, dtype=np.float64) + false_negatives = np.full(num_thresholds, num_true, dtype=np.float64) elif num_true == 0: - true_positives = np.zeros(num_thresholds) - false_positives = np.full(num_thresholds, num_predictions) - false_negatives = np.zeros(num_thresholds) + true_positives = np.zeros(num_thresholds, dtype=np.float64) + false_positives = np.full( + num_thresholds, num_predictions, dtype=np.float64 + ) + false_negatives = np.zeros(num_thresholds, dtype=np.float64) else: true_positives = sorted_matches[is_class].sum(0) false_positives = (1 - sorted_matches[is_class]).sum(0) @@ -393,14 +434,18 @@ def _compute_recall( result_recall: npt.NDArray[np.float64] = recall return result_recall - def _detections_content(self, detections: Detections) -> npt.NDArray[Any]: + def _detections_content(self, detections: Detections) -> npt.NDArray[np.generic]: """Return boxes, masks or oriented bounding boxes from detections.""" if self._metric_target == MetricTarget.BOXES: - result_boxes: npt.NDArray[np.float32] = detections.xyxy + result_boxes: npt.NDArray[np.float32] = np.asarray( + detections.xyxy, dtype=np.float32 + ) return result_boxes if self._metric_target == MetricTarget.MASKS: if detections.mask is not None: - result_masks: npt.NDArray[np.bool_] = detections.mask + result_masks: npt.NDArray[np.bool_] = np.asarray( + detections.mask, dtype=bool + ) return result_masks return self._make_empty_content() if self._metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES: @@ -411,7 +456,7 @@ def _detections_content(self, detections: Detections) -> npt.NDArray[Any]: return self._make_empty_content() raise ValueError(f"Invalid metric target: {self._metric_target}") - def _make_empty_content(self) -> npt.NDArray[Any]: + def _make_empty_content(self) -> npt.NDArray[np.generic]: if self._metric_target == MetricTarget.BOXES: empty_boxes: npt.NDArray[np.float32] = np.empty((0, 4), dtype=np.float32) return empty_boxes @@ -439,18 +484,30 @@ def _filter_detections_by_size( sizes = get_detection_size_category(new_detections, self._metric_target) size_mask = sizes == size_category.value - new_detections.xyxy = new_detections.xyxy[size_mask] + new_detections.xyxy = cast( + npt.NDArray[np.number], new_detections.xyxy[size_mask] + ) if new_detections.mask is not None: - new_detections.mask = new_detections.mask[size_mask] + new_detections.mask = cast( + npt.NDArray[np.bool_], new_detections.mask[size_mask] + ) if new_detections.class_id is not None: - new_detections.class_id = new_detections.class_id[size_mask] + new_detections.class_id = cast( + npt.NDArray[np.int32], new_detections.class_id[size_mask] + ) if new_detections.confidence is not None: - new_detections.confidence = new_detections.confidence[size_mask] + new_detections.confidence = cast( + npt.NDArray[np.float32], new_detections.confidence[size_mask] + ) if new_detections.tracker_id is not None: - new_detections.tracker_id = new_detections.tracker_id[size_mask] + new_detections.tracker_id = cast( + npt.NDArray[np.int32], new_detections.tracker_id[size_mask] + ) if new_detections.data is not None: for key, value in new_detections.data.items(): - new_detections.data[key] = np.array(value)[size_mask] + new_detections.data[key] = cast( + npt.NDArray[np.generic], np.asarray(value)[size_mask] + ) return new_detections @@ -604,7 +661,7 @@ def to_pandas(self) -> pd.DataFrame: ensure_pandas_installed() import pandas as pd - pandas_data = { + pandas_data: dict[str, object] = { "R@50": self.recall_at_50, "R@75": self.recall_at_75, } diff --git a/src/supervision/metrics/utils/object_size.py b/src/supervision/metrics/utils/object_size.py index 03f005a112..131dd5e41f 100644 --- a/src/supervision/metrics/utils/object_size.py +++ b/src/supervision/metrics/utils/object_size.py @@ -1,7 +1,7 @@ from __future__ import annotations from enum import Enum -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import numpy as np import numpy.typing as npt @@ -44,7 +44,8 @@ class ObjectSizeCategory(Enum): def get_object_size_category( - data: npt.NDArray, metric_target: MetricTarget + data: npt.NDArray[np.number] | npt.NDArray[np.bool_], + metric_target: MetricTarget, ) -> npt.NDArray[np.int_]: """ Get the size category of an object. Distinguish based on the metric target. @@ -74,15 +75,18 @@ def get_object_size_category( ``` """ if metric_target == MetricTarget.BOXES: - return get_bbox_size_category(data) + bbox_data = cast(npt.NDArray[np.number], data) + return get_bbox_size_category(bbox_data) if metric_target == MetricTarget.MASKS: - return get_mask_size_category(data) + mask_data = cast(npt.NDArray[np.bool_], data) + return get_mask_size_category(mask_data) if metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES: - return get_obb_size_category(data) + obb_data = cast(npt.NDArray[np.number], data) + return get_obb_size_category(obb_data) raise ValueError("Invalid metric type") -def get_bbox_size_category(xyxy: npt.NDArray[np.float32]) -> npt.NDArray[np.int_]: +def get_bbox_size_category(xyxy: npt.NDArray[np.number]) -> npt.NDArray[np.int_]: """ Get the size category of a bounding boxes array. @@ -165,7 +169,7 @@ def get_mask_size_category( return result -def get_obb_size_category(xyxyxyxy: npt.NDArray[np.float32]) -> npt.NDArray[np.int_]: +def get_obb_size_category(xyxyxyxy: npt.NDArray[np.number]) -> npt.NDArray[np.int_]: """ Get the size category of a oriented bounding boxes array. @@ -229,13 +233,18 @@ def get_detection_size_category( if metric_target == MetricTarget.BOXES: return get_bbox_size_category(detections.xyxy) if metric_target == MetricTarget.MASKS: - if detections.mask is None: + mask = detections.mask + if mask is None: raise ValueError("Detections mask is not available") - return get_mask_size_category(detections.mask) + return get_mask_size_category(mask) if metric_target == MetricTarget.ORIENTED_BOUNDING_BOXES: - if detections.data.get(ORIENTED_BOX_COORDINATES) is None: + oriented_box_coordinates = detections.data.get(ORIENTED_BOX_COORDINATES) + if oriented_box_coordinates is None: raise ValueError("Detections oriented bounding boxes are not available") return get_obb_size_category( - np.array(detections.data[ORIENTED_BOX_COORDINATES]) + cast( + npt.NDArray[np.number], + np.asarray(oriented_box_coordinates, dtype=np.float32), + ) ) raise ValueError("Invalid metric type") diff --git a/src/supervision/tracker/byte_tracker/core.py b/src/supervision/tracker/byte_tracker/core.py index 3b84c2af77..66dfbb1051 100644 --- a/src/supervision/tracker/byte_tracker/core.py +++ b/src/supervision/tracker/byte_tracker/core.py @@ -60,7 +60,7 @@ def __init__( minimum_matching_threshold: float = 0.8, frame_rate: float = 30, minimum_consecutive_frames: int = 1, - ): + ) -> None: self.track_activation_threshold = track_activation_threshold self.minimum_matching_threshold = minimum_matching_threshold diff --git a/src/supervision/tracker/byte_tracker/kalman_filter.py b/src/supervision/tracker/byte_tracker/kalman_filter.py index fbaf779c41..45b713b4cd 100644 --- a/src/supervision/tracker/byte_tracker/kalman_filter.py +++ b/src/supervision/tracker/byte_tracker/kalman_filter.py @@ -146,10 +146,10 @@ def multi_predict( ] sqr = np.square(np.r_[std_pos, std_vel]).T - motion_cov = [] + motion_cov_list: list[npt.NDArray[np.float32]] = [] for i in range(len(mean)): - motion_cov.append(np.diag(sqr[i])) - motion_cov = np.asarray(motion_cov) + motion_cov_list.append(np.diag(sqr[i])) + motion_cov = np.asarray(motion_cov_list) mean = np.dot(mean, self._motion_mat.T) left = np.dot(self._motion_mat, covariance).transpose((1, 0, 2)) diff --git a/src/supervision/tracker/byte_tracker/matching.py b/src/supervision/tracker/byte_tracker/matching.py index 9d2c7dc690..729376926f 100644 --- a/src/supervision/tracker/byte_tracker/matching.py +++ b/src/supervision/tracker/byte_tracker/matching.py @@ -1,15 +1,13 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import cast import numpy as np import numpy.typing as npt from scipy.optimize import linear_sum_assignment from supervision.detection.utils.iou_and_nms import box_iou_batch - -if TYPE_CHECKING: - from supervision.tracker.byte_tracker.single_object_track import STrack +from supervision.tracker.byte_tracker.single_object_track import STrack def indices_to_matches( @@ -48,17 +46,23 @@ def iou_distance( if (len(atracks) > 0 and isinstance(atracks[0], np.ndarray)) or ( len(btracks) > 0 and isinstance(btracks[0], np.ndarray) ): - atlbrs = atracks - btlbrs = btracks + atlbrs = cast(list[npt.NDArray[np.float32]], atracks) + btlbrs = cast(list[npt.NDArray[np.float32]], btracks) else: - atlbrs = [track.tlbr for track in atracks] - btlbrs = [track.tlbr for track in btracks] + atlbrs = [track.tlbr for track in cast(list[STrack], atracks)] + btlbrs = [track.tlbr for track in cast(list[STrack], btracks)] - _ious = np.zeros((len(atlbrs), len(btlbrs)), dtype=np.float32) - if _ious.size != 0: - _ious = box_iou_batch(np.asarray(atlbrs), np.asarray(btlbrs)) - cost_matrix = 1 - _ious + if len(atlbrs) == 0 or len(btlbrs) == 0: + return cast( + npt.NDArray[np.float32], + np.empty((len(atlbrs), len(btlbrs)), dtype=np.float32), + ) + ious = box_iou_batch( + np.asarray(atlbrs, dtype=np.float32), + np.asarray(btlbrs, dtype=np.float32), + ) + cost_matrix = np.asarray(1 - ious, dtype=np.float32) return cost_matrix @@ -68,8 +72,8 @@ def fuse_score( if cost_matrix.size == 0: return cost_matrix iou_sim = 1 - cost_matrix - det_scores = np.array([strack.score for strack in stracks]) + det_scores = np.array([strack.score for strack in stracks], dtype=np.float32) det_scores = np.expand_dims(det_scores, axis=0).repeat(cost_matrix.shape[0], axis=0) fuse_sim = iou_sim * det_scores - fuse_cost = 1 - fuse_sim + fuse_cost = np.asarray(1 - fuse_sim, dtype=np.float32) return fuse_cost diff --git a/src/supervision/tracker/byte_tracker/single_object_track.py b/src/supervision/tracker/byte_tracker/single_object_track.py index 26f6bb6831..084e2d257f 100644 --- a/src/supervision/tracker/byte_tracker/single_object_track.py +++ b/src/supervision/tracker/byte_tracker/single_object_track.py @@ -1,6 +1,7 @@ from __future__ import annotations from enum import Enum +from typing import cast import numpy as np import numpy.typing as npt @@ -25,7 +26,7 @@ def __init__( shared_kalman: KalmanFilter, internal_id_counter: IdCounter, external_id_counter: IdCounter, - ): + ) -> None: self.state = TrackState.New self.is_activated = False self.start_frame = 0 @@ -62,8 +63,8 @@ def predict(self) -> None: @staticmethod def multi_predict(stracks: list[STrack], shared_kalman: KalmanFilter) -> None: if len(stracks) > 0: - multi_mean = [] - multi_covariance = [] + multi_mean: list[npt.NDArray[np.float32]] = [] + multi_covariance: list[npt.NDArray[np.float32]] = [] for i, st in enumerate(stracks): assert st.mean is not None assert st.covariance is not None @@ -72,10 +73,10 @@ def multi_predict(stracks: list[STrack], shared_kalman: KalmanFilter) -> None: if st.state != TrackState.Tracked: multi_mean[i][7] = 0 - multi_mean, multi_covariance = shared_kalman.multi_predict( + multi_mean_arr, multi_covariance_arr = shared_kalman.multi_predict( np.asarray(multi_mean), np.asarray(multi_covariance) ) - for i, (mean, cov) in enumerate(zip(multi_mean, multi_covariance)): + for i, (mean, cov) in enumerate(zip(multi_mean_arr, multi_covariance_arr)): stracks[i].mean = mean stracks[i].covariance = cov @@ -142,11 +143,14 @@ def tlwh(self) -> npt.NDArray[np.float32]: width, height)`. """ if self.mean is None: - return self._tlwh.copy() + return cast( + npt.NDArray[np.float32], + np.asarray(self._tlwh.copy(), dtype=np.float32), + ) ret = self.mean[:4].copy() ret[2] *= ret[3] ret[:2] -= ret[2:] / 2 - return ret + return cast(npt.NDArray[np.float32], np.asarray(ret, dtype=np.float32)) @property def tlbr(self) -> npt.NDArray[np.float32]: @@ -155,7 +159,7 @@ def tlbr(self) -> npt.NDArray[np.float32]: """ ret = self.tlwh.copy() ret[2:] += ret[:2] - return ret + return cast(npt.NDArray[np.float32], np.asarray(ret, dtype=np.float32)) @staticmethod def tlwh_to_xyah(tlwh: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]: @@ -165,7 +169,7 @@ def tlwh_to_xyah(tlwh: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]: ret = np.asarray(tlwh).copy() ret[:2] += ret[2:] / 2 ret[2] /= ret[3] - return ret + return cast(npt.NDArray[np.float32], np.asarray(ret, dtype=np.float32)) def to_xyah(self) -> npt.NDArray[np.float32]: return self.tlwh_to_xyah(self.tlwh) @@ -174,13 +178,13 @@ def to_xyah(self) -> npt.NDArray[np.float32]: def tlbr_to_tlwh(tlbr: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]: ret = np.asarray(tlbr).copy() ret[2:] -= ret[:2] - return ret + return cast(npt.NDArray[np.float32], np.asarray(ret, dtype=np.float32)) @staticmethod def tlwh_to_tlbr(tlwh: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]: ret = np.asarray(tlwh).copy() ret[2:] += ret[:2] - return ret + return cast(npt.NDArray[np.float32], np.asarray(ret, dtype=np.float32)) def __repr__(self) -> str: return f"OT_{self.internal_track_id}_({self.start_frame}-{self.frame_id})" diff --git a/src/supervision/tracker/byte_tracker/utils.py b/src/supervision/tracker/byte_tracker/utils.py index 3404f8090e..f7d33738f5 100644 --- a/src/supervision/tracker/byte_tracker/utils.py +++ b/src/supervision/tracker/byte_tracker/utils.py @@ -2,7 +2,7 @@ class IdCounter: - def __init__(self, start_id: int = 0): + def __init__(self, start_id: int = 0) -> None: """ Initialize the ID counter. diff --git a/src/supervision/utils/conversion.py b/src/supervision/utils/conversion.py index 00a57818bf..1e46061cd0 100644 --- a/src/supervision/utils/conversion.py +++ b/src/supervision/utils/conversion.py @@ -1,6 +1,6 @@ import functools from collections.abc import Callable -from typing import Any, TypeVar, cast +from typing import TypeVar, cast import cv2 import numpy as np @@ -10,7 +10,7 @@ from supervision.draw.base import ImageType -F = TypeVar("F", bound=Callable[..., Any]) +F = TypeVar("F", bound=Callable[..., object]) def ensure_cv2_image_for_class_method( @@ -25,13 +25,25 @@ def ensure_cv2_image_for_class_method( """ @functools.wraps(annotate_func) - def wrapper(self: Any, scene: ImageType, *args: Any, **kwargs: Any) -> ImageType: + def wrapper( + self: object, + scene: ImageType, + *args: object, + **kwargs: object, + ) -> object: if isinstance(scene, np.ndarray): - return annotate_func(self, scene, *args, **kwargs) + annotated_np = cast( + npt.NDArray[np.uint8], + annotate_func(self, scene, *args, **kwargs), + ) + return annotated_np if isinstance(scene, Image.Image): scene_np = pillow_to_cv2(scene) - annotated_np = annotate_func(self, scene_np, *args, **kwargs) + annotated_np = cast( + npt.NDArray[np.uint8], + annotate_func(self, scene_np, *args, **kwargs), + ) scene.paste(cv2_to_pillow(annotated_np)) return scene @@ -62,13 +74,20 @@ def ensure_cv2_image_for_standalone_function( """ @functools.wraps(image_processing_fun) - def wrapper(image: ImageType, *args: Any, **kwargs: Any) -> ImageType: + def wrapper(image: ImageType, *args: object, **kwargs: object) -> object: if isinstance(image, np.ndarray): - return image_processing_fun(image, *args, **kwargs) + annotated_np = cast( + npt.NDArray[np.uint8], + image_processing_fun(image, *args, **kwargs), + ) + return annotated_np if isinstance(image, Image.Image): scene = pillow_to_cv2(image) - annotated = image_processing_fun(scene, *args, **kwargs) + annotated = cast( + npt.NDArray[np.uint8], + image_processing_fun(scene, *args, **kwargs), + ) return cv2_to_pillow(annotated) raise ValueError(f"Unsupported image type: {type(image)}") @@ -87,15 +106,22 @@ def ensure_pil_image_for_class_method( """ @functools.wraps(annotate_func) - def wrapper(self: Any, scene: ImageType, *args: Any, **kwargs: Any) -> ImageType: + def wrapper( + self: object, + scene: ImageType, + *args: object, + **kwargs: object, + ) -> object: if isinstance(scene, np.ndarray): scene_pil = cv2_to_pillow(scene) - annotated_pil = annotate_func(self, scene_pil, *args, **kwargs) + annotated_pil = cast( + Image.Image, annotate_func(self, scene_pil, *args, **kwargs) + ) np.copyto(scene, pillow_to_cv2(annotated_pil)) return scene if isinstance(scene, Image.Image): - return cast(ImageType, annotate_func(self, scene, *args, **kwargs)) + return cast(Image.Image, annotate_func(self, scene, *args, **kwargs)) raise ValueError(f"Unsupported image type: {type(scene)}") @@ -137,11 +163,12 @@ def images_to_cv2(images: list[ImageType]) -> list[npt.NDArray[np.uint8]]: (with order preserved). """ - result = [] + result: list[npt.NDArray[np.uint8]] = [] for image in images: - if issubclass(type(image), Image.Image): - image = pillow_to_cv2(image) - result.append(image) + if isinstance(image, Image.Image): + result.append(pillow_to_cv2(image)) + else: + result.append(image) return result @@ -156,9 +183,9 @@ def pillow_to_cv2(image: Image.Image) -> npt.NDArray[np.uint8]: Returns: Input image converted to OpenCV format. """ - scene = np.array(image) - scene = cv2.cvtColor(scene, cv2.COLOR_RGB2BGR) - return scene.astype(np.uint8) + scene: npt.NDArray[np.uint8] = cast(npt.NDArray[np.uint8], np.array(image)) + scene = cast(npt.NDArray[np.uint8], cv2.cvtColor(scene, cv2.COLOR_RGB2BGR)) + return cast(npt.NDArray[np.uint8], scene.astype(np.uint8)) def cv2_to_pillow(image: npt.NDArray[np.uint8]) -> Image.Image: @@ -172,5 +199,5 @@ def cv2_to_pillow(image: npt.NDArray[np.uint8]) -> Image.Image: Returns: Input image converted to Pillow format. """ - image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) - return Image.fromarray(image) + converted = cast(npt.NDArray[np.uint8], cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) + return Image.fromarray(converted) diff --git a/src/supervision/utils/image.py b/src/supervision/utils/image.py index f8010d5a7e..c142f40844 100644 --- a/src/supervision/utils/image.py +++ b/src/supervision/utils/image.py @@ -6,7 +6,8 @@ import shutil from collections.abc import Callable from functools import partial -from typing import Any, Literal, cast +from types import TracebackType +from typing import Literal, cast import cv2 import numpy as np @@ -33,7 +34,7 @@ @ensure_cv2_image_for_standalone_function def crop_image( image: ImageType, - xyxy: npt.NDArray[int] | list[int] | tuple[int, int, int, int], + xyxy: npt.NDArray[np.number] | list[int] | tuple[int, int, int, int], ) -> ImageType: """ Crop image based on bounding box coordinates. @@ -74,17 +75,14 @@ def crop_image( ![crop-image](https://media.roboflow.com/supervision-docs/supervision-docs-crop-image-2.png){ align=center width="1000" } """ # noqa E501 // docs - if isinstance(xyxy, (list, tuple)): - xyxy = np.array(xyxy) - - xyxy = np.round(xyxy).astype(int) - x_min, y_min, x_max, y_max = xyxy.flatten() + xyxy_arr = np.asarray(xyxy, dtype=np.float64).round().astype(np.int32) + x_min, y_min, x_max, y_max = xyxy_arr.flatten() if isinstance(image, np.ndarray): return image[y_min:y_max, x_min:x_max] if isinstance(image, Image.Image): - return image.crop((x_min, y_min, x_max, y_max)) + return image.crop((float(x_min), float(y_min), float(x_max), float(y_max))) raise TypeError( f"`image` must be a numpy.ndarray or PIL.Image.Image. Received {type(image)}" @@ -139,7 +137,10 @@ def scale_image(image: ImageType, scale_factor: float) -> ImageType: width_old, height_old = image.shape[1], image.shape[0] width_new = int(width_old * scale_factor) height_new = int(height_old * scale_factor) - return cv2.resize(image, (width_new, height_new), interpolation=cv2.INTER_LINEAR) + return cast( + npt.NDArray[np.uint8], + cv2.resize(image, (width_new, height_new), interpolation=cv2.INTER_LINEAR), + ) @ensure_cv2_image_for_standalone_function @@ -203,7 +204,10 @@ def resize_image( else: width_new, height_new = resolution_wh - return cv2.resize(image, (width_new, height_new), interpolation=cv2.INTER_LINEAR) + return cast( + npt.NDArray[np.uint8], + cv2.resize(image, (width_new, height_new), interpolation=cv2.INTER_LINEAR), + ) @ensure_cv2_image_for_standalone_function @@ -253,14 +257,17 @@ def letterbox_image( padding_bottom = resolution_wh[1] - height_new - padding_top padding_left = (resolution_wh[0] - width_new) // 2 padding_right = resolution_wh[0] - width_new - padding_left - image_with_borders = cv2.copyMakeBorder( - resized_image, - padding_top, - padding_bottom, - padding_left, - padding_right, - cv2.BORDER_CONSTANT, - value=color, + image_with_borders = cast( + npt.NDArray[np.uint8], + cv2.copyMakeBorder( + resized_image, + padding_top, + padding_bottom, + padding_left, + padding_right, + cv2.BORDER_CONSTANT, + value=color, + ), ) if image.shape[2] == 4: @@ -335,12 +342,12 @@ def overlay_image( b, g, r, alpha = cv2.split( overlay[crop_y_min:crop_y_max, crop_x_min:crop_x_max] ) - alpha = alpha[:, :, None] / 255.0 - overlay_color = cv2.merge((b, g, r)) + alpha_f32 = alpha[:, :, None].astype(np.float32) / 255.0 + overlay_color = cv2.merge((b, g, r)).astype(np.float32) - roi = image[y_min:y_max, x_min:x_max] - roi[:] = roi * (1 - alpha) + overlay_color * alpha - image[y_min:y_max, x_min:x_max] = roi + roi = image[y_min:y_max, x_min:x_max].astype(np.float32) + blended = roi * (1 - alpha_f32) + overlay_color * alpha_f32 + image[y_min:y_max, x_min:x_max] = np.clip(blended, 0, 255).astype(np.uint8) else: image[y_min:y_max, x_min:x_max] = overlay[ crop_y_min:crop_y_max, crop_x_min:crop_x_max @@ -424,8 +431,9 @@ def grayscale_image(image: ImageType) -> ImageType: ![grayscale-image](https://media.roboflow.com/supervision-docs/supervision-docs-grayscale-image-2.png){ align=center width="1000" } """ # noqa E501 // docs - grayscaled = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) - return cv2.cvtColor(grayscaled, cv2.COLOR_GRAY2BGR) + assert isinstance(image, np.ndarray) + grayscaled = cast(npt.NDArray[np.uint8], cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)) + return cast(npt.NDArray[np.uint8], cv2.cvtColor(grayscaled, cv2.COLOR_GRAY2BGR)) def get_image_resolution_wh(image: ImageType) -> tuple[int, int]: @@ -481,7 +489,7 @@ def __init__( target_dir_path: str, overwrite: bool = False, image_name_pattern: str = "image_{:05d}.png", - ): + ) -> None: """ Initialize context manager for saving images to directory. @@ -549,7 +557,7 @@ def __exit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, - exc_traceback: Any, + exc_traceback: TracebackType | None, ) -> None: pass @@ -653,32 +661,32 @@ def create_tiles( return_type = _negotiate_tiles_format(images=images) tile_padding_color = unify_to_bgr(color=tile_padding_color) tile_margin_color = unify_to_bgr(color=tile_margin_color) - images = images_to_cv2(images=images) + images_cv2 = images_to_cv2(images=images) if single_tile_size is None: - single_tile_size = _aggregate_images_shape(images=images, mode=tile_scaling) + single_tile_size = _aggregate_images_shape(images=images_cv2, mode=tile_scaling) resized_images = [ letterbox_image( image=i, resolution_wh=single_tile_size, color=tile_padding_color ) - for i in images + for i in images_cv2 ] - grid_size = _establish_grid_size(images=images, grid_size=grid_size) - if len(images) > grid_size[0] * grid_size[1]: + grid_size = _establish_grid_size(images=images_cv2, grid_size=grid_size) + if len(images_cv2) > grid_size[0] * grid_size[1]: raise ValueError( - f"Could not place {len(images)} in grid with size: {grid_size}." + f"Could not place {len(images_cv2)} in grid with size: {grid_size}." ) if titles is not None: - titles = fill(sequence=titles, desired_size=len(images), content=None) + titles = fill(sequence=titles, desired_size=len(images_cv2), content=None) if isinstance(titles_anchors, list): titles_anchors_sequence = titles_anchors else: titles_anchors_sequence = [titles_anchors] titles_anchors = fill( - sequence=titles_anchors_sequence, desired_size=len(images), content=None + sequence=titles_anchors_sequence, desired_size=len(images_cv2), content=None ) titles_color = unify_to_bgr(color=titles_color) titles_background_color = unify_to_bgr(color=titles_background_color) - tiles = _generate_tiles( + tiles_image = _generate_tiles( images=resized_images, grid_size=grid_size, single_tile_size=single_tile_size, @@ -696,8 +704,10 @@ def create_tiles( default_title_placement=default_title_placement, ) if return_type == "pillow": - tiles = cv2_to_pillow(image=tiles) - return cast(ImageType, tiles) + tiles_image_pillow: object = cv2_to_pillow(image=tiles_image) + return cast(ImageType, tiles_image_pillow) + tiles_image_cv2: object = tiles_image + return cast(ImageType, tiles_image_cv2) def _negotiate_tiles_format(images: list[ImageType]) -> Literal["cv2", "pillow"]: @@ -880,9 +890,10 @@ def _merge_tiles_elements( tile_margin: int, tile_margin_color: tuple[int, int, int], ) -> npt.NDArray[np.uint8]: - vertical_padding: npt.NDArray[np.uint8] = ( - np.ones((single_tile_size[1], tile_margin, 3), dtype=np.uint8) - * tile_margin_color + vertical_padding: npt.NDArray[np.uint8] = np.full( + (single_tile_size[1], tile_margin, 3), + tile_margin_color, + dtype=np.uint8, ) merged_rows = [ np.concatenate( @@ -896,26 +907,19 @@ def _merge_tiles_elements( for row in tiles_elements ] row_width = merged_rows[0].shape[1] - horizontal_padding = ( - np.ones((tile_margin, row_width, 3), dtype=np.uint8) * tile_margin_color + horizontal_padding: npt.NDArray[np.uint8] = np.full( + (tile_margin, row_width, 3), + tile_margin_color, + dtype=np.uint8, ) - rows_with_paddings = [] + rows_with_paddings: list[npt.NDArray[np.uint8]] = [] for row in merged_rows: rows_with_paddings.append(row) rows_with_paddings.append(horizontal_padding) - return cast( - npt.NDArray[np.uint8], - np.concatenate( - rows_with_paddings[:-1], - axis=0, - ).astype(np.uint8), - ) + return np.concatenate(rows_with_paddings[:-1], axis=0).astype(np.uint8, copy=False) def _generate_color_image( shape: tuple[int, int], color: tuple[int, int, int] ) -> npt.NDArray[np.uint8]: - return cast( - npt.NDArray[np.uint8], - np.ones((*shape[::-1], 3), dtype=np.uint8) * color, - ) + return np.full((*shape[::-1], 3), color, dtype=np.uint8) diff --git a/src/supervision/utils/internal.py b/src/supervision/utils/internal.py index 2ceddb476b..59c7bd2eaf 100644 --- a/src/supervision/utils/internal.py +++ b/src/supervision/utils/internal.py @@ -142,7 +142,7 @@ def my_method(cls): ... """ - def __init__(self, fget: Callable[..., T]): + def __init__(self, fget: Callable[..., T]) -> None: """ Args: The function that is called when the property is accessed. diff --git a/src/supervision/utils/notebook.py b/src/supervision/utils/notebook.py index ed8f4f68f7..f95dd64935 100644 --- a/src/supervision/utils/notebook.py +++ b/src/supervision/utils/notebook.py @@ -2,6 +2,8 @@ import cv2 import matplotlib.pyplot as plt +import numpy as np +import numpy.typing as npt from PIL import Image from supervision.draw.base import ImageType @@ -34,14 +36,16 @@ def plot_image( ``` """ if isinstance(image, Image.Image): - image = pillow_to_cv2(image) + image_np = pillow_to_cv2(image) + else: + image_np = image plt.figure(figsize=size) - if image.ndim == 2: - plt.imshow(image, cmap=cmap) + if image_np.ndim == 2: + plt.imshow(image_np, cmap=cmap) else: - plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) + plt.imshow(cv2.cvtColor(image_np, cv2.COLOR_BGR2RGB)) plt.axis("off") plt.show() @@ -91,11 +95,11 @@ def plot_images_grid( """ nrows, ncols = grid_size - for idx, img in enumerate(images): - if isinstance(img, Image.Image): - images[idx] = pillow_to_cv2(img) + images_np: list[npt.NDArray[np.uint8]] = [ + pillow_to_cv2(img) if isinstance(img, Image.Image) else img for img in images + ] - if len(images) > nrows * ncols: + if len(images_np) > nrows * ncols: raise ValueError( "The number of images exceeds the grid size. Please increase the grid size" " or reduce the number of images." @@ -104,11 +108,11 @@ def plot_images_grid( _fig, axes = plt.subplots(nrows=nrows, ncols=ncols, figsize=size) for idx, ax in enumerate(axes.flat): - if idx < len(images): - if images[idx].ndim == 2: - ax.imshow(images[idx], cmap=cmap) + if idx < len(images_np): + if images_np[idx].ndim == 2: + ax.imshow(images_np[idx], cmap=cmap) else: - ax.imshow(cv2.cvtColor(images[idx], cv2.COLOR_BGR2RGB)) + ax.imshow(cv2.cvtColor(images_np[idx], cv2.COLOR_BGR2RGB)) if titles is not None and idx < len(titles): ax.set_title(titles[idx]) diff --git a/src/supervision/utils/video.py b/src/supervision/utils/video.py index 1d3fe2a464..51a2e16d26 100644 --- a/src/supervision/utils/video.py +++ b/src/supervision/utils/video.py @@ -10,7 +10,8 @@ from collections.abc import Callable, Generator from dataclasses import dataclass from queue import Empty, Full, Queue -from typing import Any +from types import TracebackType +from typing import cast import cv2 import numpy as np @@ -96,18 +97,24 @@ class VideoSink: ``` """ # noqa: E501 // docs - def __init__(self, target_path: str, video_info: VideoInfo, codec: str = "mp4v"): + def __init__( + self, target_path: str, video_info: VideoInfo, codec: str = "mp4v" + ) -> None: self.target_path = target_path self.video_info = video_info self.__codec = codec - self.__writer = None + self.__fourcc: int = 0 + self.__writer: cv2.VideoWriter | None = None def __enter__(self) -> VideoSink: + fourcc_fn = cast( + Callable[[str, str, str, str], int], getattr(cv2, "VideoWriter_fourcc") + ) try: - self.__fourcc = cv2.VideoWriter_fourcc(*self.__codec) + self.__fourcc = int(fourcc_fn(*self.__codec)) except TypeError as e: logger.warning("%s. Defaulting to mp4v...", str(e)) - self.__fourcc = cv2.VideoWriter_fourcc(*"mp4v") + self.__fourcc = int(fourcc_fn(*"mp4v")) self.__writer = cv2.VideoWriter( self.target_path, self.__fourcc, @@ -131,7 +138,7 @@ def __exit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, - exc_traceback: Any, + exc_traceback: TracebackType | None, ) -> None: if self.__writer is not None: self.__writer.release() @@ -271,7 +278,7 @@ def get_video_frames_generator( if not success or frame_position >= end: break if frame is not None: - yield frame + yield cast(npt.NDArray[np.uint8], frame) for _ in range(stride - 1): success = video.grab() if not success: @@ -461,7 +468,7 @@ class FPSMonitor: A class for monitoring frames per second (FPS) to benchmark latency. """ - def __init__(self, sample_size: int = 30): + def __init__(self, sample_size: int = 30) -> None: """ Args: sample_size: The maximum number of observations for latency diff --git a/tests/assets/test_downloader.py b/tests/assets/test_downloader.py index 344d78dba5..ca0c677d6e 100644 --- a/tests/assets/test_downloader.py +++ b/tests/assets/test_downloader.py @@ -7,7 +7,7 @@ class TestMD5HashMatching: - def test_file_exists_matching_hash(self): + def test_file_exists_matching_hash(self) -> None: """Test is_md5_hash_matching when file exists and hash matches.""" test_content = b"test content" test_hash = "9473fdd0d880a43c21b7778d34872157" # MD5 of "test content" @@ -18,7 +18,7 @@ def test_file_exists_matching_hash(self): ): assert is_md5_hash_matching("dummy_file", test_hash) - def test_file_exists_not_matching_hash(self): + def test_file_exists_not_matching_hash(self) -> None: """Test is_md5_hash_matching when file exists but hash doesn't match.""" test_content = b"test content" wrong_hash = "wrong_hash" @@ -29,7 +29,7 @@ def test_file_exists_not_matching_hash(self): ): assert not is_md5_hash_matching("dummy_file", wrong_hash) - def test_file_not_exists(self): + def test_file_not_exists(self) -> None: """Test is_md5_hash_matching when file doesn't exist.""" with patch("os.path.exists", return_value=False): assert not is_md5_hash_matching("nonexistent_file", "some_hash") @@ -39,7 +39,7 @@ class TestDownloadAssets: @patch("supervision.assets.downloader.logger") @patch("supervision.assets.downloader.is_md5_hash_matching", return_value=True) @patch("pathlib.Path.exists", return_value=True) - def test_already_exists_and_valid(self, mock_exists, mock_md5, mock_logger): + def test_already_exists_and_valid(self, mock_exists, mock_md5, mock_logger) -> None: """Test download_assets when file already exists and is valid.""" filename = "vehicles.mp4" result = download_assets(filename) @@ -55,7 +55,7 @@ def test_already_exists_and_valid(self, mock_exists, mock_md5, mock_logger): @patch("pathlib.Path.exists", return_value=True) def test_already_exists_but_corrupted( self, mock_exists, mock_md5, mock_remove, mock_logger - ): + ) -> None: """Test download_assets when file exists but is corrupted (re-downloads).""" filename = "vehicles.mp4" result = download_assets(filename) @@ -79,7 +79,7 @@ def test_download_new_file( mock_mkdir, mock_open_file, mock_logger, - ): + ) -> None: """Test download_assets downloading a new file.""" filename = "vehicles.mp4" @@ -102,7 +102,7 @@ def test_download_new_file( mock_copyfileobj.assert_called_once() @patch("pathlib.Path.exists", return_value=False) - def test_invalid_asset(self, mock_exists): + def test_invalid_asset(self, mock_exists) -> None: """Test download_assets with invalid asset name.""" invalid_filename = "invalid.mp4" @@ -113,7 +113,7 @@ def test_invalid_asset(self, mock_exists): assert "vehicles.mp4" in str(exc_info.value) @patch("pathlib.Path.exists", return_value=True) - def test_invalid_asset_when_file_exists(self, mock_exists): + def test_invalid_asset_when_file_exists(self, mock_exists) -> None: """Test download_assets with invalid asset name that already exists.""" invalid_filename = "invalid.mp4" @@ -139,7 +139,7 @@ def test_with_video_enum( mock_mkdir, mock_open_file, mock_logger, - ): + ) -> None: """Test download_assets with VideoAssets enum.""" asset = VideoAssets.VEHICLES @@ -172,7 +172,7 @@ def test_with_image_enum( mock_mkdir, mock_open_file, mock_logger, - ): + ) -> None: """Test download_assets with ImageAssets enum.""" asset = ImageAssets.SOCCER diff --git a/tests/assets/test_list.py b/tests/assets/test_list.py index 0447af287a..db143ed210 100644 --- a/tests/assets/test_list.py +++ b/tests/assets/test_list.py @@ -7,7 +7,7 @@ ) -def test_video_assets_list(): +def test_video_assets_list() -> None: """Test that VideoAssets.list() returns all video filenames.""" expected_filenames = [ "vehicles.mp4", @@ -24,7 +24,7 @@ def test_video_assets_list(): assert VideoAssets.list() == expected_filenames -def test_image_assets_list(): +def test_image_assets_list() -> None: """Test that ImageAssets.list() returns all image filenames.""" expected_filenames = [ "people-walking.jpg", @@ -33,21 +33,21 @@ def test_image_assets_list(): assert ImageAssets.list() == expected_filenames -def test_video_assets_values(): +def test_video_assets_values() -> None: """Test that VideoAssets enum members have correct attributes.""" assert VideoAssets.VEHICLES.filename == "vehicles.mp4" assert VideoAssets.VEHICLES.md5_hash == "8155ff4e4de08cfa25f39de96483f918" assert VideoAssets.VEHICLES.value == "vehicles.mp4" -def test_image_assets_values(): +def test_image_assets_values() -> None: """Test that ImageAssets enum members have correct attributes.""" assert ImageAssets.SOCCER.filename == "soccer.jpg" assert ImageAssets.SOCCER.md5_hash == "0f5a4b98abf3e3973faf9e9260a7d876" assert ImageAssets.SOCCER.value == "soccer.jpg" -def test_media_assets_dict_keys(): +def test_media_assets_dict_keys() -> None: """Test that MEDIA_ASSETS has all VideoAssets and ImageAssets as keys.""" expected_keys = {asset.filename for asset in VideoAssets} | { asset.filename for asset in ImageAssets @@ -55,7 +55,7 @@ def test_media_assets_dict_keys(): assert set(MEDIA_ASSETS.keys()) == expected_keys -def test_media_assets_dict_values(): +def test_media_assets_dict_values() -> None: """Test that MEDIA_ASSETS values are tuples of (url, md5_hash).""" for filename, (url, md5_hash) in MEDIA_ASSETS.items(): assert isinstance(url, str) diff --git a/tests/dataset/formats/test_coco.py b/tests/dataset/formats/test_coco.py index d7420054df..14cc855695 100644 --- a/tests/dataset/formats/test_coco.py +++ b/tests/dataset/formats/test_coco.py @@ -12,6 +12,7 @@ build_coco_class_index_mapping, classes_to_coco_categories, coco_annotations_to_detections, + coco_annotations_to_masks, coco_categories_to_classes, detections_to_coco_annotations, group_coco_annotations_by_image_id, @@ -1159,6 +1160,31 @@ def test_load_coco_annotations_force_masks_with_no_annotations( assert annotations[no_annotations_path] == Detections.empty() +def test_coco_annotations_to_masks_handles_rle_polygon_and_invalid_dict() -> None: + image_annotations = [ + {"id": 1, "segmentation": {"size": [5, 5], "counts": [15, 2, 3, 2, 3]}}, + {"id": 2, "segmentation": [[0, 0, 4, 0, 4, 4, 0, 4]]}, + {"id": 3, "segmentation": {"size": [5, 5]}}, + ] + + with pytest.warns( + UserWarning, + match=( + "Skipping annotation 3: segmentation is a dict but missing 'counts' key " + r"\(expected RLE format\)" + ), + ): + masks = coco_annotations_to_masks( + image_annotations=image_annotations, + resolution_wh=(5, 5), + ) + + assert masks.shape == (3, 5, 5) + assert masks[0].any() + assert masks[1].any() + assert not masks[2].any() + + @pytest.mark.parametrize( "file_name", [".", "", "subdir/.."], diff --git a/tests/dataset/formats/test_pascal_voc.py b/tests/dataset/formats/test_pascal_voc.py index aca08b82e8..1f4a8260e1 100644 --- a/tests/dataset/formats/test_pascal_voc.py +++ b/tests/dataset/formats/test_pascal_voc.py @@ -14,7 +14,7 @@ from tests.helpers import _create_detections -def are_xml_elements_equal(elem1, elem2): +def are_xml_elements_equal(elem1, elem2) -> bool: if ( elem1.tag != elem2.tag or elem1.attrib != elem2.attrib @@ -66,7 +66,7 @@ def test_object_to_pascal_voc( polygon: np.ndarray | None, expected_result, exception: Exception, -): +) -> None: with exception: result = object_to_pascal_voc(xyxy=xyxy, name=name, polygon=polygon) assert are_xml_elements_equal(result, expected_result) @@ -90,7 +90,7 @@ def test_parse_polygon_points( polygon_element, expected_result: list[list], exception, -): +) -> None: with exception: result = parse_polygon_points(polygon_element) assert np.array_equal(result, expected_result) @@ -179,7 +179,7 @@ def test_parse_polygon_points( ) def test_detections_from_xml_obj( xml_string, classes, resolution_wh, force_masks, expected_result, exception -): +) -> None: with exception: root = ElementTree.fromstring(xml_string) result, _ = detections_from_xml_obj(root, classes, resolution_wh, force_masks) diff --git a/tests/detection/test_core.py b/tests/detection/test_core.py index 94434c6f76..54eaac6311 100644 --- a/tests/detection/test_core.py +++ b/tests/detection/test_core.py @@ -874,7 +874,7 @@ def test_merge_inner_detection_object_pair( detection_2: Detections, expected_result: Detections | None, exception: Exception, -): +) -> None: with exception: result = merge_inner_detection_object_pair(detection_1, detection_2) assert result == expected_result diff --git a/tests/detection/test_csv.py b/tests/detection/test_csv.py index be83766606..9137dfdfae 100644 --- a/tests/detection/test_csv.py +++ b/tests/detection/test_csv.py @@ -515,7 +515,7 @@ def test_csv_sink_manual( assert_csv_equal(file_name, expected_result) -def assert_csv_equal(file_name, expected_rows): +def assert_csv_equal(file_name, expected_rows) -> None: with open(file_name, newline="") as file: reader = csv.reader(file) for i, row in enumerate(reader): diff --git a/tests/detection/test_json.py b/tests/detection/test_json.py index 3da34865fc..b6f0ffea2b 100644 --- a/tests/detection/test_json.py +++ b/tests/detection/test_json.py @@ -395,7 +395,7 @@ def test_json_sink( assert_json_equal(file_name, expected_result) -def assert_json_equal(file_name, expected_rows): +def assert_json_equal(file_name, expected_rows) -> None: with open(file_name) as file: data = json.load(file) assert data == expected_rows, ( diff --git a/tests/detection/test_polygonzone.py b/tests/detection/test_polygonzone.py index 6e952ec7d9..45a2a98ec5 100644 --- a/tests/detection/test_polygonzone.py +++ b/tests/detection/test_polygonzone.py @@ -40,7 +40,7 @@ class TestPolygonZoneInit: ), ], ) - def test_empty_anchors_raises(self, polygon, triggering_anchors, exception): + def test_empty_anchors_raises(self, polygon, triggering_anchors, exception) -> None: with exception: sv.PolygonZone(polygon, triggering_anchors=triggering_anchors) diff --git a/tests/detection/test_vlm.py b/tests/detection/test_vlm.py index b5d035b065..82a6cc2444 100644 --- a/tests/detection/test_vlm.py +++ b/tests/detection/test_vlm.py @@ -1153,7 +1153,7 @@ def test_from_google_gemini_2_5( classes: list[str] | None, expected_results: None | (tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]), -): +) -> None: with exception: ( xyxy, @@ -1274,7 +1274,7 @@ def test_from_deepseek_vl_2( resolution_wh: tuple[int, int], classes: list[str] | None, expected_detections: Detections, -): +) -> None: with exception: detections = Detections.from_vlm( vlm=VLM.DEEPSEEK_VL_2, diff --git a/tests/detection/tools/test_smoother.py b/tests/detection/tools/test_smoother.py new file mode 100644 index 0000000000..6c9b6e01de --- /dev/null +++ b/tests/detection/tools/test_smoother.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +import numpy as np + +from supervision.detection.core import Detections +from supervision.detection.tools.smoother import DetectionsSmoother + + +def test_smoother_confidence_averaged_when_all_frames_have_confidence() -> None: + smoother = DetectionsSmoother() + + first = Detections( + xyxy=np.array([[0, 0, 10, 10]], dtype=float), + confidence=np.array([0.2], dtype=float), + tracker_id=np.array([1]), + ) + second = Detections( + xyxy=np.array([[2, 2, 12, 12]], dtype=float), + confidence=np.array([0.8], dtype=float), + tracker_id=np.array([1]), + ) + + smoother.update_with_detections(first) + smoothed = smoother.update_with_detections(second) + + assert smoothed.confidence is not None + np.testing.assert_allclose(smoothed.confidence, np.array([0.5], dtype=float)) + + +def test_detections_smoother_confidence_is_none_if_any_frame_missing() -> None: + smoother = DetectionsSmoother() + + with_confidence = Detections( + xyxy=np.array([[0, 0, 10, 10]], dtype=float), + confidence=np.array([0.2], dtype=float), + tracker_id=np.array([1]), + ) + without_confidence = Detections( + xyxy=np.array([[2, 2, 12, 12]], dtype=float), + confidence=None, + tracker_id=np.array([1]), + ) + + smoother.update_with_detections(with_confidence) + smoothed = smoother.update_with_detections(without_confidence) + + assert smoothed.confidence is None + + +def test_detections_smoother_averages_xyxy_over_two_frames() -> None: + smoother = DetectionsSmoother() + + first = Detections( + xyxy=np.array([[0, 0, 10, 10]], dtype=float), + confidence=np.array([0.8], dtype=float), + tracker_id=np.array([1]), + ) + second = Detections( + xyxy=np.array([[2, 2, 12, 12]], dtype=float), + confidence=np.array([0.8], dtype=float), + tracker_id=np.array([1]), + ) + + smoother.update_with_detections(first) + smoothed = smoother.update_with_detections(second) + + expected_xyxy = np.array([[1, 1, 11, 11]], dtype=float) + np.testing.assert_allclose(smoothed.xyxy, expected_xyxy) + + +def test_detections_smoother_empty_detections_does_not_raise() -> None: + smoother = DetectionsSmoother() + + smoothed = smoother.update_with_detections(Detections.empty()) + + assert len(smoothed) == 0 diff --git a/tests/detection/utils/test_internal.py b/tests/detection/utils/test_internal.py index c5bc08dd85..2f5e354325 100644 --- a/tests/detection/utils/test_internal.py +++ b/tests/detection/utils/test_internal.py @@ -671,7 +671,7 @@ def test_merge_data( data_list: list[dict[str, Any]], expected_result: dict[str, Any] | None, exception: Exception, -): +) -> None: with exception: result = merge_data(data_list=data_list) if expected_result is None: @@ -847,7 +847,7 @@ def test_get_data_item( index: Any, expected_result: dict[str, Any] | None, exception: Exception, -): +) -> None: with exception: result = get_data_item(data=data, index=index) for key in result: @@ -1024,7 +1024,7 @@ def test_get_data_item( ), ], ) -def test_merge_metadata(metadata_list, expected_result, exception): +def test_merge_metadata(metadata_list, expected_result, exception) -> None: with exception: result = merge_metadata(metadata_list) if expected_result is None: diff --git a/tests/detection/utils/test_vlms.py b/tests/detection/utils/test_vlms.py index 805c9681d6..4eb314afee 100644 --- a/tests/detection/utils/test_vlms.py +++ b/tests/detection/utils/test_vlms.py @@ -60,7 +60,7 @@ ("123ABC!", "123abc!", False, 0), ], ) -def test_edit_distance(string_1, string_2, case_sensitive, expected_result): +def test_edit_distance(string_1, string_2, case_sensitive, expected_result) -> None: assert ( edit_distance(string_1, string_2, case_sensitive=case_sensitive) == expected_result @@ -105,7 +105,7 @@ def test_edit_distance(string_1, string_2, case_sensitive, expected_result): ) def test_fuzzy_match_index( candidates, query, threshold, case_sensitive, expected_result -): +) -> None: assert ( fuzzy_match_index( candidates=candidates, diff --git a/tests/draw/test_utils.py b/tests/draw/test_utils.py new file mode 100644 index 0000000000..b62862088e --- /dev/null +++ b/tests/draw/test_utils.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +import cv2 +import numpy as np +import pytest + +from supervision.draw.utils import draw_image +from supervision.geometry.core import Rect + + +def test_draw_image_invalid_path_raises_oserror(tmp_path) -> None: + """ + Verify that drawing from an invalid image file raises OSError. + """ + + invalid_image_path = tmp_path / "invalid_image.dat" + invalid_image_path.write_bytes(b"not an image") + scene = np.zeros((100, 100, 3), dtype=np.uint8) + rect = Rect(x=0, y=0, width=100, height=100) + + with pytest.raises(OSError, match="Could not decode image path"): + draw_image( + scene=scene, + image=str(invalid_image_path), + opacity=1.0, + rect=rect, + ) + + +def test_draw_image_valid_image(tmp_path) -> None: + """ + Verify that drawing a valid image from disk returns an ndarray. + """ + image = np.zeros((100, 100, 3), dtype=np.uint8) + image_path = tmp_path / "image.png" + cv2.imwrite(str(image_path), image) + + scene = np.zeros((100, 100, 3), dtype=np.uint8) + rect = Rect(x=0, y=0, width=100, height=100) + + result = draw_image( + scene=scene, + image=str(image_path), + opacity=1.0, + rect=rect, + ) + + assert isinstance(result, np.ndarray) diff --git a/tests/helpers.py b/tests/helpers.py index 1ce209b39c..f6841c0301 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -185,7 +185,7 @@ def _generate_random_boxes( return out -def assert_almost_equal(actual, expected, tolerance=1e-5): +def assert_almost_equal(actual, expected, tolerance=1e-5) -> None: """ Assert that two values are equal within a specified tolerance. @@ -239,7 +239,7 @@ def assert_image_mostly_same( class _FakeTensor: """Minimal tensor wrapper for cpu().numpy() and int().""" - def __init__(self, arr: np.ndarray): + def __init__(self, arr: np.ndarray) -> None: self._arr = np.asarray(arr) def cpu(self) -> _FakeTensor: @@ -255,7 +255,7 @@ def int(self) -> _FakeTensor: class _FakeYOLOv5Results: """YOLOv5-like results exposing pred list.""" - def __init__(self, pred0: np.ndarray): + def __init__(self, pred0: np.ndarray) -> None: self.pred = [_FakeTensor(pred0)] @@ -268,7 +268,7 @@ def __init__( conf: np.ndarray, cls: np.ndarray, id_: np.ndarray | None = None, - ): + ) -> None: self.xyxy = _FakeTensor(xyxy) self.conf = _FakeTensor(conf) self.cls = _FakeTensor(cls) @@ -278,7 +278,7 @@ def __init__( class _FakeUltralyticsResults: """Ultralytics-like results container used by from_ultralytics.""" - def __init__(self, boxes, names: dict[int, str], length: int = 0): + def __init__(self, boxes, names: dict[int, str], length: int = 0) -> None: self.boxes = boxes self.names = names self.obb = None @@ -292,7 +292,7 @@ def __len__(self) -> int: class _FakeYoloNasPrediction: """YOLO-NAS-like prediction struct.""" - def __init__(self, bboxes_xyxy, confidence, labels): + def __init__(self, bboxes_xyxy, confidence, labels) -> None: self.bboxes_xyxy = bboxes_xyxy self.confidence = confidence self.labels = labels @@ -301,14 +301,14 @@ def __init__(self, bboxes_xyxy, confidence, labels): class _FakeYoloNasResults: """YOLO-NAS-like results exposing prediction.""" - def __init__(self, prediction: _FakeYoloNasPrediction): + def __init__(self, prediction: _FakeYoloNasPrediction) -> None: self.prediction = prediction class _FakeYoloNasKeyPoint: """YOLO-NAS-like key point struct.""" - def __init__(self, poses, labels=None): + def __init__(self, poses, labels=None) -> None: self.poses = np.array(poses, dtype=np.float32) if labels is not None: self.labels = np.array(labels, dtype=int) @@ -317,20 +317,20 @@ def __init__(self, poses, labels=None): class _FakeYoloNasKeyPointResults: """YOLO-NAS-like results exposing key points.""" - def __init__(self, prediction: _FakeYoloNasKeyPoint, class_names=None): + def __init__(self, prediction: _FakeYoloNasKeyPoint, class_names=None) -> None: self.prediction = prediction self.class_names = class_names class _FakeMediapipeLandmark: - def __init__(self, x, y, visibility=1.0): + def __init__(self, x, y, visibility=1.0) -> None: self.x = x self.y = y self.visibility = visibility class _FakeMediapipePose: - def __init__(self, landmarks: list[_FakeMediapipeLandmark]): + def __init__(self, landmarks: list[_FakeMediapipeLandmark]) -> None: self.landmark = landmarks @@ -342,7 +342,7 @@ def __init__( | None = None, face_landmarks: _FakeMediapipeLandmark | None = None, multi_face_landmarks: list[_FakeMediapipeLandmark] | None = None, - ): + ) -> None: self.pose_landmarks = pose_landmarks self.face_landmarks = face_landmarks self.multi_face_landmarks = multi_face_landmarks diff --git a/tests/key_points/test_annotators.py b/tests/key_points/test_annotators.py index 3ecc40ce64..62e576c04b 100644 --- a/tests/key_points/test_annotators.py +++ b/tests/key_points/test_annotators.py @@ -12,7 +12,7 @@ class TestVertexAnnotator: which is essential for human pose estimation or similar tasks. """ - def test_annotate_with_default_parameters(self, scene, sample_key_points): + def test_annotate_with_default_parameters(self, scene, sample_key_points) -> None: """ Verify that VertexAnnotator correctly draws keypoints with default parameters. @@ -27,7 +27,9 @@ def test_annotate_with_default_parameters(self, scene, sample_key_points): original=scene, annotated=result, similarity_threshold=0.8 ) - def test_annotate_with_custom_color_and_radius(self, scene, sample_key_points): + def test_annotate_with_custom_color_and_radius( + self, scene, sample_key_points + ) -> None: """ Verify that VertexAnnotator respects custom color and radius settings. @@ -45,7 +47,7 @@ def test_annotate_with_custom_color_and_radius(self, scene, sample_key_points): original=scene, annotated=result, similarity_threshold=0.7 ) - def test_annotate_empty_key_points(self, scene, empty_key_points): + def test_annotate_empty_key_points(self, scene, empty_key_points) -> None: """ Verify that VertexAnnotator handles empty keypoints without modifying the scene. @@ -67,7 +69,7 @@ class TestEdgeAnnotator: forming skeletons that help users interpret spatial relationships. """ - def test_annotate_with_default_parameters(self, scene, sample_key_points): + def test_annotate_with_default_parameters(self, scene, sample_key_points) -> None: """ Verify correctly draw skeleton edges with default parameters. @@ -82,7 +84,7 @@ def test_annotate_with_default_parameters(self, scene, sample_key_points): original=scene, annotated=result, similarity_threshold=0.7 ) - def test_annotate_with_custom_edges(self, scene, sample_key_points): + def test_annotate_with_custom_edges(self, scene, sample_key_points) -> None: """ Verify that EdgeAnnotator respects custom-defined skeleton structures. @@ -99,7 +101,7 @@ def test_annotate_with_custom_edges(self, scene, sample_key_points): original=scene, annotated=result, similarity_threshold=0.8 ) - def test_annotate_empty_key_points(self, scene, empty_key_points): + def test_annotate_empty_key_points(self, scene, empty_key_points) -> None: """ Verify that EdgeAnnotator handles empty keypoints without modifying the scene. @@ -112,7 +114,7 @@ def test_annotate_empty_key_points(self, scene, empty_key_points): # Should return the original scene unchanged assert np.array_equal(result, scene) - def test_annotate_no_edges_found(self, scene): + def test_annotate_no_edges_found(self, scene) -> None: """ Verify returning unmodified scene when no known skeleton matches. diff --git a/tests/key_points/test_core.py b/tests/key_points/test_core.py index bf10b1c29f..cf2ddb4a16 100644 --- a/tests/key_points/test_core.py +++ b/tests/key_points/test_core.py @@ -368,13 +368,13 @@ ), # kp[kp.confidence > 0.5] — single-object canonical use case ], ) -def test_key_points_getitem(key_points, index, expected_result, exception): +def test_key_points_getitem(key_points, index, expected_result, exception) -> None: with exception: result = key_points[index] assert result == expected_result -def test_key_points_empty(): +def test_key_points_empty() -> None: """Test the creation and behavior of an empty KeyPoints object.""" empty_key_points = KeyPoints.empty() assert len(empty_key_points) == 0 @@ -382,7 +382,7 @@ def test_key_points_empty(): assert empty_key_points.xy.shape == (0, 0, 2) -def test_key_points_is_empty(): +def test_key_points_is_empty() -> None: """Test the is_empty method for KeyPoints objects.""" empty_key_points = KeyPoints.empty() assert empty_key_points.is_empty() @@ -395,7 +395,7 @@ def test_key_points_is_empty(): assert not non_empty_key_points.is_empty() -def test_key_points_setitem(): +def test_key_points_setitem() -> None: """Test the __setitem__ method for KeyPoints objects.""" key_points = _create_key_points( xy=[[[0, 1], [2, 3]]], @@ -438,7 +438,7 @@ def test_key_points_setitem(): ) def test_key_points_as_detections( key_points, expected_xyxy, expected_confidence, expected_class_id -): +) -> None: """Test the as_detections method for KeyPoints objects.""" detections = key_points.as_detections() assert len(detections) == len(expected_xyxy) @@ -447,14 +447,14 @@ def test_key_points_as_detections( assert np.array_equal(detections.class_id, expected_class_id) -def test_key_points_as_detections_empty(): +def test_key_points_as_detections_empty() -> None: """Test the as_detections method for empty KeyPoints objects.""" empty_key_points = KeyPoints.empty() empty_detections = empty_key_points.as_detections() assert empty_detections.is_empty() -def test_key_points_as_detections_with_data(): +def test_key_points_as_detections_with_data() -> None: """Test the as_detections method preserves data.""" key_points = _create_key_points( xy=[[[0, 1], [2, 3], [4, 5]]], @@ -467,7 +467,7 @@ def test_key_points_as_detections_with_data(): assert np.array_equal(detections.data["custom_data"], np.array(["value1"])) -def test_key_points_iteration(): +def test_key_points_iteration() -> None: """Test the iteration over KeyPoints objects.""" key_points = _create_key_points( xy=[[[0, 1], [2, 3]], [[4, 5], [6, 7]]], @@ -485,7 +485,7 @@ def test_key_points_iteration(): assert iterations == 2 -def test_key_points_iteration_no_confidence(): +def test_key_points_iteration_no_confidence() -> None: """Test the iteration over KeyPoints objects without confidence.""" key_points_no_conf = _create_key_points( xy=[[[0, 1], [2, 3]]], @@ -537,13 +537,13 @@ def test_key_points_iteration_no_confidence(): ), ], ) -def test_key_points_equality(key_points1, key_points2, expected_equal): +def test_key_points_equality(key_points1, key_points2, expected_equal) -> None: """Test the equality comparison for KeyPoints objects.""" status = key_points1 == key_points2 assert status is expected_equal -def test_key_points_equality_with_data(): +def test_key_points_equality_with_data() -> None: """Test the equality comparison for KeyPoints objects with data.""" key_points1 = _create_key_points( xy=[[[0, 1], [2, 3]]], confidence=[[0.8, 0.9]], class_id=[0] @@ -581,13 +581,13 @@ def test_key_points_equality_with_data(): ({"predictions": []}, KeyPoints.empty()), ], ) -def test_from_inference_input(inference_results, expected_key_points): +def test_from_inference_input(inference_results, expected_key_points) -> None: """Test the from_inference method with valid input.""" key_points = KeyPoints.from_inference(inference_results) assert key_points == expected_key_points -def test_from_inference_invalid_input(): +def test_from_inference_invalid_input() -> None: """Test the from_inference method with invalid input.""" key_points = _create_key_points( xy=[[[0, 1], [2, 3]]], confidence=[[0.8, 0.9]], class_id=[0] @@ -624,7 +624,7 @@ def test_from_inference_invalid_input(): ), ], ) -def test_from_yolo_nas_input(yolo_nas_results, expected_key_points): +def test_from_yolo_nas_input(yolo_nas_results, expected_key_points) -> None: """Test the from_yolo_nas method with valid input.""" key_points = KeyPoints.from_yolo_nas(yolo_nas_results) assert key_points == expected_key_points @@ -667,7 +667,9 @@ def test_from_yolo_nas_input(yolo_nas_results, expected_key_points): ), ], ) -def test_from_mediapipe_input(mediapipe_results, resolution_wh, expected_key_points): +def test_from_mediapipe_input( + mediapipe_results, resolution_wh, expected_key_points +) -> None: """Test the from_mediapipe method with valid input.""" key_points = KeyPoints.from_mediapipe( mediapipe_results, resolution_wh=resolution_wh diff --git a/tests/key_points/test_skeletons.py b/tests/key_points/test_skeletons.py index e97562888f..b4bc4f7eb1 100644 --- a/tests/key_points/test_skeletons.py +++ b/tests/key_points/test_skeletons.py @@ -6,7 +6,7 @@ class TestSkeletons: - def test_skeleton_enum_values(self): + def test_skeleton_enum_values(self) -> None: """Test skeleton enum has correct structure.""" for skeleton in Skeleton: assert isinstance(skeleton.value, tuple) @@ -14,7 +14,7 @@ def test_skeleton_enum_values(self): isinstance(edge, tuple) and len(edge) == 2 for edge in skeleton.value ) - def test_skeletons_by_vertex_count(self): + def test_skeletons_by_vertex_count(self) -> None: """Test SKELETONS_BY_VERTEX_COUNT dictionary population.""" # Test that the dictionary is populated assert len(SKELETONS_BY_VERTEX_COUNT) > 0 @@ -24,7 +24,7 @@ def test_skeletons_by_vertex_count(self): assert 17 in SKELETONS_BY_VERTEX_COUNT # COCO has 17 keypoints assert SKELETONS_BY_VERTEX_COUNT[17] == coco_skeleton - def test_skeletons_by_edge_count(self): + def test_skeletons_by_edge_count(self) -> None: """Test SKELETONS_BY_EDGE_COUNT dictionary mapping.""" # Test that the dictionary is populated assert len(SKELETONS_BY_EDGE_COUNT) > 0 @@ -38,13 +38,13 @@ def test_skeletons_by_edge_count(self): assert SKELETONS_BY_EDGE_COUNT == expected - def test_unique_vertices_calculation(self): + def test_unique_vertices_calculation(self) -> None: """Test unique vertices calculation from skeleton edges.""" coco_skeleton = Skeleton.COCO.value unique_vertices = {vertex for edge in coco_skeleton for vertex in edge} assert len(unique_vertices) == 17 # COCO has 17 keypoints - def test_skeletons_by_vertex_count_mapping_behaviour(self): + def test_skeletons_by_vertex_count_mapping_behaviour(self) -> None: """Test SKELETONS_BY_VERTEX_COUNT uses last-in-wins for duplicate counts.""" expected_mapping = {} for skeleton in Skeleton: diff --git a/tests/metrics/conftest.py b/tests/metrics/conftest.py index c6e2c8e901..7033763b88 100644 --- a/tests/metrics/conftest.py +++ b/tests/metrics/conftest.py @@ -1,5 +1,3 @@ -from typing import Optional - import numpy as np import pytest @@ -145,7 +143,7 @@ def target_class_1(): def _yolo_dataset_factory( tmp_path, num_images: int = 20, - classes: Optional[list[str]] = None, + classes: list[str] | None = None, objects_per_image_range: tuple[int, int] = (1, 3), ): """ diff --git a/tests/metrics/test_detection.py b/tests/metrics/test_detection.py index ba7b1f9d6a..47c65a9578 100644 --- a/tests/metrics/test_detection.py +++ b/tests/metrics/test_detection.py @@ -342,7 +342,7 @@ def test_from_tensors( iou_threshold, expected_result: np.ndarray | None, exception: Exception, - ): + ) -> None: with exception: result = ConfusionMatrix.from_tensors( predictions=predictions, @@ -386,7 +386,7 @@ def test_evaluate_detection_batch( iou_threshold, expected_result: np.ndarray | None, exception: Exception, - ): + ) -> None: with exception: result = ConfusionMatrix.evaluate_detection_batch( predictions=predictions, @@ -414,7 +414,7 @@ def test_drop_extra_matches( matches, expected_result: np.ndarray | None, exception: Exception, - ): + ) -> None: with exception: result = ConfusionMatrix._drop_extra_matches(matches) @@ -934,7 +934,7 @@ def test_confusion_matrix( iou_threshold, expected_result, exception: Exception, - ): + ) -> None: with exception: confusion_matrix = ConfusionMatrix.from_detections( predictions=predictions, @@ -948,7 +948,7 @@ def test_confusion_matrix( # AssertionError if the two arrays are not equal np.testing.assert_array_equal(confusion_matrix.matrix, expected_result) - def test_confusion_matrix_on_yolo_dataset(self, yolo_dataset_structure): + def test_confusion_matrix_on_yolo_dataset(self, yolo_dataset_structure) -> None: """ Test confusion matrix calculation on a YOLO-format dataset. diff --git a/tests/metrics/test_f1_score.py b/tests/metrics/test_f1_score.py index 347da196e1..bea901f648 100644 --- a/tests/metrics/test_f1_score.py +++ b/tests/metrics/test_f1_score.py @@ -36,7 +36,7 @@ def targets_multiple_classes(self): class_id=np.array([0, 1]), ) - def test_initialization_default(self): + def test_initialization_default(self) -> None: """Test that F1Score can be initialized with default parameters""" metric = F1Score() assert metric._metric_target == MetricTarget.BOXES @@ -44,7 +44,7 @@ def test_initialization_default(self): assert metric._predictions_list == [] assert metric._targets_list == [] - def test_initialization_custom(self): + def test_initialization_custom(self) -> None: """Test that F1Score can be initialized with custom parameters""" metric = F1Score( metric_target=MetricTarget.MASKS, @@ -53,7 +53,7 @@ def test_initialization_custom(self): assert metric._metric_target == MetricTarget.MASKS assert metric.averaging_method == AveragingMethod.MACRO - def test_reset(self, dummy_prediction): + def test_reset(self, dummy_prediction) -> None: """Test that reset() clears all stored data""" metric = F1Score() @@ -69,7 +69,7 @@ def test_reset(self, dummy_prediction): assert metric._predictions_list == [] assert metric._targets_list == [] - def test_perfect_match(self, detections_50_50, targets_50_50): + def test_perfect_match(self, detections_50_50, targets_50_50) -> None: """Test F1 score with perfect matching predictions and targets""" metric = F1Score() result = metric.update(detections_50_50, targets_50_50).compute() @@ -84,7 +84,7 @@ def test_perfect_match(self, detections_50_50, targets_50_50): assert len(result.matched_classes) == 1 assert result.matched_classes[0] == 0 - def test_no_overlap(self, predictions_no_overlap, targets_no_overlap): + def test_no_overlap(self, predictions_no_overlap, targets_no_overlap) -> None: """Test F1 score with predictions that don't overlap with targets""" metric = F1Score() result = metric.update(predictions_no_overlap, targets_no_overlap).compute() @@ -96,7 +96,7 @@ def test_no_overlap(self, predictions_no_overlap, targets_no_overlap): assert result.f1_50 == 0.0 assert result.f1_75 == 0.0 - def test_empty_predictions(self, targets_50_50): + def test_empty_predictions(self, targets_50_50) -> None: """Test F1 score with empty predictions but existing targets""" predictions = Detections.empty() @@ -110,7 +110,7 @@ def test_empty_predictions(self, targets_50_50): assert result.f1_50 == 0.0 assert result.f1_75 == 0.0 - def test_empty_targets(self, detections_50_50): + def test_empty_targets(self, detections_50_50) -> None: """Test F1 score with predictions but no targets""" targets = Detections.empty() @@ -126,7 +126,7 @@ def test_empty_targets(self, detections_50_50): def test_single_class_mixed_results( self, predictions_confidence_ranking, targets_50_50 - ): + ) -> None: """Test F1 score calculation with mixed precision and recall""" metric = F1Score() result = metric.update(predictions_confidence_ranking, targets_50_50).compute() @@ -141,7 +141,7 @@ def test_single_class_mixed_results( def test_precision_recall_imbalance( self, detections_50_50, targets_two_objects_class_0 - ): + ) -> None: """Test F1 score with different precision and recall scenarios""" metric = F1Score() result = metric.update(detections_50_50, targets_two_objects_class_0).compute() @@ -156,7 +156,7 @@ def test_precision_recall_imbalance( def test_multiple_classes( self, predictions_multiple_classes, targets_multiple_classes - ): + ) -> None: """Test F1 score calculation for multiple classes""" metric = F1Score() result = metric.update( @@ -172,7 +172,9 @@ def test_multiple_classes( assert 0 in result.matched_classes assert 1 in result.matched_classes - def test_different_iou_thresholds(self, predictions_iou_064, targets_iou_064): + def test_different_iou_thresholds( + self, predictions_iou_064, targets_iou_064 + ) -> None: """Test F1 score at different IoU thresholds""" metric = F1Score() result = metric.update(predictions_iou_064, targets_iou_064).compute() @@ -183,7 +185,9 @@ def test_different_iou_thresholds(self, predictions_iou_064, targets_iou_064): assert result.f1_50 == 1.0 assert result.f1_75 == 0.0 - def test_confidence_ranking(self, predictions_confidence_ranking, targets_50_50): + def test_confidence_ranking( + self, predictions_confidence_ranking, targets_50_50 + ) -> None: """Test that F1 score respects confidence ranking""" metric = F1Score() result = metric.update(predictions_confidence_ranking, targets_50_50).compute() @@ -197,7 +201,7 @@ def test_confidence_ranking(self, predictions_confidence_ranking, targets_50_50) def test_list_inputs( self, detections_50_50, targets_50_50, prediction_class_1, target_class_1 - ): + ) -> None: """Test F1 score with list inputs""" metric = F1Score() result = metric.update( @@ -208,7 +212,7 @@ def test_list_inputs( assert result.f1_50 == 1.0 assert result.f1_75 == 1.0 - def test_mismatched_list_lengths(self, detections_50_50, targets_50_50): + def test_mismatched_list_lengths(self, detections_50_50, targets_50_50) -> None: """Test that mismatched prediction/target list lengths raise error""" metric = F1Score() @@ -216,11 +220,52 @@ def test_mismatched_list_lengths(self, detections_50_50, targets_50_50): with pytest.raises(ValueError, match="number of predictions"): metric.update([detections_50_50], [targets_50_50, targets_50_50]) + @pytest.mark.parametrize( + "missing_attribute", + ["predictions_class_id", "targets_class_id", "predictions_confidence"], + ) + def test_compute_value_error_for_missing_required_fields( + self, missing_attribute + ) -> None: + """Raises ValueError when required detection fields are missing.""" + metric = F1Score() + boxes = np.array([[10, 10, 50, 50]], dtype=np.float32) + class_id = np.array([0], dtype=np.int32) + confidence = np.array([0.9], dtype=np.float32) + + predictions = Detections( + xyxy=boxes, + confidence=confidence, + class_id=class_id, + ) + targets = Detections( + xyxy=boxes, + class_id=class_id, + ) + + if missing_attribute == "predictions_class_id": + predictions = Detections( + xyxy=boxes, + confidence=confidence, + ) + elif missing_attribute == "targets_class_id": + targets = Detections(xyxy=boxes) + else: + predictions = Detections( + xyxy=boxes, + class_id=class_id, + ) + + with pytest.raises(ValueError, match="F1Score metric requires"): + metric.update(predictions, targets).compute() + @pytest.mark.parametrize( "averaging_method", [AveragingMethod.MACRO, AveragingMethod.MICRO, AveragingMethod.WEIGHTED], ) - def test_averaging_methods(self, averaging_method, detections_50_50, targets_50_50): + def test_averaging_methods( + self, averaging_method, detections_50_50, targets_50_50 + ) -> None: """Test different averaging methods""" metric = F1Score(averaging_method=averaging_method) result = metric.update(detections_50_50, targets_50_50).compute() @@ -231,7 +276,7 @@ def test_averaging_methods(self, averaging_method, detections_50_50, targets_50_ def test_macro_averaging( self, predictions_multiple_classes, targets_multiple_classes - ): + ) -> None: """Test MACRO averaging with specific example""" metric = F1Score(averaging_method=AveragingMethod.MACRO) result = metric.update( @@ -244,7 +289,7 @@ def test_macro_averaging( def test_micro_averaging( self, predictions_multiple_classes, targets_multiple_classes - ): + ) -> None: """Test MICRO averaging with specific example""" metric = F1Score(averaging_method=AveragingMethod.MICRO) result = metric.update( @@ -257,7 +302,7 @@ def test_micro_averaging( def test_weighted_averaging( self, predictions_multiple_classes, targets_multiple_classes - ): + ) -> None: """Test WEIGHTED averaging with specific example""" metric = F1Score(averaging_method=AveragingMethod.WEIGHTED) result = metric.update( diff --git a/tests/metrics/test_mean_average_precision.py b/tests/metrics/test_mean_average_precision.py index 9d19fd7904..9c1238b745 100644 --- a/tests/metrics/test_mean_average_precision.py +++ b/tests/metrics/test_mean_average_precision.py @@ -5,7 +5,7 @@ class TestMeanAveragePrecision: - def test_single_perfect_detection(self, detections_50_50, targets_50_50): + def test_single_perfect_detection(self, detections_50_50, targets_50_50) -> None: """Test that single perfect detection gets 1.0 mAP (not 0.0 due to ID=0 bug)""" metric = MeanAveragePrecision() metric.update([detections_50_50], [targets_50_50]) @@ -14,7 +14,7 @@ def test_single_perfect_detection(self, detections_50_50, targets_50_50): # Should be perfect 1.0 mAP, not 0.0 due to ID=0 bug assert abs(result.map50_95 - 1.0) < 1e-6 - def test_multiple_perfect_detections(self): + def test_multiple_perfect_detections(self) -> None: """Test that multiple perfect detections get 1.0 mAP""" # Multiple perfect detections in one image detections = Detections( @@ -33,7 +33,9 @@ def test_multiple_perfect_detections(self): # Should be perfect 1.0 mAP assert abs(result.map50_95 - 1.0) < 1e-6 - def test_batch_updates_perfect_detections(self, detections_50_50, targets_50_50): + def test_batch_updates_perfect_detections( + self, detections_50_50, targets_50_50 + ) -> None: """Test that batch updates with perfect detections get 1.0 mAP""" metric = MeanAveragePrecision() # Add 3 batch updates @@ -45,7 +47,7 @@ def test_batch_updates_perfect_detections(self, detections_50_50, targets_50_50) # Should be perfect 1.0 mAP across all batches assert abs(result.map50_95 - 1.0) < 1e-6 - def test_scenario_1_success_case_imperfect_match(self): + def test_scenario_1_success_case_imperfect_match(self) -> None: """Scenario 1: Success Case with imperfect match""" # Small object (class 0) - area = 30*30 = 900 < 1024 small_perfect = Detections( @@ -99,7 +101,7 @@ def test_scenario_1_success_case_imperfect_match(self): result.medium_objects.map50_95 < 1.0 ) # Medium should be less than perfect - def test_scenario_2_missed_detection(self): + def test_scenario_2_missed_detection(self) -> None: """Scenario 2: GT Present, No Prediction (Missed Detection)""" # Small object - area = 30*30 = 900 < 1024 small_detection = Detections( @@ -137,7 +139,7 @@ def test_scenario_2_missed_detection(self): # Medium objects should have 0.0 mAP (missed detection) assert abs(result.medium_objects.map50_95 - 0.0) < 1e-6 - def test_scenario_3_false_positive(self): + def test_scenario_3_false_positive(self) -> None: """Scenario 3: No GT, Prediction Present (False Positive)""" # Small object - area = 30*30 = 900 < 1024 small_detection = Detections( @@ -176,7 +178,7 @@ def test_scenario_3_false_positive(self): # Medium objects should have -1 mAP (false positive, matching pycocotools) assert result.medium_objects.map50_95 == -1 - def test_scenario_4_no_data(self): + def test_scenario_4_no_data(self) -> None: """Scenario 4: No GT, No Prediction (Category has no data)""" # Small object - area = 30*30 = 900 < 1024 small_detection = Detections( @@ -226,7 +228,7 @@ def test_scenario_4_no_data(self): # Medium objects should have -1 mAP (no data, matching pycocotools) assert result.medium_objects.map50_95 == -1 - def test_scenario_5_only_one_class_present(self): + def test_scenario_5_only_one_class_present(self) -> None: """Scenario 5: Only 1 of 3 Classes Present (Perfect Match)""" # Only class 0 objects with perfect matches detections_class_0 = [ @@ -255,7 +257,7 @@ def test_scenario_5_only_one_class_present(self): def test_mixed_classes_with_missing_detections( self, detections_50_50, targets_50_50 - ): + ) -> None: """Test mixed scenario with some classes having no detections""" # Class 1: GT exists but no prediction class_1_target = Detections( @@ -283,7 +285,7 @@ def test_mixed_classes_with_missing_detections( # Should be less than 1.0 due to missed detection and false positive assert result.map50_95 < 1.0 - def test_empty_predictions_and_targets(self): + def test_empty_predictions_and_targets(self) -> None: """Test completely empty predictions and targets""" metric = MeanAveragePrecision() metric.update([Detections.empty()], [Detections.empty()]) diff --git a/tests/metrics/test_mean_average_precision_area.py b/tests/metrics/test_mean_average_precision_area.py index fcc3991a2d..f2e9e0b156 100644 --- a/tests/metrics/test_mean_average_precision_area.py +++ b/tests/metrics/test_mean_average_precision_area.py @@ -44,7 +44,7 @@ class TestMeanAveragePrecisionArea: ) def test_area_calculation_and_size_specific_map( self, xyxy, expected_areas, expected_size_maps - ): + ) -> None: """Test area calculation and size-specific mAP functionality.""" gt = Detections( xyxy=xyxy, @@ -96,7 +96,7 @@ def test_area_calculation_and_size_specific_map( "Large objects should have no data" ) - def test_area_preserved_from_data(self): + def test_area_preserved_from_data(self) -> None: """Test that area from data field is preserved (COCO case).""" gt = Detections( xyxy=np.array( diff --git a/tests/metrics/test_mean_average_recall.py b/tests/metrics/test_mean_average_recall.py index ab36e70b66..727faba184 100644 --- a/tests/metrics/test_mean_average_recall.py +++ b/tests/metrics/test_mean_average_recall.py @@ -359,7 +359,45 @@ def three_class_single_image_detections(): return predictions, targets -def test_single_perfect_detection(): +@pytest.mark.parametrize( + "missing_attribute", + ["predictions_class_id", "targets_class_id", "predictions_confidence"], +) +def test_compute_value_error_for_missing_required_fields(missing_attribute) -> None: + """Raises ValueError when required detection fields are missing.""" + metric = MeanAverageRecall() + boxes = np.array([[10, 10, 50, 50]], dtype=np.float32) + class_id = np.array([0], dtype=np.int32) + confidence = np.array([0.9], dtype=np.float32) + + predictions = Detections( + xyxy=boxes, + confidence=confidence, + class_id=class_id, + ) + targets = Detections( + xyxy=boxes, + class_id=class_id, + ) + + if missing_attribute == "predictions_class_id": + predictions = Detections( + xyxy=boxes, + confidence=confidence, + ) + elif missing_attribute == "targets_class_id": + targets = Detections(xyxy=boxes) + else: + predictions = Detections( + xyxy=boxes, + class_id=class_id, + ) + + with pytest.raises(ValueError, match="MeanAverageRecall metric requires"): + metric.update(predictions, targets).compute() + + +def test_single_perfect_detection() -> None: """Test that a single perfect detection yields 1.0 recall.""" detections = Detections( xyxy=np.array([[10, 10, 50, 50]], dtype=np.float32), @@ -377,7 +415,7 @@ def test_single_perfect_detection(): def test_complex_integration_scenario( complex_scenario_predictions, complex_scenario_targets -): +) -> None: """Test integration scenario with multiple images and varying performance.""" def mock_detections_list(boxes_list): @@ -403,7 +441,9 @@ def mock_detections_list(boxes_list): np.testing.assert_almost_equal(result.recall_scores, expected_result, decimal=6) -def test_mar_at_k_limits_per_image_not_per_class(two_class_two_image_detections): +def test_mar_at_k_limits_per_image_not_per_class( + two_class_two_image_detections, +) -> None: """ Test that `mAR @ K` limits detections per image, not per class. @@ -458,7 +498,7 @@ def test_mar_at_k_limits_per_image_not_per_class(two_class_two_image_detections) ) -def test_three_class_single_image_scenario(three_class_single_image_detections): +def test_three_class_single_image_scenario(three_class_single_image_detections) -> None: """ Test with 3 classes on single image - explicit N x K bug reproduction. @@ -505,7 +545,7 @@ def test_three_class_single_image_scenario(three_class_single_image_detections): ) -def test_dataset_split_integration(yolo_dataset_two_classes): +def test_dataset_split_integration(yolo_dataset_two_classes) -> None: """ Test mAR with a roboflow-format dataset loaded from disk. diff --git a/tests/metrics/test_precision.py b/tests/metrics/test_precision.py index 5c9303ec84..e57b757628 100644 --- a/tests/metrics/test_precision.py +++ b/tests/metrics/test_precision.py @@ -36,7 +36,7 @@ def targets_multiple_classes(self): class_id=np.array([0, 0, 1]), ) - def test_initialization_default(self): + def test_initialization_default(self) -> None: """Test that Precision can be initialized with default parameters""" metric = Precision() assert metric._metric_target == MetricTarget.BOXES @@ -44,7 +44,7 @@ def test_initialization_default(self): assert metric._predictions_list == [] assert metric._targets_list == [] - def test_initialization_custom(self): + def test_initialization_custom(self) -> None: """Test that Precision can be initialized with custom parameters""" metric = Precision( metric_target=MetricTarget.MASKS, @@ -53,7 +53,7 @@ def test_initialization_custom(self): assert metric._metric_target == MetricTarget.MASKS assert metric.averaging_method == AveragingMethod.MACRO - def test_reset(self, dummy_prediction): + def test_reset(self, dummy_prediction) -> None: """Test that reset() clears all stored data""" metric = Precision() @@ -69,7 +69,7 @@ def test_reset(self, dummy_prediction): assert metric._predictions_list == [] assert metric._targets_list == [] - def test_perfect_match(self, detections_50_50, targets_50_50): + def test_perfect_match(self, detections_50_50, targets_50_50) -> None: """Test precision with perfect matching predictions and targets""" metric = Precision() result = metric.update(detections_50_50, targets_50_50).compute() @@ -82,7 +82,7 @@ def test_perfect_match(self, detections_50_50, targets_50_50): assert len(result.matched_classes) == 1 assert result.matched_classes[0] == 0 - def test_no_overlap(self, predictions_no_overlap, targets_no_overlap): + def test_no_overlap(self, predictions_no_overlap, targets_no_overlap) -> None: """Test precision with predictions that don't overlap with targets""" metric = Precision() result = metric.update(predictions_no_overlap, targets_no_overlap).compute() @@ -92,7 +92,7 @@ def test_no_overlap(self, predictions_no_overlap, targets_no_overlap): assert result.precision_at_50 == 0.0 assert result.precision_at_75 == 0.0 - def test_empty_predictions(self, targets_50_50): + def test_empty_predictions(self, targets_50_50) -> None: """Test precision with empty predictions but existing targets""" predictions = Detections.empty() @@ -103,7 +103,7 @@ def test_empty_predictions(self, targets_50_50): assert result.precision_at_50 == 0.0 assert result.precision_at_75 == 0.0 - def test_empty_targets(self, detections_50_50): + def test_empty_targets(self, detections_50_50) -> None: """Test precision with predictions but no targets""" targets = Detections.empty() @@ -115,7 +115,7 @@ def test_empty_targets(self, detections_50_50): assert result.precision_at_50 == 0.0 assert result.precision_at_75 == 0.0 - def test_single_class(self, predictions_confidence_ranking, targets_50_50): + def test_single_class(self, predictions_confidence_ranking, targets_50_50) -> None: """Test precision calculation for single class with mixed results""" metric = Precision() result = metric.update(predictions_confidence_ranking, targets_50_50).compute() @@ -127,7 +127,7 @@ def test_single_class(self, predictions_confidence_ranking, targets_50_50): def test_multiple_classes( self, predictions_multiple_classes, targets_multiple_classes - ): + ) -> None: """Test precision calculation for multiple classes""" metric = Precision() result = metric.update( @@ -144,7 +144,9 @@ def test_multiple_classes( assert 0 in result.matched_classes assert 1 in result.matched_classes - def test_different_iou_thresholds(self, predictions_iou_064, targets_iou_064): + def test_different_iou_thresholds( + self, predictions_iou_064, targets_iou_064 + ) -> None: """Test precision at different IoU thresholds""" metric = Precision() result = metric.update(predictions_iou_064, targets_iou_064).compute() @@ -154,7 +156,9 @@ def test_different_iou_thresholds(self, predictions_iou_064, targets_iou_064): assert result.precision_at_50 == 1.0 # TP=1, FP=0 assert result.precision_at_75 == 0.0 # TP=0, FP=1 - def test_confidence_ranking(self, predictions_confidence_ranking, targets_50_50): + def test_confidence_ranking( + self, predictions_confidence_ranking, targets_50_50 + ) -> None: """Test that predictions are ranked by confidence""" metric = Precision() result = metric.update(predictions_confidence_ranking, targets_50_50).compute() @@ -165,7 +169,7 @@ def test_confidence_ranking(self, predictions_confidence_ranking, targets_50_50) def test_list_inputs( self, detections_50_50, targets_50_50, prediction_class_1, target_class_1 - ): + ) -> None: """Test precision with list inputs""" metric = Precision() result = metric.update( @@ -176,7 +180,7 @@ def test_list_inputs( assert result.precision_at_50 == 1.0 assert result.precision_at_75 == 1.0 - def test_mismatched_list_lengths(self, detections_50_50, targets_50_50): + def test_mismatched_list_lengths(self, detections_50_50, targets_50_50) -> None: """Test that mismatched prediction/target list lengths raise error""" metric = Precision() @@ -184,11 +188,52 @@ def test_mismatched_list_lengths(self, detections_50_50, targets_50_50): with pytest.raises(ValueError, match="number of predictions"): metric.update([detections_50_50], [targets_50_50, targets_50_50]) + @pytest.mark.parametrize( + "missing_attribute", + ["predictions_class_id", "targets_class_id", "predictions_confidence"], + ) + def test_compute_value_error_for_missing_required_fields( + self, missing_attribute + ) -> None: + """Raises ValueError when required detection fields are missing.""" + metric = Precision() + boxes = np.array([[10, 10, 50, 50]], dtype=np.float32) + class_id = np.array([0], dtype=np.int32) + confidence = np.array([0.9], dtype=np.float32) + + predictions = Detections( + xyxy=boxes, + confidence=confidence, + class_id=class_id, + ) + targets = Detections( + xyxy=boxes, + class_id=class_id, + ) + + if missing_attribute == "predictions_class_id": + predictions = Detections( + xyxy=boxes, + confidence=confidence, + ) + elif missing_attribute == "targets_class_id": + targets = Detections(xyxy=boxes) + else: + predictions = Detections( + xyxy=boxes, + class_id=class_id, + ) + + with pytest.raises(ValueError, match="Precision metric requires"): + metric.update(predictions, targets).compute() + @pytest.mark.parametrize( "averaging_method", [AveragingMethod.MACRO, AveragingMethod.MICRO, AveragingMethod.WEIGHTED], ) - def test_averaging_methods(self, averaging_method, detections_50_50, targets_50_50): + def test_averaging_methods( + self, averaging_method, detections_50_50, targets_50_50 + ) -> None: """Test different averaging methods""" metric = Precision(averaging_method=averaging_method) result = metric.update(detections_50_50, targets_50_50).compute() diff --git a/tests/metrics/test_recall.py b/tests/metrics/test_recall.py index 4772ee3aa0..8f83e1677f 100644 --- a/tests/metrics/test_recall.py +++ b/tests/metrics/test_recall.py @@ -36,7 +36,7 @@ def targets_multiple_classes(self): class_id=np.array([0, 0, 1]), ) - def test_initialization_default(self): + def test_initialization_default(self) -> None: """Test that Recall can be initialized with default parameters""" metric = Recall() assert metric._metric_target == MetricTarget.BOXES @@ -44,7 +44,7 @@ def test_initialization_default(self): assert metric._predictions_list == [] assert metric._targets_list == [] - def test_initialization_custom(self): + def test_initialization_custom(self) -> None: """Test that Recall can be initialized with custom parameters""" metric = Recall( metric_target=MetricTarget.MASKS, @@ -53,7 +53,7 @@ def test_initialization_custom(self): assert metric._metric_target == MetricTarget.MASKS assert metric.averaging_method == AveragingMethod.MACRO - def test_reset(self, dummy_prediction): + def test_reset(self, dummy_prediction) -> None: """Test that reset() clears all stored data""" metric = Recall() @@ -69,7 +69,7 @@ def test_reset(self, dummy_prediction): assert metric._predictions_list == [] assert metric._targets_list == [] - def test_perfect_match(self, detections_50_50, targets_50_50): + def test_perfect_match(self, detections_50_50, targets_50_50) -> None: """Test recall with perfect matching predictions and targets""" metric = Recall() result = metric.update(detections_50_50, targets_50_50).compute() @@ -81,7 +81,7 @@ def test_perfect_match(self, detections_50_50, targets_50_50): assert len(result.matched_classes) == 1 assert result.matched_classes[0] == 0 - def test_no_overlap(self, predictions_no_overlap, targets_no_overlap): + def test_no_overlap(self, predictions_no_overlap, targets_no_overlap) -> None: """Test recall with predictions that don't overlap with targets""" metric = Recall() result = metric.update(predictions_no_overlap, targets_no_overlap).compute() @@ -91,7 +91,7 @@ def test_no_overlap(self, predictions_no_overlap, targets_no_overlap): assert result.recall_at_50 == 0.0 assert result.recall_at_75 == 0.0 - def test_empty_predictions(self, targets_50_50): + def test_empty_predictions(self, targets_50_50) -> None: """Test recall with empty predictions but existing targets""" predictions = Detections.empty() @@ -102,7 +102,7 @@ def test_empty_predictions(self, targets_50_50): assert result.recall_at_50 == 0.0 assert result.recall_at_75 == 0.0 - def test_empty_targets(self, detections_50_50): + def test_empty_targets(self, detections_50_50) -> None: """Test recall with predictions but no targets""" targets = Detections.empty() @@ -115,7 +115,7 @@ def test_empty_targets(self, detections_50_50): def test_single_class_missed_detections( self, detections_50_50, targets_two_objects_class_0 - ): + ) -> None: """Test recall calculation with some missed detections""" metric = Recall() result = metric.update(detections_50_50, targets_two_objects_class_0).compute() @@ -127,7 +127,7 @@ def test_single_class_missed_detections( def test_multiple_classes( self, predictions_multiple_classes, targets_multiple_classes - ): + ) -> None: """Test recall calculation for multiple classes""" metric = Recall() result = metric.update( @@ -144,7 +144,9 @@ def test_multiple_classes( assert 0 in result.matched_classes assert 1 in result.matched_classes - def test_different_iou_thresholds(self, predictions_iou_064, targets_iou_064): + def test_different_iou_thresholds( + self, predictions_iou_064, targets_iou_064 + ) -> None: """Test recall at different IoU thresholds""" metric = Recall() result = metric.update(predictions_iou_064, targets_iou_064).compute() @@ -154,7 +156,9 @@ def test_different_iou_thresholds(self, predictions_iou_064, targets_iou_064): assert result.recall_at_50 == 1.0 # TP=1, FN=0 assert result.recall_at_75 == 0.0 # TP=0, FN=1 - def test_confidence_ranking(self, predictions_confidence_ranking, targets_50_50): + def test_confidence_ranking( + self, predictions_confidence_ranking, targets_50_50 + ) -> None: """Test that higher confidence predictions are preferred for matching""" metric = Recall() result = metric.update(predictions_confidence_ranking, targets_50_50).compute() @@ -165,7 +169,7 @@ def test_confidence_ranking(self, predictions_confidence_ranking, targets_50_50) def test_multiple_predictions_one_target( self, predictions_confidence_ranking, targets_50_50 - ): + ) -> None: """Test recall when multiple predictions compete for one target""" metric = Recall() result = metric.update(predictions_confidence_ranking, targets_50_50).compute() @@ -176,7 +180,7 @@ def test_multiple_predictions_one_target( def test_list_inputs( self, detections_50_50, targets_50_50, prediction_class_1, target_class_1 - ): + ) -> None: """Test recall with list inputs""" metric = Recall() result = metric.update( @@ -187,7 +191,7 @@ def test_list_inputs( assert result.recall_at_50 == 1.0 assert result.recall_at_75 == 1.0 - def test_mismatched_list_lengths(self, detections_50_50, targets_50_50): + def test_mismatched_list_lengths(self, detections_50_50, targets_50_50) -> None: """Test that mismatched prediction/target list lengths raise error""" metric = Recall() @@ -195,11 +199,52 @@ def test_mismatched_list_lengths(self, detections_50_50, targets_50_50): with pytest.raises(ValueError, match="number of predictions"): metric.update([detections_50_50], [targets_50_50, targets_50_50]) + @pytest.mark.parametrize( + "missing_attribute", + ["predictions_class_id", "targets_class_id", "predictions_confidence"], + ) + def test_compute_value_error_for_missing_required_fields( + self, missing_attribute + ) -> None: + """Raises ValueError when required detection fields are missing.""" + metric = Recall() + boxes = np.array([[10, 10, 50, 50]], dtype=np.float32) + class_id = np.array([0], dtype=np.int32) + confidence = np.array([0.9], dtype=np.float32) + + predictions = Detections( + xyxy=boxes, + confidence=confidence, + class_id=class_id, + ) + targets = Detections( + xyxy=boxes, + class_id=class_id, + ) + + if missing_attribute == "predictions_class_id": + predictions = Detections( + xyxy=boxes, + confidence=confidence, + ) + elif missing_attribute == "targets_class_id": + targets = Detections(xyxy=boxes) + else: + predictions = Detections( + xyxy=boxes, + class_id=class_id, + ) + + with pytest.raises(ValueError, match="Recall metric requires"): + metric.update(predictions, targets).compute() + @pytest.mark.parametrize( "averaging_method", [AveragingMethod.MACRO, AveragingMethod.MICRO, AveragingMethod.WEIGHTED], ) - def test_averaging_methods(self, averaging_method, detections_50_50, targets_50_50): + def test_averaging_methods( + self, averaging_method, detections_50_50, targets_50_50 + ) -> None: """Test different averaging methods""" metric = Recall(averaging_method=averaging_method) result = metric.update(detections_50_50, targets_50_50).compute() @@ -208,7 +253,7 @@ def test_averaging_methods(self, averaging_method, detections_50_50, targets_50_ assert result.recall_at_50 == 1.0 assert result.averaging_method == averaging_method - def test_macro_averaging(self): + def test_macro_averaging(self) -> None: """Test MACRO averaging with specific example""" # Class 0: 1/2 targets matched -> recall = 0.5 # Class 1: 1/1 targets matched -> recall = 1.0 diff --git a/tests/utils/test_file.py b/tests/utils/test_file.py index 3b88a6e87b..79a9701da0 100644 --- a/tests/utils/test_file.py +++ b/tests/utils/test_file.py @@ -63,7 +63,7 @@ def test_read_txt_file( skip_empty: bool, expected_result: list[str] | None, exception: Exception, -): +) -> None: with exception: result = read_txt_file(file_name, skip_empty) assert result == expected_result diff --git a/tests/utils/test_image.py b/tests/utils/test_image.py index f7e68ecd4e..2d0226eaf5 100644 --- a/tests/utils/test_image.py +++ b/tests/utils/test_image.py @@ -131,7 +131,7 @@ def test_letterbox_image_for_pillow_image() -> None: ), ], ) -def test_crop_image(image, xyxy, expected_size): +def test_crop_image(image, xyxy, expected_size) -> None: cropped = crop_image(image=image, xyxy=xyxy) if isinstance(image, np.ndarray): assert isinstance(cropped, np.ndarray) @@ -155,6 +155,6 @@ def test_crop_image(image, xyxy, expected_size): (Image.new("L", (20, 10), color=0), (20, 10)), ], ) -def test_get_image_resolution_wh(image, expected): +def test_get_image_resolution_wh(image, expected) -> None: resolution = get_image_resolution_wh(image) assert resolution == expected diff --git a/tests/utils/test_internal.py b/tests/utils/test_internal.py index eb4cab411d..6db27855c5 100644 --- a/tests/utils/test_internal.py +++ b/tests/utils/test_internal.py @@ -10,30 +10,30 @@ class MockClass: - def __init__(self): + def __init__(self) -> None: self.public = 0 self._protected = 1 self.__private = 2 - def public_method(self): + def public_method(self) -> None: pass - def _protected_method(self): + def _protected_method(self) -> None: pass - def __private_method(self): + def __private_method(self) -> None: pass @property - def public_property(self): + def public_property(self) -> int: return 0 @property - def _protected_property(self): + def _protected_property(self) -> int: return 1 @property - def __private_property(self): + def __private_property(self) -> int: return 2 @@ -51,25 +51,25 @@ class MockDataclass: _protected_field_with_factory: dict = field(default_factory=dict) __private_field_with_factory: dict = field(default_factory=dict) - def public_method(self): + def public_method(self) -> None: pass - def _protected_method(self): + def _protected_method(self) -> None: pass - def __private_method(self): + def __private_method(self) -> None: pass @property - def public_property(self): + def public_property(self) -> int: return 0 @property - def _protected_property(self): + def _protected_property(self) -> int: return 1 @property - def __private_property(self): + def __private_property(self) -> int: return 2 diff --git a/tests/utils/test_logger.py b/tests/utils/test_logger.py index bf3bc1df2f..8370d5e8a6 100644 --- a/tests/utils/test_logger.py +++ b/tests/utils/test_logger.py @@ -8,46 +8,46 @@ class TestGetLogger: - def test_default_name(self): + def test_default_name(self) -> None: """Logger is created with default name.""" logger = _get_logger() assert logger.name == "supervision" - def test_custom_name(self): + def test_custom_name(self) -> None: """Logger is created with provided name.""" logger = _get_logger("supervision.test_module") assert logger.name == "supervision.test_module" - def test_default_level_is_info(self): + def test_default_level_is_info(self) -> None: """Logger defaults to INFO level when LOG_LEVEL env var is not set.""" with patch.dict("os.environ", {}, clear=True): # Use a unique name to avoid cached logger state from other tests logger = _get_logger("supervision.test_default_level") assert logger.level == logging.INFO - def test_explicit_level(self): + def test_explicit_level(self) -> None: """Logger uses the explicitly provided level.""" logger = _get_logger("supervision.test_explicit_level", level=logging.DEBUG) assert logger.level == logging.DEBUG - def test_log_level_env_var(self): + def test_log_level_env_var(self) -> None: """Logger respects the LOG_LEVEL environment variable.""" with patch.dict("os.environ", {"LOG_LEVEL": "DEBUG"}): logger = _get_logger("supervision.test_env_level") assert logger.level == logging.DEBUG - def test_log_level_env_var_warning(self): + def test_log_level_env_var_warning(self) -> None: """Logger respects the LOG_LEVEL=WARNING environment variable.""" with patch.dict("os.environ", {"LOG_LEVEL": "WARNING"}): logger = _get_logger("supervision.test_env_warning") assert logger.level == logging.WARNING - def test_two_handlers_configured(self): + def test_two_handlers_configured(self) -> None: """Logger has exactly two handlers: one for stdout, one for stderr.""" logger = _get_logger("supervision.test_handlers") assert len(logger.handlers) == 2 - def test_stdout_handler_present(self): + def test_stdout_handler_present(self) -> None: """Logger has a StreamHandler pointing to stdout.""" logger = _get_logger("supervision.test_stdout") stdout_handlers = [ @@ -57,7 +57,7 @@ def test_stdout_handler_present(self): ] assert len(stdout_handlers) == 1 - def test_stderr_handler_present(self): + def test_stderr_handler_present(self) -> None: """Logger has a StreamHandler pointing to stderr for warnings.""" logger = _get_logger("supervision.test_stderr") stderr_handlers = [ @@ -67,12 +67,12 @@ def test_stderr_handler_present(self): ] assert len(stderr_handlers) == 1 - def test_no_propagation(self): + def test_no_propagation(self) -> None: """Logger does not propagate to the root logger.""" logger = _get_logger("supervision.test_propagation") assert not logger.propagate - def test_idempotent_no_duplicate_handlers(self): + def test_idempotent_no_duplicate_handlers(self) -> None: """Calling _get_logger twice with the same name does not add extra handlers.""" name = "supervision.test_idempotent" logger1 = _get_logger(name) diff --git a/tests/utils/test_video.py b/tests/utils/test_video.py index c2df5a4607..2310ec8020 100644 --- a/tests/utils/test_video.py +++ b/tests/utils/test_video.py @@ -26,7 +26,7 @@ def dummy_video_path(tmp_path): return path -def test_process_video_exception_handling(dummy_video_path, tmp_path): +def test_process_video_exception_handling(dummy_video_path, tmp_path) -> None: """ Verify that process_video correctly propagates exceptions from the callback. @@ -49,7 +49,7 @@ def callback_with_exception(frame, index): ) -def test_process_video_success(dummy_video_path, tmp_path): +def test_process_video_success(dummy_video_path, tmp_path) -> None: """ Verify successful video processing with a pass-through callback. @@ -70,7 +70,7 @@ def callback_success(frame, index): assert os.path.exists(target_path) -def test_process_video_exception_with_small_buffer(dummy_video_path, tmp_path): +def test_process_video_exception_with_small_buffer(dummy_video_path, tmp_path) -> None: """ Verify that process_video handles exceptions correctly even with small buffers. @@ -94,7 +94,7 @@ def callback_with_exception(frame, index): ) -def test_process_video_max_frames(dummy_video_path, tmp_path): +def test_process_video_max_frames(dummy_video_path, tmp_path) -> None: """ Verify that process_video respects the max_frames parameter. @@ -120,7 +120,7 @@ def callback(frame, index): assert processed_indices == [0, 1, 2, 3, 4] -def test_process_video_custom_params(dummy_video_path, tmp_path): +def test_process_video_custom_params(dummy_video_path, tmp_path) -> None: """ Verify that process_video works correctly with custom performance parameters. @@ -145,7 +145,7 @@ def callback(frame, index): assert os.path.exists(target_path) -def test_video_info(dummy_video_path): +def test_video_info(dummy_video_path) -> None: """ Verify that VideoInfo correctly retrieves metadata from a video file. @@ -162,7 +162,7 @@ def test_video_info(dummy_video_path): assert video_info.resolution_wh == (640, 480) -def test_video_info_float_fps(dummy_video_path, monkeypatch): +def test_video_info_float_fps(dummy_video_path, monkeypatch) -> None: """ Verify that VideoInfo preserves non-integer FPS values as floats. @@ -185,7 +185,7 @@ def mocked_get(self, prop_id): assert video_info.fps != int(video_info.fps) -def test_get_video_frames_generator(dummy_video_path): +def test_get_video_frames_generator(dummy_video_path) -> None: """ Verify that get_video_frames_generator yields frames with correct shapes. @@ -200,7 +200,7 @@ def test_get_video_frames_generator(dummy_video_path): assert all(frame.shape == (480, 640, 3) for frame in frames) -def test_get_video_frames_generator_with_stride(dummy_video_path): +def test_get_video_frames_generator_with_stride(dummy_video_path) -> None: """ Verify that get_video_frames_generator correctly handles the stride parameter. @@ -213,7 +213,7 @@ def test_get_video_frames_generator_with_stride(dummy_video_path): assert len(frames) == 5 -def test_process_video_preserve_audio_calls_mux(dummy_video_path, tmp_path): +def test_process_video_preserve_audio_calls_mux(dummy_video_path, tmp_path) -> None: """ Verify that process_video calls _mux_audio when preserve_audio=True. @@ -235,7 +235,7 @@ def test_process_video_preserve_audio_calls_mux(dummy_video_path, tmp_path): ) -def test_process_video_no_audio_by_default(dummy_video_path, tmp_path): +def test_process_video_no_audio_by_default(dummy_video_path, tmp_path) -> None: """ Verify that process_video does not call _mux_audio when preserve_audio=False. @@ -272,7 +272,7 @@ def test_process_video_no_audio_by_default(dummy_video_path, tmp_path): ) def test_mux_audio_file_unchanged_on_failure( dummy_video_path, tmp_path, which_rv, run_kwargs -): +) -> None: """_mux_audio leaves the output file unchanged when muxing cannot complete.""" target_path = str(tmp_path / "video.mp4") shutil.copy(dummy_video_path, target_path) @@ -287,7 +287,7 @@ def test_mux_audio_file_unchanged_on_failure( assert os.path.getsize(target_path) == original_size -def test_mux_audio_replaces_file_on_success(dummy_video_path, tmp_path): +def test_mux_audio_replaces_file_on_success(dummy_video_path, tmp_path) -> None: """_mux_audio calls os.replace with video_path as destination on success.""" target_path = str(tmp_path / "video.mp4") shutil.copy(dummy_video_path, target_path) @@ -307,7 +307,7 @@ def test_mux_audio_replaces_file_on_success(dummy_video_path, tmp_path): assert mock_replace.call_args[0][1] == target_path -def test_get_video_frames_generator_with_start_end(dummy_video_path): +def test_get_video_frames_generator_with_start_end(dummy_video_path) -> None: """ Verify that get_video_frames_generator respects start and end frame indices. diff --git a/uv.lock b/uv.lock index bd3804448b..77be3138f4 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.9" +requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.12' and sys_platform == 'darwin'", "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", @@ -8,12 +8,9 @@ resolution-markers = [ "python_full_version == '3.11.*' and sys_platform == 'darwin'", "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", ] [[package]] @@ -57,8 +54,8 @@ name = "argon2-cffi-bindings" version = "21.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.10' and sys_platform == 'darwin') or (platform_machine == 'arm64' and sys_platform == 'darwin')" }, - { name = "cffi", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.10' and platform_machine != 'arm64') or sys_platform != 'darwin'" }, + { name = "cffi", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin'" }, + { name = "cffi", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/e9/184b8ccce6683b0aa2fbb7ba5683ea4b9c5763f1356347f1312c32e3c66e/argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3", size = 1779911, upload-time = "2021-12-01T08:52:55.68Z" } wheels = [ @@ -184,7 +181,7 @@ name = "build" version = "1.2.2.post1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "(python_full_version < '3.10' and os_name == 'nt' and platform_machine != 'arm64' and sys_platform == 'darwin') or (os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "colorama", marker = "(os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" }, { name = "packaging" }, { name = "pyproject-hooks" }, @@ -209,8 +206,8 @@ name = "cairocffi" version = "1.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.10' and sys_platform == 'darwin') or (platform_machine == 'arm64' and sys_platform == 'darwin')" }, - { name = "cffi", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.10' and platform_machine != 'arm64') or sys_platform != 'darwin'" }, + { name = "cffi", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin'" }, + { name = "cffi", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/70/c5/1a4dc131459e68a173cbdab5fad6b524f53f9c1ef7861b7698e998b837cc/cairocffi-1.7.1.tar.gz", hash = "sha256:2e48ee864884ec4a3a34bfa8c9ab9999f688286eb714a15a43ec9d068c36557b", size = 88096, upload-time = "2024-06-18T10:56:06.741Z" } wheels = [ @@ -249,11 +246,10 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.12' and sys_platform == 'darwin'", "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", ] dependencies = [ - { name = "pycparser", marker = "(python_full_version >= '3.10' and sys_platform == 'darwin') or (platform_machine == 'arm64' and sys_platform == 'darwin')" }, + { name = "pycparser", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } wheels = [ @@ -265,8 +261,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, - { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220, upload-time = "2024-09-04T20:45:01.577Z" }, - { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605, upload-time = "2024-09-04T20:45:03.837Z" }, ] [[package]] @@ -278,18 +272,14 @@ resolution-markers = [ "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", ] dependencies = [ - { name = "pycparser", marker = "(python_full_version < '3.10' and implementation_name != 'PyPy' and platform_machine != 'arm64') or (implementation_name != 'PyPy' and sys_platform != 'darwin')" }, + { name = "pycparser", marker = "implementation_name != 'PyPy' and sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, @@ -300,8 +290,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, - { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, @@ -313,8 +301,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, @@ -325,8 +311,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, @@ -337,8 +321,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, @@ -348,8 +330,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, @@ -359,49 +339,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, - { url = "https://files.pythonhosted.org/packages/c0/cc/08ed5a43f2996a16b462f64a7055c6e962803534924b9b2f1371d8c00b7b/cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf", size = 184288, upload-time = "2025-09-08T23:23:48.404Z" }, - { url = "https://files.pythonhosted.org/packages/3d/de/38d9726324e127f727b4ecc376bc85e505bfe61ef130eaf3f290c6847dd4/cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", size = 180509, upload-time = "2025-09-08T23:23:49.73Z" }, - { url = "https://files.pythonhosted.org/packages/9b/13/c92e36358fbcc39cf0962e83223c9522154ee8630e1df7c0b3a39a8124e2/cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", size = 208813, upload-time = "2025-09-08T23:23:51.263Z" }, - { url = "https://files.pythonhosted.org/packages/15/12/a7a79bd0df4c3bff744b2d7e52cc1b68d5e7e427b384252c42366dc1ecbc/cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", size = 216498, upload-time = "2025-09-08T23:23:52.494Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ad/5c51c1c7600bdd7ed9a24a203ec255dccdd0ebf4527f7b922a0bde2fb6ed/cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", size = 203243, upload-time = "2025-09-08T23:23:53.836Z" }, - { url = "https://files.pythonhosted.org/packages/32/f2/81b63e288295928739d715d00952c8c6034cb6c6a516b17d37e0c8be5600/cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", size = 203158, upload-time = "2025-09-08T23:23:55.169Z" }, - { url = "https://files.pythonhosted.org/packages/1f/74/cc4096ce66f5939042ae094e2e96f53426a979864aa1f96a621ad128be27/cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", size = 216548, upload-time = "2025-09-08T23:23:56.506Z" }, - { url = "https://files.pythonhosted.org/packages/e8/be/f6424d1dc46b1091ffcc8964fa7c0ab0cd36839dd2761b49c90481a6ba1b/cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", size = 218897, upload-time = "2025-09-08T23:23:57.825Z" }, - { url = "https://files.pythonhosted.org/packages/f7/e0/dda537c2309817edf60109e39265f24f24aa7f050767e22c98c53fe7f48b/cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", size = 211249, upload-time = "2025-09-08T23:23:59.139Z" }, - { url = "https://files.pythonhosted.org/packages/2b/e7/7c769804eb75e4c4b35e658dba01de1640a351a9653c3d49ca89d16ccc91/cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", size = 218041, upload-time = "2025-09-08T23:24:00.496Z" }, - { url = "https://files.pythonhosted.org/packages/aa/d9/6218d78f920dcd7507fc16a766b5ef8f3b913cc7aa938e7fc80b9978d089/cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", size = 172138, upload-time = "2025-09-08T23:24:01.7Z" }, - { url = "https://files.pythonhosted.org/packages/54/8f/a1e836f82d8e32a97e6b29cc8f641779181ac7363734f12df27db803ebda/cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", size = 182794, upload-time = "2025-09-08T23:24:02.943Z" }, -] - -[[package]] -name = "cfgv" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, ] [[package]] name = "cfgv" version = "3.5.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, @@ -474,56 +417,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, - { url = "https://files.pythonhosted.org/packages/28/f8/dfb01ff6cc9af38552c69c9027501ff5a5117c4cc18dcd27cb5259fa1888/charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", size = 201671, upload-time = "2025-05-02T08:34:12.696Z" }, - { url = "https://files.pythonhosted.org/packages/32/fb/74e26ee556a9dbfe3bd264289b67be1e6d616329403036f6507bb9f3f29c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", size = 144744, upload-time = "2025-05-02T08:34:14.665Z" }, - { url = "https://files.pythonhosted.org/packages/ad/06/8499ee5aa7addc6f6d72e068691826ff093329fe59891e83b092ae4c851c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", size = 154993, upload-time = "2025-05-02T08:34:17.134Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a2/5e4c187680728219254ef107a6949c60ee0e9a916a5dadb148c7ae82459c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", size = 147382, upload-time = "2025-05-02T08:34:19.081Z" }, - { url = "https://files.pythonhosted.org/packages/4c/fe/56aca740dda674f0cc1ba1418c4d84534be51f639b5f98f538b332dc9a95/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", size = 149536, upload-time = "2025-05-02T08:34:21.073Z" }, - { url = "https://files.pythonhosted.org/packages/53/13/db2e7779f892386b589173dd689c1b1e304621c5792046edd8a978cbf9e0/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", size = 151349, upload-time = "2025-05-02T08:34:23.193Z" }, - { url = "https://files.pythonhosted.org/packages/69/35/e52ab9a276186f729bce7a0638585d2982f50402046e4b0faa5d2c3ef2da/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", size = 146365, upload-time = "2025-05-02T08:34:25.187Z" }, - { url = "https://files.pythonhosted.org/packages/a6/d8/af7333f732fc2e7635867d56cb7c349c28c7094910c72267586947561b4b/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", size = 154499, upload-time = "2025-05-02T08:34:27.359Z" }, - { url = "https://files.pythonhosted.org/packages/7a/3d/a5b2e48acef264d71e036ff30bcc49e51bde80219bb628ba3e00cf59baac/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", size = 157735, upload-time = "2025-05-02T08:34:29.798Z" }, - { url = "https://files.pythonhosted.org/packages/85/d8/23e2c112532a29f3eef374375a8684a4f3b8e784f62b01da931186f43494/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", size = 154786, upload-time = "2025-05-02T08:34:31.858Z" }, - { url = "https://files.pythonhosted.org/packages/c7/57/93e0169f08ecc20fe82d12254a200dfaceddc1c12a4077bf454ecc597e33/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", size = 150203, upload-time = "2025-05-02T08:34:33.88Z" }, - { url = "https://files.pythonhosted.org/packages/2c/9d/9bf2b005138e7e060d7ebdec7503d0ef3240141587651f4b445bdf7286c2/charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", size = 98436, upload-time = "2025-05-02T08:34:35.907Z" }, - { url = "https://files.pythonhosted.org/packages/6d/24/5849d46cf4311bbf21b424c443b09b459f5b436b1558c04e45dbb7cc478b/charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", size = 105772, upload-time = "2025-05-02T08:34:37.935Z" }, { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, ] -[[package]] -name = "click" -version = "8.1.8" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, -] - [[package]] name = "click" version = "8.2.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } wheels = [ @@ -551,103 +453,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e6/75/49e5bfe642f71f272236b5b2d2691cf915a7283cc0ceda56357b61daa538/comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3", size = 7180, upload-time = "2024-03-12T16:53:39.226Z" }, ] -[[package]] -name = "contourpy" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f5/f6/31a8f28b4a2a4fa0e01085e542f3081ab0588eff8e589d39d775172c9792/contourpy-1.3.0.tar.gz", hash = "sha256:7ffa0db17717a8ffb127efd0c95a4362d996b892c2904db72428d5b52e1938a4", size = 13464370, upload-time = "2024-08-27T21:00:03.328Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/e0/be8dcc796cfdd96708933e0e2da99ba4bb8f9b2caa9d560a50f3f09a65f3/contourpy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:880ea32e5c774634f9fcd46504bf9f080a41ad855f4fef54f5380f5133d343c7", size = 265366, upload-time = "2024-08-27T20:50:09.947Z" }, - { url = "https://files.pythonhosted.org/packages/50/d6/c953b400219443535d412fcbbc42e7a5e823291236bc0bb88936e3cc9317/contourpy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76c905ef940a4474a6289c71d53122a4f77766eef23c03cd57016ce19d0f7b42", size = 249226, upload-time = "2024-08-27T20:50:16.1Z" }, - { url = "https://files.pythonhosted.org/packages/6f/b4/6fffdf213ffccc28483c524b9dad46bb78332851133b36ad354b856ddc7c/contourpy-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92f8557cbb07415a4d6fa191f20fd9d2d9eb9c0b61d1b2f52a8926e43c6e9af7", size = 308460, upload-time = "2024-08-27T20:50:22.536Z" }, - { url = "https://files.pythonhosted.org/packages/cf/6c/118fc917b4050f0afe07179a6dcbe4f3f4ec69b94f36c9e128c4af480fb8/contourpy-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36f965570cff02b874773c49bfe85562b47030805d7d8360748f3eca570f4cab", size = 347623, upload-time = "2024-08-27T20:50:28.806Z" }, - { url = "https://files.pythonhosted.org/packages/f9/a4/30ff110a81bfe3abf7b9673284d21ddce8cc1278f6f77393c91199da4c90/contourpy-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cacd81e2d4b6f89c9f8a5b69b86490152ff39afc58a95af002a398273e5ce589", size = 317761, upload-time = "2024-08-27T20:50:35.126Z" }, - { url = "https://files.pythonhosted.org/packages/99/e6/d11966962b1aa515f5586d3907ad019f4b812c04e4546cc19ebf62b5178e/contourpy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69375194457ad0fad3a839b9e29aa0b0ed53bb54db1bfb6c3ae43d111c31ce41", size = 322015, upload-time = "2024-08-27T20:50:40.318Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e3/182383743751d22b7b59c3c753277b6aee3637049197624f333dac5b4c80/contourpy-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a52040312b1a858b5e31ef28c2e865376a386c60c0e248370bbea2d3f3b760d", size = 1262672, upload-time = "2024-08-27T20:50:55.643Z" }, - { url = "https://files.pythonhosted.org/packages/78/53/974400c815b2e605f252c8fb9297e2204347d1755a5374354ee77b1ea259/contourpy-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3faeb2998e4fcb256542e8a926d08da08977f7f5e62cf733f3c211c2a5586223", size = 1321688, upload-time = "2024-08-27T20:51:11.293Z" }, - { url = "https://files.pythonhosted.org/packages/52/29/99f849faed5593b2926a68a31882af98afbeac39c7fdf7de491d9c85ec6a/contourpy-1.3.0-cp310-cp310-win32.whl", hash = "sha256:36e0cff201bcb17a0a8ecc7f454fe078437fa6bda730e695a92f2d9932bd507f", size = 171145, upload-time = "2024-08-27T20:51:15.2Z" }, - { url = "https://files.pythonhosted.org/packages/a9/97/3f89bba79ff6ff2b07a3cbc40aa693c360d5efa90d66e914f0ff03b95ec7/contourpy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:87ddffef1dbe5e669b5c2440b643d3fdd8622a348fe1983fad7a0f0ccb1cd67b", size = 216019, upload-time = "2024-08-27T20:51:19.365Z" }, - { url = "https://files.pythonhosted.org/packages/b3/1f/9375917786cb39270b0ee6634536c0e22abf225825602688990d8f5c6c19/contourpy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fa4c02abe6c446ba70d96ece336e621efa4aecae43eaa9b030ae5fb92b309ad", size = 266356, upload-time = "2024-08-27T20:51:24.146Z" }, - { url = "https://files.pythonhosted.org/packages/05/46/9256dd162ea52790c127cb58cfc3b9e3413a6e3478917d1f811d420772ec/contourpy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:834e0cfe17ba12f79963861e0f908556b2cedd52e1f75e6578801febcc6a9f49", size = 250915, upload-time = "2024-08-27T20:51:28.683Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5d/3056c167fa4486900dfbd7e26a2fdc2338dc58eee36d490a0ed3ddda5ded/contourpy-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbc4c3217eee163fa3984fd1567632b48d6dfd29216da3ded3d7b844a8014a66", size = 310443, upload-time = "2024-08-27T20:51:33.675Z" }, - { url = "https://files.pythonhosted.org/packages/ca/c2/1a612e475492e07f11c8e267ea5ec1ce0d89971be496c195e27afa97e14a/contourpy-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4865cd1d419e0c7a7bf6de1777b185eebdc51470800a9f42b9e9decf17762081", size = 348548, upload-time = "2024-08-27T20:51:39.322Z" }, - { url = "https://files.pythonhosted.org/packages/45/cf/2c2fc6bb5874158277b4faf136847f0689e1b1a1f640a36d76d52e78907c/contourpy-1.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:303c252947ab4b14c08afeb52375b26781ccd6a5ccd81abcdfc1fafd14cf93c1", size = 319118, upload-time = "2024-08-27T20:51:44.717Z" }, - { url = "https://files.pythonhosted.org/packages/03/33/003065374f38894cdf1040cef474ad0546368eea7e3a51d48b8a423961f8/contourpy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637f674226be46f6ba372fd29d9523dd977a291f66ab2a74fbeb5530bb3f445d", size = 323162, upload-time = "2024-08-27T20:51:49.683Z" }, - { url = "https://files.pythonhosted.org/packages/42/80/e637326e85e4105a802e42959f56cff2cd39a6b5ef68d5d9aee3ea5f0e4c/contourpy-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76a896b2f195b57db25d6b44e7e03f221d32fe318d03ede41f8b4d9ba1bff53c", size = 1265396, upload-time = "2024-08-27T20:52:04.926Z" }, - { url = "https://files.pythonhosted.org/packages/7c/3b/8cbd6416ca1bbc0202b50f9c13b2e0b922b64be888f9d9ee88e6cfabfb51/contourpy-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e1fd23e9d01591bab45546c089ae89d926917a66dceb3abcf01f6105d927e2cb", size = 1324297, upload-time = "2024-08-27T20:52:21.843Z" }, - { url = "https://files.pythonhosted.org/packages/4d/2c/021a7afaa52fe891f25535506cc861c30c3c4e5a1c1ce94215e04b293e72/contourpy-1.3.0-cp311-cp311-win32.whl", hash = "sha256:d402880b84df3bec6eab53cd0cf802cae6a2ef9537e70cf75e91618a3801c20c", size = 171808, upload-time = "2024-08-27T20:52:25.163Z" }, - { url = "https://files.pythonhosted.org/packages/8d/2f/804f02ff30a7fae21f98198828d0857439ec4c91a96e20cf2d6c49372966/contourpy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:6cb6cc968059db9c62cb35fbf70248f40994dfcd7aa10444bbf8b3faeb7c2d67", size = 217181, upload-time = "2024-08-27T20:52:29.13Z" }, - { url = "https://files.pythonhosted.org/packages/c9/92/8e0bbfe6b70c0e2d3d81272b58c98ac69ff1a4329f18c73bd64824d8b12e/contourpy-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:570ef7cf892f0afbe5b2ee410c507ce12e15a5fa91017a0009f79f7d93a1268f", size = 267838, upload-time = "2024-08-27T20:52:33.911Z" }, - { url = "https://files.pythonhosted.org/packages/e3/04/33351c5d5108460a8ce6d512307690b023f0cfcad5899499f5c83b9d63b1/contourpy-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da84c537cb8b97d153e9fb208c221c45605f73147bd4cadd23bdae915042aad6", size = 251549, upload-time = "2024-08-27T20:52:39.179Z" }, - { url = "https://files.pythonhosted.org/packages/51/3d/aa0fe6ae67e3ef9f178389e4caaaa68daf2f9024092aa3c6032e3d174670/contourpy-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0be4d8425bfa755e0fd76ee1e019636ccc7c29f77a7c86b4328a9eb6a26d0639", size = 303177, upload-time = "2024-08-27T20:52:44.789Z" }, - { url = "https://files.pythonhosted.org/packages/56/c3/c85a7e3e0cab635575d3b657f9535443a6f5d20fac1a1911eaa4bbe1aceb/contourpy-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c0da700bf58f6e0b65312d0a5e695179a71d0163957fa381bb3c1f72972537c", size = 341735, upload-time = "2024-08-27T20:52:51.05Z" }, - { url = "https://files.pythonhosted.org/packages/dd/8d/20f7a211a7be966a53f474bc90b1a8202e9844b3f1ef85f3ae45a77151ee/contourpy-1.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb8b141bb00fa977d9122636b16aa67d37fd40a3d8b52dd837e536d64b9a4d06", size = 314679, upload-time = "2024-08-27T20:52:58.473Z" }, - { url = "https://files.pythonhosted.org/packages/6e/be/524e377567defac0e21a46e2a529652d165fed130a0d8a863219303cee18/contourpy-1.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3634b5385c6716c258d0419c46d05c8aa7dc8cb70326c9a4fb66b69ad2b52e09", size = 320549, upload-time = "2024-08-27T20:53:06.593Z" }, - { url = "https://files.pythonhosted.org/packages/0f/96/fdb2552a172942d888915f3a6663812e9bc3d359d53dafd4289a0fb462f0/contourpy-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0dce35502151b6bd35027ac39ba6e5a44be13a68f55735c3612c568cac3805fd", size = 1263068, upload-time = "2024-08-27T20:53:23.442Z" }, - { url = "https://files.pythonhosted.org/packages/2a/25/632eab595e3140adfa92f1322bf8915f68c932bac468e89eae9974cf1c00/contourpy-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea348f053c645100612b333adc5983d87be69acdc6d77d3169c090d3b01dc35", size = 1322833, upload-time = "2024-08-27T20:53:39.243Z" }, - { url = "https://files.pythonhosted.org/packages/73/e3/69738782e315a1d26d29d71a550dbbe3eb6c653b028b150f70c1a5f4f229/contourpy-1.3.0-cp312-cp312-win32.whl", hash = "sha256:90f73a5116ad1ba7174341ef3ea5c3150ddf20b024b98fb0c3b29034752c8aeb", size = 172681, upload-time = "2024-08-27T20:53:43.05Z" }, - { url = "https://files.pythonhosted.org/packages/0c/89/9830ba00d88e43d15e53d64931e66b8792b46eb25e2050a88fec4a0df3d5/contourpy-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b11b39aea6be6764f84360fce6c82211a9db32a7c7de8fa6dd5397cf1d079c3b", size = 218283, upload-time = "2024-08-27T20:53:47.232Z" }, - { url = "https://files.pythonhosted.org/packages/53/a1/d20415febfb2267af2d7f06338e82171824d08614084714fb2c1dac9901f/contourpy-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3e1c7fa44aaae40a2247e2e8e0627f4bea3dd257014764aa644f319a5f8600e3", size = 267879, upload-time = "2024-08-27T20:53:51.597Z" }, - { url = "https://files.pythonhosted.org/packages/aa/45/5a28a3570ff6218d8bdfc291a272a20d2648104815f01f0177d103d985e1/contourpy-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:364174c2a76057feef647c802652f00953b575723062560498dc7930fc9b1cb7", size = 251573, upload-time = "2024-08-27T20:53:55.659Z" }, - { url = "https://files.pythonhosted.org/packages/39/1c/d3f51540108e3affa84f095c8b04f0aa833bb797bc8baa218a952a98117d/contourpy-1.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32b238b3b3b649e09ce9aaf51f0c261d38644bdfa35cbaf7b263457850957a84", size = 303184, upload-time = "2024-08-27T20:54:00.225Z" }, - { url = "https://files.pythonhosted.org/packages/00/56/1348a44fb6c3a558c1a3a0cd23d329d604c99d81bf5a4b58c6b71aab328f/contourpy-1.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d51fca85f9f7ad0b65b4b9fe800406d0d77017d7270d31ec3fb1cc07358fdea0", size = 340262, upload-time = "2024-08-27T20:54:05.234Z" }, - { url = "https://files.pythonhosted.org/packages/2b/23/00d665ba67e1bb666152131da07e0f24c95c3632d7722caa97fb61470eca/contourpy-1.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:732896af21716b29ab3e988d4ce14bc5133733b85956316fb0c56355f398099b", size = 313806, upload-time = "2024-08-27T20:54:09.889Z" }, - { url = "https://files.pythonhosted.org/packages/5a/42/3cf40f7040bb8362aea19af9a5fb7b32ce420f645dd1590edcee2c657cd5/contourpy-1.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d73f659398a0904e125280836ae6f88ba9b178b2fed6884f3b1f95b989d2c8da", size = 319710, upload-time = "2024-08-27T20:54:14.536Z" }, - { url = "https://files.pythonhosted.org/packages/05/32/f3bfa3fc083b25e1a7ae09197f897476ee68e7386e10404bdf9aac7391f0/contourpy-1.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c6c7c2408b7048082932cf4e641fa3b8ca848259212f51c8c59c45aa7ac18f14", size = 1264107, upload-time = "2024-08-27T20:54:29.735Z" }, - { url = "https://files.pythonhosted.org/packages/1c/1e/1019d34473a736664f2439542b890b2dc4c6245f5c0d8cdfc0ccc2cab80c/contourpy-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f317576606de89da6b7e0861cf6061f6146ead3528acabff9236458a6ba467f8", size = 1322458, upload-time = "2024-08-27T20:54:45.507Z" }, - { url = "https://files.pythonhosted.org/packages/22/85/4f8bfd83972cf8909a4d36d16b177f7b8bdd942178ea4bf877d4a380a91c/contourpy-1.3.0-cp313-cp313-win32.whl", hash = "sha256:31cd3a85dbdf1fc002280c65caa7e2b5f65e4a973fcdf70dd2fdcb9868069294", size = 172643, upload-time = "2024-08-27T20:55:52.754Z" }, - { url = "https://files.pythonhosted.org/packages/cc/4a/fb3c83c1baba64ba90443626c228ca14f19a87c51975d3b1de308dd2cf08/contourpy-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4553c421929ec95fb07b3aaca0fae668b2eb5a5203d1217ca7c34c063c53d087", size = 218301, upload-time = "2024-08-27T20:55:56.509Z" }, - { url = "https://files.pythonhosted.org/packages/76/65/702f4064f397821fea0cb493f7d3bc95a5d703e20954dce7d6d39bacf378/contourpy-1.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:345af746d7766821d05d72cb8f3845dfd08dd137101a2cb9b24de277d716def8", size = 278972, upload-time = "2024-08-27T20:54:50.347Z" }, - { url = "https://files.pythonhosted.org/packages/80/85/21f5bba56dba75c10a45ec00ad3b8190dbac7fd9a8a8c46c6116c933e9cf/contourpy-1.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3bb3808858a9dc68f6f03d319acd5f1b8a337e6cdda197f02f4b8ff67ad2057b", size = 263375, upload-time = "2024-08-27T20:54:54.909Z" }, - { url = "https://files.pythonhosted.org/packages/0a/64/084c86ab71d43149f91ab3a4054ccf18565f0a8af36abfa92b1467813ed6/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:420d39daa61aab1221567b42eecb01112908b2cab7f1b4106a52caaec8d36973", size = 307188, upload-time = "2024-08-27T20:55:00.184Z" }, - { url = "https://files.pythonhosted.org/packages/3d/ff/d61a4c288dc42da0084b8d9dc2aa219a850767165d7d9a9c364ff530b509/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d63ee447261e963af02642ffcb864e5a2ee4cbfd78080657a9880b8b1868e18", size = 345644, upload-time = "2024-08-27T20:55:05.673Z" }, - { url = "https://files.pythonhosted.org/packages/ca/aa/00d2313d35ec03f188e8f0786c2fc61f589306e02fdc158233697546fd58/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:167d6c890815e1dac9536dca00828b445d5d0df4d6a8c6adb4a7ec3166812fa8", size = 317141, upload-time = "2024-08-27T20:55:11.047Z" }, - { url = "https://files.pythonhosted.org/packages/8d/6a/b5242c8cb32d87f6abf4f5e3044ca397cb1a76712e3fa2424772e3ff495f/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:710a26b3dc80c0e4febf04555de66f5fd17e9cf7170a7b08000601a10570bda6", size = 323469, upload-time = "2024-08-27T20:55:15.914Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a6/73e929d43028a9079aca4bde107494864d54f0d72d9db508a51ff0878593/contourpy-1.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:75ee7cb1a14c617f34a51d11fa7524173e56551646828353c4af859c56b766e2", size = 1260894, upload-time = "2024-08-27T20:55:31.553Z" }, - { url = "https://files.pythonhosted.org/packages/2b/1e/1e726ba66eddf21c940821df8cf1a7d15cb165f0682d62161eaa5e93dae1/contourpy-1.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:33c92cdae89ec5135d036e7218e69b0bb2851206077251f04a6c4e0e21f03927", size = 1314829, upload-time = "2024-08-27T20:55:47.837Z" }, - { url = "https://files.pythonhosted.org/packages/b3/e3/b9f72758adb6ef7397327ceb8b9c39c75711affb220e4f53c745ea1d5a9a/contourpy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a11077e395f67ffc2c44ec2418cfebed032cd6da3022a94fc227b6faf8e2acb8", size = 265518, upload-time = "2024-08-27T20:56:01.333Z" }, - { url = "https://files.pythonhosted.org/packages/ec/22/19f5b948367ab5260fb41d842c7a78dae645603881ea6bc39738bcfcabf6/contourpy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8134301d7e204c88ed7ab50028ba06c683000040ede1d617298611f9dc6240c", size = 249350, upload-time = "2024-08-27T20:56:05.432Z" }, - { url = "https://files.pythonhosted.org/packages/26/76/0c7d43263dd00ae21a91a24381b7e813d286a3294d95d179ef3a7b9fb1d7/contourpy-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12968fdfd5bb45ffdf6192a590bd8ddd3ba9e58360b29683c6bb71a7b41edca", size = 309167, upload-time = "2024-08-27T20:56:10.034Z" }, - { url = "https://files.pythonhosted.org/packages/96/3b/cadff6773e89f2a5a492c1a8068e21d3fccaf1a1c1df7d65e7c8e3ef60ba/contourpy-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd2a0fc506eccaaa7595b7e1418951f213cf8255be2600f1ea1b61e46a60c55f", size = 348279, upload-time = "2024-08-27T20:56:15.41Z" }, - { url = "https://files.pythonhosted.org/packages/e1/86/158cc43aa549d2081a955ab11c6bdccc7a22caacc2af93186d26f5f48746/contourpy-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cfb5c62ce023dfc410d6059c936dcf96442ba40814aefbfa575425a3a7f19dc", size = 318519, upload-time = "2024-08-27T20:56:21.813Z" }, - { url = "https://files.pythonhosted.org/packages/05/11/57335544a3027e9b96a05948c32e566328e3a2f84b7b99a325b7a06d2b06/contourpy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68a32389b06b82c2fdd68276148d7b9275b5f5cf13e5417e4252f6d1a34f72a2", size = 321922, upload-time = "2024-08-27T20:56:26.983Z" }, - { url = "https://files.pythonhosted.org/packages/0b/e3/02114f96543f4a1b694333b92a6dcd4f8eebbefcc3a5f3bbb1316634178f/contourpy-1.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94e848a6b83da10898cbf1311a815f770acc9b6a3f2d646f330d57eb4e87592e", size = 1258017, upload-time = "2024-08-27T20:56:42.246Z" }, - { url = "https://files.pythonhosted.org/packages/f3/3b/bfe4c81c6d5881c1c643dde6620be0b42bf8aab155976dd644595cfab95c/contourpy-1.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d78ab28a03c854a873787a0a42254a0ccb3cb133c672f645c9f9c8f3ae9d0800", size = 1316773, upload-time = "2024-08-27T20:56:58.58Z" }, - { url = "https://files.pythonhosted.org/packages/f1/17/c52d2970784383cafb0bd918b6fb036d98d96bbf0bc1befb5d1e31a07a70/contourpy-1.3.0-cp39-cp39-win32.whl", hash = "sha256:81cb5ed4952aae6014bc9d0421dec7c5835c9c8c31cdf51910b708f548cf58e5", size = 171353, upload-time = "2024-08-27T20:57:02.718Z" }, - { url = "https://files.pythonhosted.org/packages/53/23/db9f69676308e094d3c45f20cc52e12d10d64f027541c995d89c11ad5c75/contourpy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:14e262f67bd7e6eb6880bc564dcda30b15e351a594657e55b7eec94b6ef72843", size = 211817, upload-time = "2024-08-27T20:57:06.328Z" }, - { url = "https://files.pythonhosted.org/packages/d1/09/60e486dc2b64c94ed33e58dcfb6f808192c03dfc5574c016218b9b7680dc/contourpy-1.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe41b41505a5a33aeaed2a613dccaeaa74e0e3ead6dd6fd3a118fb471644fd6c", size = 261886, upload-time = "2024-08-27T20:57:10.863Z" }, - { url = "https://files.pythonhosted.org/packages/19/20/b57f9f7174fcd439a7789fb47d764974ab646fa34d1790551de386457a8e/contourpy-1.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca7e17a65f72a5133bdbec9ecf22401c62bcf4821361ef7811faee695799779", size = 311008, upload-time = "2024-08-27T20:57:15.588Z" }, - { url = "https://files.pythonhosted.org/packages/74/fc/5040d42623a1845d4f17a418e590fd7a79ae8cb2bad2b2f83de63c3bdca4/contourpy-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1ec4dc6bf570f5b22ed0d7efba0dfa9c5b9e0431aeea7581aa217542d9e809a4", size = 215690, upload-time = "2024-08-27T20:57:19.321Z" }, - { url = "https://files.pythonhosted.org/packages/2b/24/dc3dcd77ac7460ab7e9d2b01a618cb31406902e50e605a8d6091f0a8f7cc/contourpy-1.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:00ccd0dbaad6d804ab259820fa7cb0b8036bda0686ef844d24125d8287178ce0", size = 261894, upload-time = "2024-08-27T20:57:23.873Z" }, - { url = "https://files.pythonhosted.org/packages/b1/db/531642a01cfec39d1682e46b5457b07cf805e3c3c584ec27e2a6223f8f6c/contourpy-1.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca947601224119117f7c19c9cdf6b3ab54c5726ef1d906aa4a69dfb6dd58102", size = 311099, upload-time = "2024-08-27T20:57:28.58Z" }, - { url = "https://files.pythonhosted.org/packages/38/1e/94bda024d629f254143a134eead69e21c836429a2a6ce82209a00ddcb79a/contourpy-1.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6ec93afeb848a0845a18989da3beca3eec2c0f852322efe21af1931147d12cb", size = 215838, upload-time = "2024-08-27T20:57:32.913Z" }, -] - [[package]] name = "contourpy" version = "1.3.2" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } @@ -710,142 +521,10 @@ wheels = [ { 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 = "coverage" -version = "7.10.7" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, - { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, - { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, - { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, - { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, - { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, - { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, - { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, - { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, - { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, - { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, - { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, - { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, - { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, - { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, - { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, - { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, - { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, - { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, - { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, - { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, - { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, - { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, - { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, - { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, - { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, - { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, - { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, - { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, - { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, - { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, - { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, - { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, - { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, - { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, - { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, - { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, - { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, - { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, - { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, - { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, - { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, - { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, - { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, - { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, - { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, - { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, - { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, - { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, - { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, - { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, - { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, - { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, - { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, - { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, - { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, - { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, - { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, - { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, - { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, - { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, - { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, - { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, - { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, - { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, - { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, - { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, - { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, - { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, - { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, - { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, - { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, - { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, - { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, - { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, - { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, - { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, - { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, - { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, - { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, - { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ad/d1c25053764b4c42eb294aae92ab617d2e4f803397f9c7c8295caa77a260/coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3", size = 217978, upload-time = "2025-09-21T20:03:30.362Z" }, - { url = "https://files.pythonhosted.org/packages/52/2f/b9f9daa39b80ece0b9548bbb723381e29bc664822d9a12c2135f8922c22b/coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c", size = 218370, upload-time = "2025-09-21T20:03:32.147Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6e/30d006c3b469e58449650642383dddf1c8fb63d44fdf92994bfd46570695/coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396", size = 244802, upload-time = "2025-09-21T20:03:33.919Z" }, - { url = "https://files.pythonhosted.org/packages/b0/49/8a070782ce7e6b94ff6a0b6d7c65ba6bc3091d92a92cef4cd4eb0767965c/coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40", size = 246625, upload-time = "2025-09-21T20:03:36.09Z" }, - { url = "https://files.pythonhosted.org/packages/6a/92/1c1c5a9e8677ce56d42b97bdaca337b2d4d9ebe703d8c174ede52dbabd5f/coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594", size = 248399, upload-time = "2025-09-21T20:03:38.342Z" }, - { url = "https://files.pythonhosted.org/packages/c0/54/b140edee7257e815de7426d5d9846b58505dffc29795fff2dfb7f8a1c5a0/coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a", size = 245142, upload-time = "2025-09-21T20:03:40.591Z" }, - { url = "https://files.pythonhosted.org/packages/e4/9e/6d6b8295940b118e8b7083b29226c71f6154f7ff41e9ca431f03de2eac0d/coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b", size = 246284, upload-time = "2025-09-21T20:03:42.355Z" }, - { url = "https://files.pythonhosted.org/packages/db/e5/5e957ca747d43dbe4d9714358375c7546cb3cb533007b6813fc20fce37ad/coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3", size = 244353, upload-time = "2025-09-21T20:03:44.218Z" }, - { url = "https://files.pythonhosted.org/packages/9a/45/540fc5cc92536a1b783b7ef99450bd55a4b3af234aae35a18a339973ce30/coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0", size = 244430, upload-time = "2025-09-21T20:03:46.065Z" }, - { url = "https://files.pythonhosted.org/packages/75/0b/8287b2e5b38c8fe15d7e3398849bb58d382aedc0864ea0fa1820e8630491/coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f", size = 245311, upload-time = "2025-09-21T20:03:48.19Z" }, - { url = "https://files.pythonhosted.org/packages/0c/1d/29724999984740f0c86d03e6420b942439bf5bd7f54d4382cae386a9d1e9/coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431", size = 220500, upload-time = "2025-09-21T20:03:50.024Z" }, - { url = "https://files.pythonhosted.org/packages/43/11/4b1e6b129943f905ca54c339f343877b55b365ae2558806c1be4f7476ed5/coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07", size = 221408, upload-time = "2025-09-21T20:03:51.803Z" }, - { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, -] - -[package.optional-dependencies] -toml = [ - { name = "tomli", marker = "python_full_version < '3.10'" }, -] - [[package]] name = "coverage" version = "7.13.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/2d/9a/3742e58fd04b233df95c012ee9f3dfe04708a5e1d32613bd2d47d4e1be0d/coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147", size = 218633, upload-time = "2025-12-28T15:40:10.165Z" }, @@ -943,7 +622,7 @@ wheels = [ [package.optional-dependencies] toml = [ - { name = "tomli", marker = "python_full_version >= '3.10' and python_full_version <= '3.11'" }, + { name = "tomli", marker = "python_full_version <= '3.11'" }, ] [[package]] @@ -951,8 +630,8 @@ name = "cryptography" version = "46.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and platform_python_implementation != 'PyPy') or (platform_python_implementation != 'PyPy' and sys_platform != 'darwin')" }, - { name = "typing-extensions", marker = "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, + { name = "cffi", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation != 'PyPy' and sys_platform != 'darwin'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' and sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } wheels = [ @@ -1033,10 +712,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ba/f1/6f2ee3f991327ad9e4c2f8b82611a467052a0fb0e247390192580e89f7ff/debugpy-1.8.14-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f920c7f9af409d90f5fd26e313e119d908b0dd2952c2393cd3247a462331f15", size = 4217514, upload-time = "2025-04-10T19:46:34.336Z" }, { url = "https://files.pythonhosted.org/packages/79/28/b9d146f8f2dc535c236ee09ad3e5ac899adb39d7a19b49f03ac95d216beb/debugpy-1.8.14-cp313-cp313-win32.whl", hash = "sha256:3784ec6e8600c66cbdd4ca2726c72d8ca781e94bce2f396cc606d458146f8f4e", size = 5254756, upload-time = "2025-04-10T19:46:36.199Z" }, { url = "https://files.pythonhosted.org/packages/e0/62/a7b4a57013eac4ccaef6977966e6bec5c63906dd25a86e35f155952e29a1/debugpy-1.8.14-cp313-cp313-win_amd64.whl", hash = "sha256:684eaf43c95a3ec39a96f1f5195a7ff3d4144e4a18d69bb66beeb1a6de605d6e", size = 5297119, upload-time = "2025-04-10T19:46:38.141Z" }, - { url = "https://files.pythonhosted.org/packages/85/6f/96ba96545f55b6a675afa08c96b42810de9b18c7ad17446bbec82762127a/debugpy-1.8.14-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:413512d35ff52c2fb0fd2d65e69f373ffd24f0ecb1fac514c04a668599c5ce7f", size = 2077696, upload-time = "2025-04-10T19:46:46.817Z" }, - { url = "https://files.pythonhosted.org/packages/fa/84/f378a2dd837d94de3c85bca14f1db79f8fcad7e20b108b40d59da56a6d22/debugpy-1.8.14-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c9156f7524a0d70b7a7e22b2e311d8ba76a15496fb00730e46dcdeedb9e1eea", size = 3554846, upload-time = "2025-04-10T19:46:48.72Z" }, - { url = "https://files.pythonhosted.org/packages/db/52/88824fe5d6893f59933f664c6e12783749ab537a2101baf5c713164d8aa2/debugpy-1.8.14-cp39-cp39-win32.whl", hash = "sha256:b44985f97cc3dd9d52c42eb59ee9d7ee0c4e7ecd62bca704891f997de4cef23d", size = 5209350, upload-time = "2025-04-10T19:46:50.284Z" }, - { url = "https://files.pythonhosted.org/packages/41/35/72e9399be24a04cb72cfe1284572c9fcd1d742c7fa23786925c18fa54ad8/debugpy-1.8.14-cp39-cp39-win_amd64.whl", hash = "sha256:b1528cfee6c1b1c698eb10b6b096c598738a8238822d218173d21c3086de8123", size = 5241852, upload-time = "2025-04-10T19:46:52.022Z" }, { url = "https://files.pythonhosted.org/packages/97/1a/481f33c37ee3ac8040d3d51fc4c4e4e7e61cb08b8bc8971d6032acc2279f/debugpy-1.8.14-py2.py3-none-any.whl", hash = "sha256:5cd9a579d553b6cb9759a7908a41988ee6280b961f24f63336835d9418216a20", size = 5256230, upload-time = "2025-04-10T19:46:54.077Z" }, ] @@ -1106,35 +781,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/90/2b/0817a2b257fe88725c25589d89aec060581aabf668707a8d03b2e9e0cb2a/fastjsonschema-2.21.1-py3-none-any.whl", hash = "sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667", size = 23924, upload-time = "2024-12-02T10:55:07.599Z" }, ] -[[package]] -name = "filelock" -version = "3.18.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, -] - [[package]] name = "filelock" version = "3.20.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, @@ -1194,14 +844,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/42/07/d6f775d950ee8a841012472c7303f8819423d8cc3b4530915de7265ebfa2/fonttools-4.60.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82cceceaf9c09a965a75b84a4b240dd3768e596ffb65ef53852681606fe7c9ba", size = 5002036, upload-time = "2025-12-09T13:37:42.639Z" }, { url = "https://files.pythonhosted.org/packages/73/f6/ba6458f83ce1a9f8c3b17bd8f7b8a2205a126aac1055796b7e7cfebbd38f/fonttools-4.60.2-cp314-cp314t-win32.whl", hash = "sha256:bbfbc918a75437fe7e6d64d1b1e1f713237df1cf00f3a36dedae910b2ba01cee", size = 2330985, upload-time = "2025-12-09T13:37:45.157Z" }, { url = "https://files.pythonhosted.org/packages/91/24/fea0ba4d3a32d4ed1103a1098bfd99dc78b5fe3bb97202920744a37b73dc/fonttools-4.60.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0e5cd9b0830f6550d58c84f3ab151a9892b50c4f9d538c5603c0ce6fff2eb3f1", size = 2396226, upload-time = "2025-12-09T13:37:47.355Z" }, - { url = "https://files.pythonhosted.org/packages/55/ae/a6d9446cb258d3fe87e311c2d7bacf8e8da3e5809fbdc3a8306db4f6b14e/fonttools-4.60.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a3c75b8b42f7f93906bdba9eb1197bb76aecbe9a0a7cf6feec75f7605b5e8008", size = 2857184, upload-time = "2025-12-09T13:37:49.96Z" }, - { url = "https://files.pythonhosted.org/packages/3a/f3/1b41d0b6a8b908aa07f652111155dd653ebbf0b3385e66562556c5206685/fonttools-4.60.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0f86c8c37bc0ec0b9c141d5e90c717ff614e93c187f06d80f18c7057097f71bc", size = 2401877, upload-time = "2025-12-09T13:37:52.307Z" }, - { url = "https://files.pythonhosted.org/packages/71/57/048fd781680c38b05c5463657d0d95d5f2391a51972176e175c01de29d42/fonttools-4.60.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe905403fe59683b0e9a45f234af2866834376b8821f34633b1c76fb731b6311", size = 4878073, upload-time = "2025-12-09T13:37:56.477Z" }, - { url = "https://files.pythonhosted.org/packages/45/bb/363364f052a893cebd3d449588b21244a9d873620fda03ad92702d2e1bc7/fonttools-4.60.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38ce703b60a906e421e12d9e3a7f064883f5e61bb23e8961f4be33cfe578500b", size = 4835385, upload-time = "2025-12-09T13:37:58.882Z" }, - { url = "https://files.pythonhosted.org/packages/1c/38/e392bb930b2436287e6021672345db26441bf1f85f1e98f8b9784334e41d/fonttools-4.60.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9e810c06f3e79185cecf120e58b343ea5a89b54dd695fd644446bcf8c026da5e", size = 4853084, upload-time = "2025-12-09T13:38:01.578Z" }, - { url = "https://files.pythonhosted.org/packages/65/60/0d77faeaecf7a3276a8a6dc49e2274357e6b3ed6a1774e2fdb2a7f142db0/fonttools-4.60.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:38faec8cc1d12122599814d15a402183f5123fb7608dac956121e7c6742aebc5", size = 4971144, upload-time = "2025-12-09T13:38:03.748Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c7/6d3ac3afbcd598631bce24c3ecb919e7d0644a82fea8ddc4454312fc0be6/fonttools-4.60.2-cp39-cp39-win32.whl", hash = "sha256:80a45cf7bf659acb7b36578f300231873daba67bd3ca8cce181c73f861f14a37", size = 1499411, upload-time = "2025-12-09T13:38:05.586Z" }, - { url = "https://files.pythonhosted.org/packages/5a/1c/9dedf6420e23f9fa630bb97941839dddd2e1e57d1b2b85a902378dbe0bd2/fonttools-4.60.2-cp39-cp39-win_amd64.whl", hash = "sha256:c355d5972071938e1b1e0f5a1df001f68ecf1a62f34a3407dc8e0beccf052501", size = 1547943, upload-time = "2025-12-09T13:38:07.604Z" }, { url = "https://files.pythonhosted.org/packages/79/6c/10280af05b44fafd1dff69422805061fa1af29270bc52dce031ac69540bf/fonttools-4.60.2-py3-none-any.whl", hash = "sha256:73cf92eeda67cf6ff10c8af56fc8f4f07c1647d989a979be9e388a49be26552a", size = 1144610, upload-time = "2025-12-09T13:38:09.5Z" }, ] @@ -1311,35 +953,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/cb/18326d2d89ad3b0dd143da971e77afd1e6ca6674f1b1c3df4b6bec6279fc/id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658", size = 13611, upload-time = "2024-12-04T19:53:03.02Z" }, ] -[[package]] -name = "identify" -version = "2.6.15" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, -] - [[package]] name = "identify" version = "2.6.16" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" }, @@ -1370,9 +987,6 @@ wheels = [ name = "importlib-resources" version = "6.5.2" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp", marker = "python_full_version < '3.10'" }, -] sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, @@ -1395,8 +1009,7 @@ dependencies = [ { name = "appnope", marker = "sys_platform == 'darwin'" }, { name = "comm" }, { name = "debugpy" }, - { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "ipython", version = "9.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "jupyter-client" }, { name = "jupyter-core" }, @@ -1413,54 +1026,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/5c/368ae6c01c7628438358e6d337c19b05425727fbb221d2a3c4303c372f42/ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5", size = 117173, upload-time = "2024-07-01T14:07:19.603Z" }, ] -[[package]] -name = "ipython" -version = "8.18.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version < '3.10'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, - { name = "jedi", marker = "python_full_version < '3.10'" }, - { name = "matplotlib-inline", marker = "python_full_version < '3.10'" }, - { name = "pexpect", marker = "python_full_version < '3.10' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version < '3.10'" }, - { name = "pygments", marker = "python_full_version < '3.10'" }, - { name = "stack-data", marker = "python_full_version < '3.10'" }, - { name = "traitlets", marker = "python_full_version < '3.10'" }, - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/b9/3ba6c45a6df813c09a48bac313c22ff83efa26cbb55011218d925a46e2ad/ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27", size = 5486330, upload-time = "2023-11-27T09:58:34.596Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/6b/d9fdcdef2eb6a23f391251fde8781c38d42acd82abe84d054cb74f7863b0/ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397", size = 808161, upload-time = "2023-11-27T09:58:30.538Z" }, -] - [[package]] name = "ipython" version = "8.37.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", ] dependencies = [ - { name = "colorama", marker = "python_full_version == '3.10.*' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version == '3.10.*'" }, - { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, - { name = "jedi", marker = "python_full_version == '3.10.*'" }, - { name = "matplotlib-inline", marker = "python_full_version == '3.10.*'" }, - { name = "pexpect", marker = "python_full_version == '3.10.*' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version == '3.10.*'" }, - { name = "pygments", marker = "python_full_version == '3.10.*'" }, - { name = "stack-data", marker = "python_full_version == '3.10.*'" }, - { name = "traitlets", marker = "python_full_version == '3.10.*'" }, - { name = "typing-extensions", marker = "python_full_version == '3.10.*'" }, + { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version < '3.11'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "jedi", marker = "python_full_version < '3.11'" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.11'" }, + { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.11'" }, + { name = "pygments", marker = "python_full_version < '3.11'" }, + { name = "stack-data", marker = "python_full_version < '3.11'" }, + { name = "traitlets", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/85/31/10ac88f3357fc276dc8a64e8880c82e80e7459326ae1d0a211b40abf6665/ipython-8.37.0.tar.gz", hash = "sha256:ca815841e1a41a1e6b73a0b08f3038af9b2252564d01fc405356d34033012216", size = 5606088, upload-time = "2025-05-31T16:39:09.613Z" } wheels = [ @@ -1515,8 +1101,7 @@ version = "8.1.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "comm" }, - { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "ipython", version = "9.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "jupyterlab-widgets" }, { name = "traitlets" }, @@ -1670,7 +1255,6 @@ name = "jupyter-client" version = "8.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jupyter-core" }, { name = "python-dateutil" }, { name = "pyzmq" }, @@ -1720,7 +1304,6 @@ name = "jupyter-lsp" version = "2.2.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jupyter-server" }, ] sdist = { url = "https://files.pythonhosted.org/packages/85/b4/3200b0b09c12bc3b72d943d923323c398eff382d1dcc7c0dbc8b74630e40/jupyter-lsp-2.2.5.tar.gz", hash = "sha256:793147a05ad446f809fd53ef1cd19a9f5256fd0a2d6b7ce943a982cb4f545001", size = 48741, upload-time = "2024-04-09T17:59:44.918Z" } @@ -1745,7 +1328,7 @@ dependencies = [ { name = "overrides", marker = "python_full_version < '3.12'" }, { name = "packaging" }, { name = "prometheus-client" }, - { name = "pywinpty", marker = "(python_full_version < '3.10' and os_name == 'nt' and platform_machine != 'arm64' and sys_platform == 'darwin') or (os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "pywinpty", marker = "(os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, { name = "pyzmq" }, { name = "send2trash" }, { name = "terminado" }, @@ -1763,7 +1346,7 @@ name = "jupyter-server-terminals" version = "0.5.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pywinpty", marker = "(python_full_version < '3.10' and os_name == 'nt' and platform_machine != 'arm64' and sys_platform == 'darwin') or (os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "pywinpty", marker = "(os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, { name = "terminado" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fc/d5/562469734f476159e99a55426d697cbf8e7eb5efe89fb0e0b4f83a3d3459/jupyter_server_terminals-0.5.3.tar.gz", hash = "sha256:5ae0295167220e9ace0edcfdb212afd2b01ee8d179fe6f23c899590e9b8a5269", size = 31430, upload-time = "2024-03-12T14:37:03.049Z" } @@ -1778,7 +1361,6 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-lru" }, { name = "httpx" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "ipykernel" }, { name = "jinja2" }, { name = "jupyter-core" }, @@ -1812,7 +1394,6 @@ version = "2.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jinja2" }, { name = "json5" }, { name = "jsonschema" }, @@ -1869,126 +1450,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085, upload-time = "2024-12-25T15:26:44.377Z" }, ] -[[package]] -name = "kiwisolver" -version = "1.4.7" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/85/4d/2255e1c76304cbd60b48cee302b66d1dde4468dc5b1160e4b7cb43778f2a/kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60", size = 97286, upload-time = "2024-09-04T09:39:44.302Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/14/fc943dd65268a96347472b4fbe5dcc2f6f55034516f80576cd0dd3a8930f/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6", size = 122440, upload-time = "2024-09-04T09:03:44.9Z" }, - { url = "https://files.pythonhosted.org/packages/1e/46/e68fed66236b69dd02fcdb506218c05ac0e39745d696d22709498896875d/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17", size = 65758, upload-time = "2024-09-04T09:03:46.582Z" }, - { url = "https://files.pythonhosted.org/packages/ef/fa/65de49c85838681fc9cb05de2a68067a683717321e01ddafb5b8024286f0/kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9", size = 64311, upload-time = "2024-09-04T09:03:47.973Z" }, - { url = "https://files.pythonhosted.org/packages/42/9c/cc8d90f6ef550f65443bad5872ffa68f3dee36de4974768628bea7c14979/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9", size = 1637109, upload-time = "2024-09-04T09:03:49.281Z" }, - { url = "https://files.pythonhosted.org/packages/55/91/0a57ce324caf2ff5403edab71c508dd8f648094b18cfbb4c8cc0fde4a6ac/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c", size = 1617814, upload-time = "2024-09-04T09:03:51.444Z" }, - { url = "https://files.pythonhosted.org/packages/12/5d/c36140313f2510e20207708adf36ae4919416d697ee0236b0ddfb6fd1050/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599", size = 1400881, upload-time = "2024-09-04T09:03:53.357Z" }, - { url = "https://files.pythonhosted.org/packages/56/d0/786e524f9ed648324a466ca8df86298780ef2b29c25313d9a4f16992d3cf/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05", size = 1512972, upload-time = "2024-09-04T09:03:55.082Z" }, - { url = "https://files.pythonhosted.org/packages/67/5a/77851f2f201e6141d63c10a0708e996a1363efaf9e1609ad0441b343763b/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407", size = 1444787, upload-time = "2024-09-04T09:03:56.588Z" }, - { url = "https://files.pythonhosted.org/packages/06/5f/1f5eaab84355885e224a6fc8d73089e8713dc7e91c121f00b9a1c58a2195/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278", size = 2199212, upload-time = "2024-09-04T09:03:58.557Z" }, - { url = "https://files.pythonhosted.org/packages/b5/28/9152a3bfe976a0ae21d445415defc9d1cd8614b2910b7614b30b27a47270/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5", size = 2346399, upload-time = "2024-09-04T09:04:00.178Z" }, - { url = "https://files.pythonhosted.org/packages/26/f6/453d1904c52ac3b400f4d5e240ac5fec25263716723e44be65f4d7149d13/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad", size = 2308688, upload-time = "2024-09-04T09:04:02.216Z" }, - { url = "https://files.pythonhosted.org/packages/5a/9a/d4968499441b9ae187e81745e3277a8b4d7c60840a52dc9d535a7909fac3/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895", size = 2445493, upload-time = "2024-09-04T09:04:04.571Z" }, - { url = "https://files.pythonhosted.org/packages/07/c9/032267192e7828520dacb64dfdb1d74f292765f179e467c1cba97687f17d/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3", size = 2262191, upload-time = "2024-09-04T09:04:05.969Z" }, - { url = "https://files.pythonhosted.org/packages/6c/ad/db0aedb638a58b2951da46ddaeecf204be8b4f5454df020d850c7fa8dca8/kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc", size = 46644, upload-time = "2024-09-04T09:04:07.408Z" }, - { url = "https://files.pythonhosted.org/packages/12/ca/d0f7b7ffbb0be1e7c2258b53554efec1fd652921f10d7d85045aff93ab61/kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c", size = 55877, upload-time = "2024-09-04T09:04:08.869Z" }, - { url = "https://files.pythonhosted.org/packages/97/6c/cfcc128672f47a3e3c0d918ecb67830600078b025bfc32d858f2e2d5c6a4/kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a", size = 48347, upload-time = "2024-09-04T09:04:10.106Z" }, - { url = "https://files.pythonhosted.org/packages/e9/44/77429fa0a58f941d6e1c58da9efe08597d2e86bf2b2cce6626834f49d07b/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54", size = 122442, upload-time = "2024-09-04T09:04:11.432Z" }, - { url = "https://files.pythonhosted.org/packages/e5/20/8c75caed8f2462d63c7fd65e16c832b8f76cda331ac9e615e914ee80bac9/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95", size = 65762, upload-time = "2024-09-04T09:04:12.468Z" }, - { url = "https://files.pythonhosted.org/packages/f4/98/fe010f15dc7230f45bc4cf367b012d651367fd203caaa992fd1f5963560e/kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935", size = 64319, upload-time = "2024-09-04T09:04:13.635Z" }, - { url = "https://files.pythonhosted.org/packages/8b/1b/b5d618f4e58c0675654c1e5051bcf42c776703edb21c02b8c74135541f60/kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb", size = 1334260, upload-time = "2024-09-04T09:04:14.878Z" }, - { url = "https://files.pythonhosted.org/packages/b8/01/946852b13057a162a8c32c4c8d2e9ed79f0bb5d86569a40c0b5fb103e373/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02", size = 1426589, upload-time = "2024-09-04T09:04:16.514Z" }, - { url = "https://files.pythonhosted.org/packages/70/d1/c9f96df26b459e15cf8a965304e6e6f4eb291e0f7a9460b4ad97b047561e/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51", size = 1541080, upload-time = "2024-09-04T09:04:18.322Z" }, - { url = "https://files.pythonhosted.org/packages/d3/73/2686990eb8b02d05f3de759d6a23a4ee7d491e659007dd4c075fede4b5d0/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052", size = 1470049, upload-time = "2024-09-04T09:04:20.266Z" }, - { url = "https://files.pythonhosted.org/packages/a7/4b/2db7af3ed3af7c35f388d5f53c28e155cd402a55432d800c543dc6deb731/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18", size = 1426376, upload-time = "2024-09-04T09:04:22.419Z" }, - { url = "https://files.pythonhosted.org/packages/05/83/2857317d04ea46dc5d115f0df7e676997bbd968ced8e2bd6f7f19cfc8d7f/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545", size = 2222231, upload-time = "2024-09-04T09:04:24.526Z" }, - { url = "https://files.pythonhosted.org/packages/0d/b5/866f86f5897cd4ab6d25d22e403404766a123f138bd6a02ecb2cdde52c18/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b", size = 2368634, upload-time = "2024-09-04T09:04:25.899Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ee/73de8385403faba55f782a41260210528fe3273d0cddcf6d51648202d6d0/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36", size = 2329024, upload-time = "2024-09-04T09:04:28.523Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e7/cd101d8cd2cdfaa42dc06c433df17c8303d31129c9fdd16c0ea37672af91/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3", size = 2468484, upload-time = "2024-09-04T09:04:30.547Z" }, - { url = "https://files.pythonhosted.org/packages/e1/72/84f09d45a10bc57a40bb58b81b99d8f22b58b2040c912b7eb97ebf625bf2/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523", size = 2284078, upload-time = "2024-09-04T09:04:33.218Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d4/71828f32b956612dc36efd7be1788980cb1e66bfb3706e6dec9acad9b4f9/kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d", size = 46645, upload-time = "2024-09-04T09:04:34.371Z" }, - { url = "https://files.pythonhosted.org/packages/a1/65/d43e9a20aabcf2e798ad1aff6c143ae3a42cf506754bcb6a7ed8259c8425/kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b", size = 56022, upload-time = "2024-09-04T09:04:35.786Z" }, - { url = "https://files.pythonhosted.org/packages/35/b3/9f75a2e06f1b4ca00b2b192bc2b739334127d27f1d0625627ff8479302ba/kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376", size = 48536, upload-time = "2024-09-04T09:04:37.525Z" }, - { url = "https://files.pythonhosted.org/packages/97/9c/0a11c714cf8b6ef91001c8212c4ef207f772dd84540104952c45c1f0a249/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2", size = 121808, upload-time = "2024-09-04T09:04:38.637Z" }, - { url = "https://files.pythonhosted.org/packages/f2/d8/0fe8c5f5d35878ddd135f44f2af0e4e1d379e1c7b0716f97cdcb88d4fd27/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a", size = 65531, upload-time = "2024-09-04T09:04:39.694Z" }, - { url = "https://files.pythonhosted.org/packages/80/c5/57fa58276dfdfa612241d640a64ca2f76adc6ffcebdbd135b4ef60095098/kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee", size = 63894, upload-time = "2024-09-04T09:04:41.6Z" }, - { url = "https://files.pythonhosted.org/packages/8b/e9/26d3edd4c4ad1c5b891d8747a4f81b1b0aba9fb9721de6600a4adc09773b/kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640", size = 1369296, upload-time = "2024-09-04T09:04:42.886Z" }, - { url = "https://files.pythonhosted.org/packages/b6/67/3f4850b5e6cffb75ec40577ddf54f7b82b15269cc5097ff2e968ee32ea7d/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f", size = 1461450, upload-time = "2024-09-04T09:04:46.284Z" }, - { url = "https://files.pythonhosted.org/packages/52/be/86cbb9c9a315e98a8dc6b1d23c43cffd91d97d49318854f9c37b0e41cd68/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483", size = 1579168, upload-time = "2024-09-04T09:04:47.91Z" }, - { url = "https://files.pythonhosted.org/packages/0f/00/65061acf64bd5fd34c1f4ae53f20b43b0a017a541f242a60b135b9d1e301/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258", size = 1507308, upload-time = "2024-09-04T09:04:49.465Z" }, - { url = "https://files.pythonhosted.org/packages/21/e4/c0b6746fd2eb62fe702118b3ca0cb384ce95e1261cfada58ff693aeec08a/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e", size = 1464186, upload-time = "2024-09-04T09:04:50.949Z" }, - { url = "https://files.pythonhosted.org/packages/0a/0f/529d0a9fffb4d514f2782c829b0b4b371f7f441d61aa55f1de1c614c4ef3/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107", size = 2247877, upload-time = "2024-09-04T09:04:52.388Z" }, - { url = "https://files.pythonhosted.org/packages/d1/e1/66603ad779258843036d45adcbe1af0d1a889a07af4635f8b4ec7dccda35/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948", size = 2404204, upload-time = "2024-09-04T09:04:54.385Z" }, - { url = "https://files.pythonhosted.org/packages/8d/61/de5fb1ca7ad1f9ab7970e340a5b833d735df24689047de6ae71ab9d8d0e7/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038", size = 2352461, upload-time = "2024-09-04T09:04:56.307Z" }, - { url = "https://files.pythonhosted.org/packages/ba/d2/0edc00a852e369827f7e05fd008275f550353f1f9bcd55db9363d779fc63/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383", size = 2501358, upload-time = "2024-09-04T09:04:57.922Z" }, - { url = "https://files.pythonhosted.org/packages/84/15/adc15a483506aec6986c01fb7f237c3aec4d9ed4ac10b756e98a76835933/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520", size = 2314119, upload-time = "2024-09-04T09:04:59.332Z" }, - { url = "https://files.pythonhosted.org/packages/36/08/3a5bb2c53c89660863a5aa1ee236912269f2af8762af04a2e11df851d7b2/kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b", size = 46367, upload-time = "2024-09-04T09:05:00.804Z" }, - { url = "https://files.pythonhosted.org/packages/19/93/c05f0a6d825c643779fc3c70876bff1ac221f0e31e6f701f0e9578690d70/kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb", size = 55884, upload-time = "2024-09-04T09:05:01.924Z" }, - { url = "https://files.pythonhosted.org/packages/d2/f9/3828d8f21b6de4279f0667fb50a9f5215e6fe57d5ec0d61905914f5b6099/kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a", size = 48528, upload-time = "2024-09-04T09:05:02.983Z" }, - { url = "https://files.pythonhosted.org/packages/c4/06/7da99b04259b0f18b557a4effd1b9c901a747f7fdd84cf834ccf520cb0b2/kiwisolver-1.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e", size = 121913, upload-time = "2024-09-04T09:05:04.072Z" }, - { url = "https://files.pythonhosted.org/packages/97/f5/b8a370d1aa593c17882af0a6f6755aaecd643640c0ed72dcfd2eafc388b9/kiwisolver-1.4.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6", size = 65627, upload-time = "2024-09-04T09:05:05.119Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fc/6c0374f7503522539e2d4d1b497f5ebad3f8ed07ab51aed2af988dd0fb65/kiwisolver-1.4.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750", size = 63888, upload-time = "2024-09-04T09:05:06.191Z" }, - { url = "https://files.pythonhosted.org/packages/bf/3e/0b7172793d0f41cae5c923492da89a2ffcd1adf764c16159ca047463ebd3/kiwisolver-1.4.7-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d", size = 1369145, upload-time = "2024-09-04T09:05:07.919Z" }, - { url = "https://files.pythonhosted.org/packages/77/92/47d050d6f6aced2d634258123f2688fbfef8ded3c5baf2c79d94d91f1f58/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379", size = 1461448, upload-time = "2024-09-04T09:05:10.01Z" }, - { url = "https://files.pythonhosted.org/packages/9c/1b/8f80b18e20b3b294546a1adb41701e79ae21915f4175f311a90d042301cf/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c", size = 1578750, upload-time = "2024-09-04T09:05:11.598Z" }, - { url = "https://files.pythonhosted.org/packages/a4/fe/fe8e72f3be0a844f257cadd72689c0848c6d5c51bc1d60429e2d14ad776e/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34", size = 1507175, upload-time = "2024-09-04T09:05:13.22Z" }, - { url = "https://files.pythonhosted.org/packages/39/fa/cdc0b6105d90eadc3bee525fecc9179e2b41e1ce0293caaf49cb631a6aaf/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1", size = 1463963, upload-time = "2024-09-04T09:05:15.925Z" }, - { url = "https://files.pythonhosted.org/packages/6e/5c/0c03c4e542720c6177d4f408e56d1c8315899db72d46261a4e15b8b33a41/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f", size = 2248220, upload-time = "2024-09-04T09:05:17.434Z" }, - { url = "https://files.pythonhosted.org/packages/3d/ee/55ef86d5a574f4e767df7da3a3a7ff4954c996e12d4fbe9c408170cd7dcc/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b", size = 2404463, upload-time = "2024-09-04T09:05:18.997Z" }, - { url = "https://files.pythonhosted.org/packages/0f/6d/73ad36170b4bff4825dc588acf4f3e6319cb97cd1fb3eb04d9faa6b6f212/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27", size = 2352842, upload-time = "2024-09-04T09:05:21.299Z" }, - { url = "https://files.pythonhosted.org/packages/0b/16/fa531ff9199d3b6473bb4d0f47416cdb08d556c03b8bc1cccf04e756b56d/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a", size = 2501635, upload-time = "2024-09-04T09:05:23.588Z" }, - { url = "https://files.pythonhosted.org/packages/78/7e/aa9422e78419db0cbe75fb86d8e72b433818f2e62e2e394992d23d23a583/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee", size = 2314556, upload-time = "2024-09-04T09:05:25.907Z" }, - { url = "https://files.pythonhosted.org/packages/a8/b2/15f7f556df0a6e5b3772a1e076a9d9f6c538ce5f05bd590eca8106508e06/kiwisolver-1.4.7-cp313-cp313-win32.whl", hash = "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07", size = 46364, upload-time = "2024-09-04T09:05:27.184Z" }, - { url = "https://files.pythonhosted.org/packages/0b/db/32e897e43a330eee8e4770bfd2737a9584b23e33587a0812b8e20aac38f7/kiwisolver-1.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76", size = 55887, upload-time = "2024-09-04T09:05:28.372Z" }, - { url = "https://files.pythonhosted.org/packages/c8/a4/df2bdca5270ca85fd25253049eb6708d4127be2ed0e5c2650217450b59e9/kiwisolver-1.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650", size = 48530, upload-time = "2024-09-04T09:05:30.225Z" }, - { url = "https://files.pythonhosted.org/packages/11/88/37ea0ea64512997b13d69772db8dcdc3bfca5442cda3a5e4bb943652ee3e/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd", size = 122449, upload-time = "2024-09-04T09:05:55.311Z" }, - { url = "https://files.pythonhosted.org/packages/4e/45/5a5c46078362cb3882dcacad687c503089263c017ca1241e0483857791eb/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583", size = 65757, upload-time = "2024-09-04T09:05:56.906Z" }, - { url = "https://files.pythonhosted.org/packages/8a/be/a6ae58978772f685d48dd2e84460937761c53c4bbd84e42b0336473d9775/kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417", size = 64312, upload-time = "2024-09-04T09:05:58.384Z" }, - { url = "https://files.pythonhosted.org/packages/f4/04/18ef6f452d311e1e1eb180c9bf5589187fa1f042db877e6fe443ef10099c/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904", size = 1626966, upload-time = "2024-09-04T09:05:59.855Z" }, - { url = "https://files.pythonhosted.org/packages/21/b1/40655f6c3fa11ce740e8a964fa8e4c0479c87d6a7944b95af799c7a55dfe/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a", size = 1607044, upload-time = "2024-09-04T09:06:02.16Z" }, - { url = "https://files.pythonhosted.org/packages/fd/93/af67dbcfb9b3323bbd2c2db1385a7139d8f77630e4a37bb945b57188eb2d/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8", size = 1391879, upload-time = "2024-09-04T09:06:03.908Z" }, - { url = "https://files.pythonhosted.org/packages/40/6f/d60770ef98e77b365d96061d090c0cd9e23418121c55fff188fa4bdf0b54/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2", size = 1504751, upload-time = "2024-09-04T09:06:05.58Z" }, - { url = "https://files.pythonhosted.org/packages/fa/3a/5f38667d313e983c432f3fcd86932177519ed8790c724e07d77d1de0188a/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88", size = 1436990, upload-time = "2024-09-04T09:06:08.126Z" }, - { url = "https://files.pythonhosted.org/packages/cb/3b/1520301a47326e6a6043b502647e42892be33b3f051e9791cc8bb43f1a32/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde", size = 2191122, upload-time = "2024-09-04T09:06:10.345Z" }, - { url = "https://files.pythonhosted.org/packages/cf/c4/eb52da300c166239a2233f1f9c4a1b767dfab98fae27681bfb7ea4873cb6/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c", size = 2338126, upload-time = "2024-09-04T09:06:12.321Z" }, - { url = "https://files.pythonhosted.org/packages/1a/cb/42b92fd5eadd708dd9107c089e817945500685f3437ce1fd387efebc6d6e/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2", size = 2298313, upload-time = "2024-09-04T09:06:14.562Z" }, - { url = "https://files.pythonhosted.org/packages/4f/eb/be25aa791fe5fc75a8b1e0c965e00f942496bc04635c9aae8035f6b76dcd/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb", size = 2437784, upload-time = "2024-09-04T09:06:16.767Z" }, - { url = "https://files.pythonhosted.org/packages/c5/22/30a66be7f3368d76ff95689e1c2e28d382383952964ab15330a15d8bfd03/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327", size = 2253988, upload-time = "2024-09-04T09:06:18.705Z" }, - { url = "https://files.pythonhosted.org/packages/35/d3/5f2ecb94b5211c8a04f218a76133cc8d6d153b0f9cd0b45fad79907f0689/kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644", size = 46980, upload-time = "2024-09-04T09:06:20.106Z" }, - { url = "https://files.pythonhosted.org/packages/ef/17/cd10d020578764ea91740204edc6b3236ed8106228a46f568d716b11feb2/kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4", size = 55847, upload-time = "2024-09-04T09:06:21.407Z" }, - { url = "https://files.pythonhosted.org/packages/91/84/32232502020bd78d1d12be7afde15811c64a95ed1f606c10456db4e4c3ac/kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f", size = 48494, upload-time = "2024-09-04T09:06:22.648Z" }, - { url = "https://files.pythonhosted.org/packages/ac/59/741b79775d67ab67ced9bb38552da688c0305c16e7ee24bba7a2be253fb7/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643", size = 59491, upload-time = "2024-09-04T09:06:24.188Z" }, - { url = "https://files.pythonhosted.org/packages/58/cc/fb239294c29a5656e99e3527f7369b174dd9cc7c3ef2dea7cb3c54a8737b/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706", size = 57648, upload-time = "2024-09-04T09:06:25.559Z" }, - { url = "https://files.pythonhosted.org/packages/3b/ef/2f009ac1f7aab9f81efb2d837301d255279d618d27b6015780115ac64bdd/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6", size = 84257, upload-time = "2024-09-04T09:06:27.038Z" }, - { url = "https://files.pythonhosted.org/packages/81/e1/c64f50987f85b68b1c52b464bb5bf73e71570c0f7782d626d1eb283ad620/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2", size = 80906, upload-time = "2024-09-04T09:06:28.48Z" }, - { url = "https://files.pythonhosted.org/packages/fd/71/1687c5c0a0be2cee39a5c9c389e546f9c6e215e46b691d00d9f646892083/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4", size = 79951, upload-time = "2024-09-04T09:06:29.966Z" }, - { url = "https://files.pythonhosted.org/packages/ea/8b/d7497df4a1cae9367adf21665dd1f896c2a7aeb8769ad77b662c5e2bcce7/kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a", size = 55715, upload-time = "2024-09-04T09:06:31.489Z" }, - { url = "https://files.pythonhosted.org/packages/d5/df/ce37d9b26f07ab90880923c94d12a6ff4d27447096b4c849bfc4339ccfdf/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39", size = 58666, upload-time = "2024-09-04T09:06:43.756Z" }, - { url = "https://files.pythonhosted.org/packages/b0/d3/e4b04f43bc629ac8e186b77b2b1a251cdfa5b7610fa189dc0db622672ce6/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e", size = 57088, upload-time = "2024-09-04T09:06:45.406Z" }, - { url = "https://files.pythonhosted.org/packages/30/1c/752df58e2d339e670a535514d2db4fe8c842ce459776b8080fbe08ebb98e/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608", size = 84321, upload-time = "2024-09-04T09:06:47.557Z" }, - { url = "https://files.pythonhosted.org/packages/f0/f8/fe6484e847bc6e238ec9f9828089fb2c0bb53f2f5f3a79351fde5b565e4f/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674", size = 80776, upload-time = "2024-09-04T09:06:49.235Z" }, - { url = "https://files.pythonhosted.org/packages/9b/57/d7163c0379f250ef763aba85330a19feefb5ce6cb541ade853aaba881524/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225", size = 79984, upload-time = "2024-09-04T09:06:51.336Z" }, - { url = "https://files.pythonhosted.org/packages/8c/95/4a103776c265d13b3d2cd24fb0494d4e04ea435a8ef97e1b2c026d43250b/kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0", size = 55811, upload-time = "2024-09-04T09:06:53.078Z" }, -] - [[package]] name = "kiwisolver" version = "1.4.8" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538, upload-time = "2024-12-24T18:30:51.519Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/47/5f/4d8e9e852d98ecd26cdf8eaf7ed8bc33174033bba5e07001b289f07308fd/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db", size = 124623, upload-time = "2024-12-24T18:28:17.687Z" }, @@ -2076,9 +1541,6 @@ wheels = [ name = "markdown" version = "3.8.2" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, -] sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" }, @@ -2152,109 +1614,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, - { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344, upload-time = "2024-10-18T15:21:43.721Z" }, - { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389, upload-time = "2024-10-18T15:21:44.666Z" }, - { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607, upload-time = "2024-10-18T15:21:45.452Z" }, - { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728, upload-time = "2024-10-18T15:21:46.295Z" }, - { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826, upload-time = "2024-10-18T15:21:47.134Z" }, - { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843, upload-time = "2024-10-18T15:21:48.334Z" }, - { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219, upload-time = "2024-10-18T15:21:49.587Z" }, - { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946, upload-time = "2024-10-18T15:21:50.441Z" }, - { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063, upload-time = "2024-10-18T15:21:51.385Z" }, - { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506, upload-time = "2024-10-18T15:21:52.974Z" }, -] - -[[package]] -name = "matplotlib" -version = "3.9.4" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "contourpy", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "cycler", marker = "python_full_version < '3.10'" }, - { name = "fonttools", marker = "python_full_version < '3.10'" }, - { name = "importlib-resources", marker = "python_full_version < '3.10'" }, - { name = "kiwisolver", version = "1.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "packaging", marker = "python_full_version < '3.10'" }, - { name = "pillow", marker = "python_full_version < '3.10'" }, - { name = "pyparsing", marker = "python_full_version < '3.10'" }, - { name = "python-dateutil", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/17/1747b4154034befd0ed33b52538f5eb7752d05bb51c5e2a31470c3bc7d52/matplotlib-3.9.4.tar.gz", hash = "sha256:1e00e8be7393cbdc6fedfa8a6fba02cf3e83814b285db1c60b906a023ba41bc3", size = 36106529, upload-time = "2024-12-13T05:56:34.184Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/94/27d2e2c30d54b56c7b764acc1874a909e34d1965a427fc7092bb6a588b63/matplotlib-3.9.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fdd7abfb706dfa8d307af64a87f1a862879ec3cd8d0ec8637458f0885b9c50", size = 7885089, upload-time = "2024-12-13T05:54:24.224Z" }, - { url = "https://files.pythonhosted.org/packages/c6/25/828273307e40a68eb8e9df832b6b2aaad075864fdc1de4b1b81e40b09e48/matplotlib-3.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d89bc4e85e40a71d1477780366c27fb7c6494d293e1617788986f74e2a03d7ff", size = 7770600, upload-time = "2024-12-13T05:54:27.214Z" }, - { url = "https://files.pythonhosted.org/packages/f2/65/f841a422ec994da5123368d76b126acf4fc02ea7459b6e37c4891b555b83/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddf9f3c26aae695c5daafbf6b94e4c1a30d6cd617ba594bbbded3b33a1fcfa26", size = 8200138, upload-time = "2024-12-13T05:54:29.497Z" }, - { url = "https://files.pythonhosted.org/packages/07/06/272aca07a38804d93b6050813de41ca7ab0e29ba7a9dd098e12037c919a9/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18ebcf248030173b59a868fda1fe42397253f6698995b55e81e1f57431d85e50", size = 8312711, upload-time = "2024-12-13T05:54:34.396Z" }, - { url = "https://files.pythonhosted.org/packages/98/37/f13e23b233c526b7e27ad61be0a771894a079e0f7494a10d8d81557e0e9a/matplotlib-3.9.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974896ec43c672ec23f3f8c648981e8bc880ee163146e0312a9b8def2fac66f5", size = 9090622, upload-time = "2024-12-13T05:54:36.808Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8c/b1f5bd2bd70e60f93b1b54c4d5ba7a992312021d0ddddf572f9a1a6d9348/matplotlib-3.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:4598c394ae9711cec135639374e70871fa36b56afae17bdf032a345be552a88d", size = 7828211, upload-time = "2024-12-13T05:54:40.596Z" }, - { url = "https://files.pythonhosted.org/packages/74/4b/65be7959a8fa118a3929b49a842de5b78bb55475236fcf64f3e308ff74a0/matplotlib-3.9.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d4dd29641d9fb8bc4492420c5480398dd40a09afd73aebe4eb9d0071a05fbe0c", size = 7894430, upload-time = "2024-12-13T05:54:44.049Z" }, - { url = "https://files.pythonhosted.org/packages/e9/18/80f70d91896e0a517b4a051c3fd540daa131630fd75e02e250365353b253/matplotlib-3.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30e5b22e8bcfb95442bf7d48b0d7f3bdf4a450cbf68986ea45fca3d11ae9d099", size = 7780045, upload-time = "2024-12-13T05:54:46.414Z" }, - { url = "https://files.pythonhosted.org/packages/a2/73/ccb381026e3238c5c25c3609ba4157b2d1a617ec98d65a8b4ee4e1e74d02/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bb0030d1d447fd56dcc23b4c64a26e44e898f0416276cac1ebc25522e0ac249", size = 8209906, upload-time = "2024-12-13T05:54:49.459Z" }, - { url = "https://files.pythonhosted.org/packages/ab/33/1648da77b74741c89f5ea95cbf42a291b4b364f2660b316318811404ed97/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aca90ed222ac3565d2752b83dbb27627480d27662671e4d39da72e97f657a423", size = 8322873, upload-time = "2024-12-13T05:54:53.066Z" }, - { url = "https://files.pythonhosted.org/packages/57/d3/8447ba78bc6593c9044c372d1609f8ea10fb1e071e7a9e0747bea74fc16c/matplotlib-3.9.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a181b2aa2906c608fcae72f977a4a2d76e385578939891b91c2550c39ecf361e", size = 9099566, upload-time = "2024-12-13T05:54:55.522Z" }, - { url = "https://files.pythonhosted.org/packages/23/e1/4f0e237bf349c02ff9d1b6e7109f1a17f745263809b9714a8576dc17752b/matplotlib-3.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:1f6882828231eca17f501c4dcd98a05abb3f03d157fbc0769c6911fe08b6cfd3", size = 7838065, upload-time = "2024-12-13T05:54:58.337Z" }, - { url = "https://files.pythonhosted.org/packages/1a/2b/c918bf6c19d6445d1cefe3d2e42cb740fb997e14ab19d4daeb6a7ab8a157/matplotlib-3.9.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dfc48d67e6661378a21c2983200a654b72b5c5cdbd5d2cf6e5e1ece860f0cc70", size = 7891131, upload-time = "2024-12-13T05:55:02.837Z" }, - { url = "https://files.pythonhosted.org/packages/c1/e5/b4e8fc601ca302afeeabf45f30e706a445c7979a180e3a978b78b2b681a4/matplotlib-3.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47aef0fab8332d02d68e786eba8113ffd6f862182ea2999379dec9e237b7e483", size = 7776365, upload-time = "2024-12-13T05:55:05.158Z" }, - { url = "https://files.pythonhosted.org/packages/99/06/b991886c506506476e5d83625c5970c656a491b9f80161458fed94597808/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fba1f52c6b7dc764097f52fd9ab627b90db452c9feb653a59945de16752e965f", size = 8200707, upload-time = "2024-12-13T05:55:09.48Z" }, - { url = "https://files.pythonhosted.org/packages/c3/e2/556b627498cb27e61026f2d1ba86a78ad1b836fef0996bef5440e8bc9559/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:173ac3748acaac21afcc3fa1633924609ba1b87749006bc25051c52c422a5d00", size = 8313761, upload-time = "2024-12-13T05:55:12.95Z" }, - { url = "https://files.pythonhosted.org/packages/58/ff/165af33ec766ff818306ea88e91f9f60d2a6ed543be1eb122a98acbf3b0d/matplotlib-3.9.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320edea0cadc07007765e33f878b13b3738ffa9745c5f707705692df70ffe0e0", size = 9095284, upload-time = "2024-12-13T05:55:16.199Z" }, - { url = "https://files.pythonhosted.org/packages/9f/8b/3d0c7a002db3b1ed702731c2a9a06d78d035f1f2fb0fb936a8e43cc1e9f4/matplotlib-3.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a4a4cfc82330b27042a7169533da7991e8789d180dd5b3daeaee57d75cd5a03b", size = 7841160, upload-time = "2024-12-13T05:55:19.991Z" }, - { url = "https://files.pythonhosted.org/packages/49/b1/999f89a7556d101b23a2f0b54f1b6e140d73f56804da1398f2f0bc0924bc/matplotlib-3.9.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37eeffeeca3c940985b80f5b9a7b95ea35671e0e7405001f249848d2b62351b6", size = 7891499, upload-time = "2024-12-13T05:55:22.142Z" }, - { url = "https://files.pythonhosted.org/packages/87/7b/06a32b13a684977653396a1bfcd34d4e7539c5d55c8cbfaa8ae04d47e4a9/matplotlib-3.9.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3e7465ac859ee4abcb0d836137cd8414e7bb7ad330d905abced457217d4f0f45", size = 7776802, upload-time = "2024-12-13T05:55:25.947Z" }, - { url = "https://files.pythonhosted.org/packages/65/87/ac498451aff739e515891bbb92e566f3c7ef31891aaa878402a71f9b0910/matplotlib-3.9.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4c12302c34afa0cf061bea23b331e747e5e554b0fa595c96e01c7b75bc3b858", size = 8200802, upload-time = "2024-12-13T05:55:28.461Z" }, - { url = "https://files.pythonhosted.org/packages/f8/6b/9eb761c00e1cb838f6c92e5f25dcda3f56a87a52f6cb8fdfa561e6cf6a13/matplotlib-3.9.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b8c97917f21b75e72108b97707ba3d48f171541a74aa2a56df7a40626bafc64", size = 8313880, upload-time = "2024-12-13T05:55:30.965Z" }, - { url = "https://files.pythonhosted.org/packages/d7/a2/c8eaa600e2085eec7e38cbbcc58a30fc78f8224939d31d3152bdafc01fd1/matplotlib-3.9.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0229803bd7e19271b03cb09f27db76c918c467aa4ce2ae168171bc67c3f508df", size = 9094637, upload-time = "2024-12-13T05:55:33.701Z" }, - { url = "https://files.pythonhosted.org/packages/71/1f/c6e1daea55b7bfeb3d84c6cb1abc449f6a02b181e7e2a5e4db34c3afb793/matplotlib-3.9.4-cp313-cp313-win_amd64.whl", hash = "sha256:7c0d8ef442ebf56ff5e206f8083d08252ee738e04f3dc88ea882853a05488799", size = 7841311, upload-time = "2024-12-13T05:55:36.737Z" }, - { url = "https://files.pythonhosted.org/packages/c0/3a/2757d3f7d388b14dd48f5a83bea65b6d69f000e86b8f28f74d86e0d375bd/matplotlib-3.9.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a04c3b00066a688834356d196136349cb32f5e1003c55ac419e91585168b88fb", size = 7919989, upload-time = "2024-12-13T05:55:39.024Z" }, - { url = "https://files.pythonhosted.org/packages/24/28/f5077c79a4f521589a37fe1062d6a6ea3534e068213f7357e7cfffc2e17a/matplotlib-3.9.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04c519587f6c210626741a1e9a68eefc05966ede24205db8982841826af5871a", size = 7809417, upload-time = "2024-12-13T05:55:42.412Z" }, - { url = "https://files.pythonhosted.org/packages/36/c8/c523fd2963156692916a8eb7d4069084cf729359f7955cf09075deddfeaf/matplotlib-3.9.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308afbf1a228b8b525fcd5cec17f246bbbb63b175a3ef6eb7b4d33287ca0cf0c", size = 8226258, upload-time = "2024-12-13T05:55:47.259Z" }, - { url = "https://files.pythonhosted.org/packages/f6/88/499bf4b8fa9349b6f5c0cf4cead0ebe5da9d67769129f1b5651e5ac51fbc/matplotlib-3.9.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddb3b02246ddcffd3ce98e88fed5b238bc5faff10dbbaa42090ea13241d15764", size = 8335849, upload-time = "2024-12-13T05:55:49.763Z" }, - { url = "https://files.pythonhosted.org/packages/b8/9f/20a4156b9726188646a030774ee337d5ff695a965be45ce4dbcb9312c170/matplotlib-3.9.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8a75287e9cb9eee48cb79ec1d806f75b29c0fde978cb7223a1f4c5848d696041", size = 9102152, upload-time = "2024-12-13T05:55:51.997Z" }, - { url = "https://files.pythonhosted.org/packages/10/11/237f9c3a4e8d810b1759b67ff2da7c32c04f9c80aa475e7beb36ed43a8fb/matplotlib-3.9.4-cp313-cp313t-win_amd64.whl", hash = "sha256:488deb7af140f0ba86da003e66e10d55ff915e152c78b4b66d231638400b1965", size = 7896987, upload-time = "2024-12-13T05:55:55.941Z" }, - { url = "https://files.pythonhosted.org/packages/56/eb/501b465c9fef28f158e414ea3a417913dc2ac748564c7ed41535f23445b4/matplotlib-3.9.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3c3724d89a387ddf78ff88d2a30ca78ac2b4c89cf37f2db4bd453c34799e933c", size = 7885919, upload-time = "2024-12-13T05:55:59.66Z" }, - { url = "https://files.pythonhosted.org/packages/da/36/236fbd868b6c91309a5206bd90c3f881f4f44b2d997cd1d6239ef652f878/matplotlib-3.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d5f0a8430ffe23d7e32cfd86445864ccad141797f7d25b7c41759a5b5d17cfd7", size = 7771486, upload-time = "2024-12-13T05:56:04.264Z" }, - { url = "https://files.pythonhosted.org/packages/e0/4b/105caf2d54d5ed11d9f4335398f5103001a03515f2126c936a752ccf1461/matplotlib-3.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bb0141a21aef3b64b633dc4d16cbd5fc538b727e4958be82a0e1c92a234160e", size = 8201838, upload-time = "2024-12-13T05:56:06.792Z" }, - { url = "https://files.pythonhosted.org/packages/5d/a7/bb01188fb4013d34d274caf44a2f8091255b0497438e8b6c0a7c1710c692/matplotlib-3.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57aa235109e9eed52e2c2949db17da185383fa71083c00c6c143a60e07e0888c", size = 8314492, upload-time = "2024-12-13T05:56:09.964Z" }, - { url = "https://files.pythonhosted.org/packages/33/19/02e1a37f7141fc605b193e927d0a9cdf9dc124a20b9e68793f4ffea19695/matplotlib-3.9.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b18c600061477ccfdd1e6fd050c33d8be82431700f3452b297a56d9ed7037abb", size = 9092500, upload-time = "2024-12-13T05:56:13.55Z" }, - { url = "https://files.pythonhosted.org/packages/57/68/c2feb4667adbf882ffa4b3e0ac9967f848980d9f8b5bebd86644aa67ce6a/matplotlib-3.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:ef5f2d1b67d2d2145ff75e10f8c008bfbf71d45137c4b648c87193e7dd053eac", size = 7822962, upload-time = "2024-12-13T05:56:16.358Z" }, - { url = "https://files.pythonhosted.org/packages/0c/22/2ef6a364cd3f565442b0b055e0599744f1e4314ec7326cdaaa48a4d864d7/matplotlib-3.9.4-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:44e0ed786d769d85bc787b0606a53f2d8d2d1d3c8a2608237365e9121c1a338c", size = 7877995, upload-time = "2024-12-13T05:56:18.805Z" }, - { url = "https://files.pythonhosted.org/packages/87/b8/2737456e566e9f4d94ae76b8aa0d953d9acb847714f9a7ad80184474f5be/matplotlib-3.9.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:09debb9ce941eb23ecdbe7eab972b1c3e0276dcf01688073faff7b0f61d6c6ca", size = 7769300, upload-time = "2024-12-13T05:56:21.315Z" }, - { url = "https://files.pythonhosted.org/packages/b2/1f/e709c6ec7b5321e6568769baa288c7178e60a93a9da9e682b39450da0e29/matplotlib-3.9.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc53cf157a657bfd03afab14774d54ba73aa84d42cfe2480c91bd94873952db", size = 8313423, upload-time = "2024-12-13T05:56:26.719Z" }, - { url = "https://files.pythonhosted.org/packages/5e/b6/5a1f868782cd13f053a679984e222007ecff654a9bfbac6b27a65f4eeb05/matplotlib-3.9.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ad45da51be7ad02387801fd154ef74d942f49fe3fcd26a64c94842ba7ec0d865", size = 7854624, upload-time = "2024-12-13T05:56:29.359Z" }, ] [[package]] name = "matplotlib" version = "3.10.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "cycler", marker = "python_full_version >= '3.10'" }, - { name = "fonttools", marker = "python_full_version >= '3.10'" }, - { name = "kiwisolver", version = "1.4.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "contourpy" }, + { 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.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "packaging", marker = "python_full_version >= '3.10'" }, - { name = "pillow", marker = "python_full_version >= '3.10'" }, - { name = "pyparsing", marker = "python_full_version >= '3.10'" }, - { name = "python-dateutil", marker = "python_full_version >= '3.10'" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, ] sdist = { url = "https://files.pythonhosted.org/packages/26/91/d49359a21893183ed2a5b6c76bec40e0b1dcbf8ca148f864d134897cfc75/matplotlib-3.10.3.tar.gz", hash = "sha256:2f82d2c5bb7ae93aaaa4cd42aca65d76ce6376f83304fa3a630b569aca274df0", size = 34799811, upload-time = "2025-05-08T19:10:54.39Z" } wheels = [ @@ -2371,11 +1747,9 @@ name = "mkdocs" version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "click" }, { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "ghp-import" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jinja2" }, { name = "markdown" }, { name = "markupsafe" }, @@ -2411,7 +1785,6 @@ name = "mkdocs-get-deps" version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "mergedeep" }, { name = "platformdirs" }, { name = "pyyaml" }, @@ -2509,7 +1882,6 @@ name = "mkdocstrings" version = "0.29.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jinja2" }, { name = "markdown" }, { name = "markupsafe" }, @@ -2569,7 +1941,6 @@ dependencies = [ { name = "beautifulsoup4" }, { name = "bleach", extra = ["css"] }, { name = "defusedxml" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "jinja2" }, { name = "jupyter-core" }, { name = "jupyterlab-pygments" }, @@ -2679,71 +2050,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307, upload-time = "2024-02-14T23:35:16.286Z" }, ] -[[package]] -name = "numpy" -version = "2.0.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015, upload-time = "2024-08-26T20:19:40.945Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/21/91/3495b3237510f79f5d81f2508f9f13fea78ebfdf07538fc7444badda173d/numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece", size = 21165245, upload-time = "2024-08-26T20:04:14.625Z" }, - { url = "https://files.pythonhosted.org/packages/05/33/26178c7d437a87082d11019292dce6d3fe6f0e9026b7b2309cbf3e489b1d/numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04", size = 13738540, upload-time = "2024-08-26T20:04:36.784Z" }, - { url = "https://files.pythonhosted.org/packages/ec/31/cc46e13bf07644efc7a4bf68df2df5fb2a1a88d0cd0da9ddc84dc0033e51/numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66", size = 5300623, upload-time = "2024-08-26T20:04:46.491Z" }, - { url = "https://files.pythonhosted.org/packages/6e/16/7bfcebf27bb4f9d7ec67332ffebee4d1bf085c84246552d52dbb548600e7/numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b", size = 6901774, upload-time = "2024-08-26T20:04:58.173Z" }, - { url = "https://files.pythonhosted.org/packages/f9/a3/561c531c0e8bf082c5bef509d00d56f82e0ea7e1e3e3a7fc8fa78742a6e5/numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd", size = 13907081, upload-time = "2024-08-26T20:05:19.098Z" }, - { url = "https://files.pythonhosted.org/packages/fa/66/f7177ab331876200ac7563a580140643d1179c8b4b6a6b0fc9838de2a9b8/numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318", size = 19523451, upload-time = "2024-08-26T20:05:47.479Z" }, - { url = "https://files.pythonhosted.org/packages/25/7f/0b209498009ad6453e4efc2c65bcdf0ae08a182b2b7877d7ab38a92dc542/numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8", size = 19927572, upload-time = "2024-08-26T20:06:17.137Z" }, - { url = "https://files.pythonhosted.org/packages/3e/df/2619393b1e1b565cd2d4c4403bdd979621e2c4dea1f8532754b2598ed63b/numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326", size = 14400722, upload-time = "2024-08-26T20:06:39.16Z" }, - { url = "https://files.pythonhosted.org/packages/22/ad/77e921b9f256d5da36424ffb711ae79ca3f451ff8489eeca544d0701d74a/numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97", size = 6472170, upload-time = "2024-08-26T20:06:50.361Z" }, - { url = "https://files.pythonhosted.org/packages/10/05/3442317535028bc29cf0c0dd4c191a4481e8376e9f0db6bcf29703cadae6/numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131", size = 15905558, upload-time = "2024-08-26T20:07:13.881Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cf/034500fb83041aa0286e0fb16e7c76e5c8b67c0711bb6e9e9737a717d5fe/numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448", size = 21169137, upload-time = "2024-08-26T20:07:45.345Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d9/32de45561811a4b87fbdee23b5797394e3d1504b4a7cf40c10199848893e/numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195", size = 13703552, upload-time = "2024-08-26T20:08:06.666Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ca/2f384720020c7b244d22508cb7ab23d95f179fcfff33c31a6eeba8d6c512/numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57", size = 5298957, upload-time = "2024-08-26T20:08:15.83Z" }, - { url = "https://files.pythonhosted.org/packages/0e/78/a3e4f9fb6aa4e6fdca0c5428e8ba039408514388cf62d89651aade838269/numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a", size = 6905573, upload-time = "2024-08-26T20:08:27.185Z" }, - { url = "https://files.pythonhosted.org/packages/a0/72/cfc3a1beb2caf4efc9d0b38a15fe34025230da27e1c08cc2eb9bfb1c7231/numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669", size = 13914330, upload-time = "2024-08-26T20:08:48.058Z" }, - { url = "https://files.pythonhosted.org/packages/ba/a8/c17acf65a931ce551fee11b72e8de63bf7e8a6f0e21add4c937c83563538/numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951", size = 19534895, upload-time = "2024-08-26T20:09:16.536Z" }, - { url = "https://files.pythonhosted.org/packages/ba/86/8767f3d54f6ae0165749f84648da9dcc8cd78ab65d415494962c86fac80f/numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9", size = 19937253, upload-time = "2024-08-26T20:09:46.263Z" }, - { url = "https://files.pythonhosted.org/packages/df/87/f76450e6e1c14e5bb1eae6836478b1028e096fd02e85c1c37674606ab752/numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15", size = 14414074, upload-time = "2024-08-26T20:10:08.483Z" }, - { url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4", size = 6470640, upload-time = "2024-08-26T20:10:19.732Z" }, - { url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc", size = 15910230, upload-time = "2024-08-26T20:10:43.413Z" }, - { url = "https://files.pythonhosted.org/packages/45/40/2e117be60ec50d98fa08c2f8c48e09b3edea93cfcabd5a9ff6925d54b1c2/numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b", size = 20895803, upload-time = "2024-08-26T20:11:13.916Z" }, - { url = "https://files.pythonhosted.org/packages/46/92/1b8b8dee833f53cef3e0a3f69b2374467789e0bb7399689582314df02651/numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e", size = 13471835, upload-time = "2024-08-26T20:11:34.779Z" }, - { url = "https://files.pythonhosted.org/packages/7f/19/e2793bde475f1edaea6945be141aef6c8b4c669b90c90a300a8954d08f0a/numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c", size = 5038499, upload-time = "2024-08-26T20:11:43.902Z" }, - { url = "https://files.pythonhosted.org/packages/e3/ff/ddf6dac2ff0dd50a7327bcdba45cb0264d0e96bb44d33324853f781a8f3c/numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c", size = 6633497, upload-time = "2024-08-26T20:11:55.09Z" }, - { url = "https://files.pythonhosted.org/packages/72/21/67f36eac8e2d2cd652a2e69595a54128297cdcb1ff3931cfc87838874bd4/numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692", size = 13621158, upload-time = "2024-08-26T20:12:14.95Z" }, - { url = "https://files.pythonhosted.org/packages/39/68/e9f1126d757653496dbc096cb429014347a36b228f5a991dae2c6b6cfd40/numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a", size = 19236173, upload-time = "2024-08-26T20:12:44.049Z" }, - { url = "https://files.pythonhosted.org/packages/d1/e9/1f5333281e4ebf483ba1c888b1d61ba7e78d7e910fdd8e6499667041cc35/numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c", size = 19634174, upload-time = "2024-08-26T20:13:13.634Z" }, - { url = "https://files.pythonhosted.org/packages/71/af/a469674070c8d8408384e3012e064299f7a2de540738a8e414dcfd639996/numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded", size = 14099701, upload-time = "2024-08-26T20:13:34.851Z" }, - { url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313, upload-time = "2024-08-26T20:13:45.653Z" }, - { url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179, upload-time = "2024-08-26T20:14:08.786Z" }, - { url = "https://files.pythonhosted.org/packages/43/c1/41c8f6df3162b0c6ffd4437d729115704bd43363de0090c7f913cfbc2d89/numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c", size = 21169942, upload-time = "2024-08-26T20:14:40.108Z" }, - { url = "https://files.pythonhosted.org/packages/39/bc/fd298f308dcd232b56a4031fd6ddf11c43f9917fbc937e53762f7b5a3bb1/numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd", size = 13711512, upload-time = "2024-08-26T20:15:00.985Z" }, - { url = "https://files.pythonhosted.org/packages/96/ff/06d1aa3eeb1c614eda245c1ba4fb88c483bee6520d361641331872ac4b82/numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b", size = 5306976, upload-time = "2024-08-26T20:15:10.876Z" }, - { url = "https://files.pythonhosted.org/packages/2d/98/121996dcfb10a6087a05e54453e28e58694a7db62c5a5a29cee14c6e047b/numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729", size = 6906494, upload-time = "2024-08-26T20:15:22.055Z" }, - { url = "https://files.pythonhosted.org/packages/15/31/9dffc70da6b9bbf7968f6551967fc21156207366272c2a40b4ed6008dc9b/numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1", size = 13912596, upload-time = "2024-08-26T20:15:42.452Z" }, - { url = "https://files.pythonhosted.org/packages/b9/14/78635daab4b07c0930c919d451b8bf8c164774e6a3413aed04a6d95758ce/numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd", size = 19526099, upload-time = "2024-08-26T20:16:11.048Z" }, - { url = "https://files.pythonhosted.org/packages/26/4c/0eeca4614003077f68bfe7aac8b7496f04221865b3a5e7cb230c9d055afd/numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d", size = 19932823, upload-time = "2024-08-26T20:16:40.171Z" }, - { url = "https://files.pythonhosted.org/packages/f1/46/ea25b98b13dccaebddf1a803f8c748680d972e00507cd9bc6dcdb5aa2ac1/numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d", size = 14404424, upload-time = "2024-08-26T20:17:02.604Z" }, - { url = "https://files.pythonhosted.org/packages/c8/a6/177dd88d95ecf07e722d21008b1b40e681a929eb9e329684d449c36586b2/numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa", size = 6476809, upload-time = "2024-08-26T20:17:13.553Z" }, - { url = "https://files.pythonhosted.org/packages/ea/2b/7fc9f4e7ae5b507c1a3a21f0f15ed03e794c1242ea8a242ac158beb56034/numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73", size = 15911314, upload-time = "2024-08-26T20:17:36.72Z" }, - { url = "https://files.pythonhosted.org/packages/8f/3b/df5a870ac6a3be3a86856ce195ef42eec7ae50d2a202be1f5a4b3b340e14/numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8", size = 21025288, upload-time = "2024-08-26T20:18:07.732Z" }, - { url = "https://files.pythonhosted.org/packages/2c/97/51af92f18d6f6f2d9ad8b482a99fb74e142d71372da5d834b3a2747a446e/numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4", size = 6762793, upload-time = "2024-08-26T20:18:19.125Z" }, - { url = "https://files.pythonhosted.org/packages/12/46/de1fbd0c1b5ccaa7f9a005b66761533e2f6a3e560096682683a223631fe9/numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c", size = 19334885, upload-time = "2024-08-26T20:18:47.237Z" }, - { url = "https://files.pythonhosted.org/packages/cc/dc/d330a6faefd92b446ec0f0dfea4c3207bb1fef3c4771d19cf4543efd2c78/numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385", size = 15828784, upload-time = "2024-08-26T20:19:11.19Z" }, -] - [[package]] name = "numpy" version = "2.2.6" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", ] sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } wheels = [ @@ -2874,8 +2188,7 @@ name = "opencv-python" version = "4.11.0.86" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/17/06/68c27a523103dad5837dc5b87e71285280c4f098c60e4fe8a8db6486ab09/opencv-python-4.11.0.86.tar.gz", hash = "sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4", size = 95171956, upload-time = "2025-01-16T13:52:24.737Z" } @@ -2920,8 +2233,7 @@ name = "pandas" version = "2.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "python-dateutil" }, { name = "pytz" }, @@ -2963,13 +2275,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/85/86/1fa345fc17caf5d7780d2699985c03dbe186c68fee00b526813939062bb0/pandas-2.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f4d6feeba91744872a600e6edbbd5b033005b431d5ae8379abee5bcfa479fab", size = 11883998, upload-time = "2025-07-07T19:19:34.267Z" }, { url = "https://files.pythonhosted.org/packages/81/aa/e58541a49b5e6310d89474333e994ee57fea97c8aaa8fc7f00b873059bbf/pandas-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fe37e757f462d31a9cd7580236a82f353f5713a80e059a29753cf938c6775d96", size = 12704705, upload-time = "2025-07-07T19:19:36.856Z" }, { url = "https://files.pythonhosted.org/packages/d5/f9/07086f5b0f2a19872554abeea7658200824f5835c58a106fa8f2ae96a46c/pandas-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5db9637dbc24b631ff3707269ae4559bce4b7fd75c1c4d7e13f40edc42df4444", size = 13189044, upload-time = "2025-07-07T19:19:39.999Z" }, - { url = "https://files.pythonhosted.org/packages/6e/21/ecf2df680982616459409b09962a8c2065330c7151dc6538069f3b634acf/pandas-2.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4645f770f98d656f11c69e81aeb21c6fca076a44bed3dcbb9396a4311bc7f6d8", size = 11567275, upload-time = "2025-07-07T19:19:45.152Z" }, - { url = "https://files.pythonhosted.org/packages/1e/1a/dcb50e44b75419e96b276c9fb023b0f147b3c411be1cd517492aa2a184d4/pandas-2.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:342e59589cc454aaff7484d75b816a433350b3d7964d7847327edda4d532a2e3", size = 10811488, upload-time = "2025-07-07T19:19:47.797Z" }, - { url = "https://files.pythonhosted.org/packages/2d/55/66cd2b679f6a27398380eac7574bc24746128f74626a3c02b978ea00e5ce/pandas-2.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d12f618d80379fde6af007f65f0c25bd3e40251dbd1636480dfffce2cf1e6da", size = 11763000, upload-time = "2025-07-07T19:19:50.83Z" }, - { url = "https://files.pythonhosted.org/packages/ae/1c/5b9b263c80fd5e231b77df6f78cd7426d1d4ad3a4e858e85b7b3d93d0e9c/pandas-2.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd71c47a911da120d72ef173aeac0bf5241423f9bfea57320110a978457e069e", size = 12361395, upload-time = "2025-07-07T19:19:53.714Z" }, - { url = "https://files.pythonhosted.org/packages/f7/74/7e817b31413fbb96366ea327d43d1926a9c48c58074e27e094e2839a0e36/pandas-2.3.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:09e3b1587f0f3b0913e21e8b32c3119174551deb4a4eba4a89bc7377947977e7", size = 13225086, upload-time = "2025-07-07T19:19:56.378Z" }, - { url = "https://files.pythonhosted.org/packages/1f/0f/bc0a44b47eba2f22ae4235719a573d552ef7ad76ed3ea39ae62d554e040b/pandas-2.3.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2323294c73ed50f612f67e2bf3ae45aea04dce5690778e08a09391897f35ff88", size = 13871698, upload-time = "2025-07-07T19:19:58.854Z" }, - { url = "https://files.pythonhosted.org/packages/fa/cb/6c32f8fadefa4314b740fbe8f74f6a02423bd1549e7c930826df35ac3c1b/pandas-2.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:b4b0de34dc8499c2db34000ef8baad684cfa4cbd836ecee05f323ebfba348c7d", size = 11357186, upload-time = "2025-07-07T19:20:01.475Z" }, ] [[package]] @@ -3097,17 +2402,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" }, { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" }, { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8e/9c089f01677d1264ab8648352dcb7773f37da6ad002542760c80107da816/pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f", size = 5316478, upload-time = "2025-07-01T09:15:52.209Z" }, - { url = "https://files.pythonhosted.org/packages/b5/a9/5749930caf674695867eb56a581e78eb5f524b7583ff10b01b6e5048acb3/pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081", size = 4686522, upload-time = "2025-07-01T09:15:54.162Z" }, - { url = "https://files.pythonhosted.org/packages/43/46/0b85b763eb292b691030795f9f6bb6fcaf8948c39413c81696a01c3577f7/pillow-11.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4", size = 5853376, upload-time = "2025-07-03T13:11:01.066Z" }, - { url = "https://files.pythonhosted.org/packages/5e/c6/1a230ec0067243cbd60bc2dad5dc3ab46a8a41e21c15f5c9b52b26873069/pillow-11.3.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc", size = 7626020, upload-time = "2025-07-03T13:11:06.479Z" }, - { url = "https://files.pythonhosted.org/packages/63/dd/f296c27ffba447bfad76c6a0c44c1ea97a90cb9472b9304c94a732e8dbfb/pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06", size = 5956732, upload-time = "2025-07-01T09:15:56.111Z" }, - { url = "https://files.pythonhosted.org/packages/a5/a0/98a3630f0b57f77bae67716562513d3032ae70414fcaf02750279c389a9e/pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a", size = 6624404, upload-time = "2025-07-01T09:15:58.245Z" }, - { url = "https://files.pythonhosted.org/packages/de/e6/83dfba5646a290edd9a21964da07674409e410579c341fc5b8f7abd81620/pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978", size = 6067760, upload-time = "2025-07-01T09:16:00.003Z" }, - { url = "https://files.pythonhosted.org/packages/bc/41/15ab268fe6ee9a2bc7391e2bbb20a98d3974304ab1a406a992dcb297a370/pillow-11.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d", size = 6700534, upload-time = "2025-07-01T09:16:02.29Z" }, - { url = "https://files.pythonhosted.org/packages/64/79/6d4f638b288300bed727ff29f2a3cb63db054b33518a95f27724915e3fbc/pillow-11.3.0-cp39-cp39-win32.whl", hash = "sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71", size = 6277091, upload-time = "2025-07-01T09:16:04.4Z" }, - { url = "https://files.pythonhosted.org/packages/46/05/4106422f45a05716fd34ed21763f8ec182e8ea00af6e9cb05b93a247361a/pillow-11.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada", size = 6986091, upload-time = "2025-07-01T09:16:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/63/c6/287fd55c2c12761d0591549d48885187579b7c257bef0c6660755b0b59ae/pillow-11.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb", size = 2422632, upload-time = "2025-07-01T09:16:08.142Z" }, { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556, upload-time = "2025-07-01T09:16:09.961Z" }, { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625, upload-time = "2025-07-01T09:16:11.913Z" }, { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207, upload-time = "2025-07-03T13:11:10.201Z" }, @@ -3142,48 +2436,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] -[[package]] -name = "pre-commit" -version = "4.3.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "cfgv", version = "3.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "identify", version = "2.6.15", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "nodeenv", marker = "python_full_version < '3.10'" }, - { name = "pyyaml", marker = "python_full_version < '3.10'" }, - { name = "virtualenv", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, -] - [[package]] name = "pre-commit" version = "4.5.1" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version >= '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] dependencies = [ - { name = "cfgv", version = "3.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "identify", version = "2.6.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "nodeenv", marker = "python_full_version >= '3.10'" }, - { name = "pyyaml", marker = "python_full_version >= '3.10'" }, - { name = "virtualenv", marker = "python_full_version >= '3.10'" }, + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } wheels = [ @@ -3338,8 +2600,7 @@ name = "pytest-cov" version = "7.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.10'" }, - { name = "coverage", version = "7.13.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" }, + { name = "coverage", extra = ["toml"] }, { name = "pluggy" }, { name = "pytest" }, ] @@ -3364,9 +2625,6 @@ wheels = [ name = "python-json-logger" version = "3.3.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.10'" }, -] sdist = { url = "https://files.pythonhosted.org/packages/9e/de/d3144a0bceede957f961e975f3752760fbe390d57fbe194baf709d8f1f7b/python_json_logger-3.3.0.tar.gz", hash = "sha256:12b7e74b17775e7d565129296105bbe3910842d9d0eb083fc83a6a617aa8df84", size = 16642, upload-time = "2025-03-07T07:08:27.301Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/08/20/0f2523b9e50a8052bc6a8b732dfc8568abbdc42010aef03a2d750bdab3b2/python_json_logger-3.3.0-py3-none-any.whl", hash = "sha256:dd980fae8cffb24c13caf6e158d3d61c0d6d22342f932cb6e9deedab3d35eec7", size = 15163, upload-time = "2025-03-07T07:08:25.627Z" }, @@ -3398,8 +2656,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1c/09/9c1b978ffc4ae53999e89c19c77ba882d9fce476729f23ef55211ea1c034/pywin32-310-cp313-cp313-win32.whl", hash = "sha256:5d241a659c496ada3253cd01cfaa779b048e90ce4b2b38cd44168ad555ce74ab", size = 8794384, upload-time = "2025-03-17T00:56:04.383Z" }, { url = "https://files.pythonhosted.org/packages/45/3c/b4640f740ffebadd5d34df35fecba0e1cfef8fde9f3e594df91c28ad9b50/pywin32-310-cp313-cp313-win_amd64.whl", hash = "sha256:667827eb3a90208ddbdcc9e860c81bde63a135710e21e4cb3348968e4bd5249e", size = 9503039, upload-time = "2025-03-17T00:56:06.207Z" }, { url = "https://files.pythonhosted.org/packages/b4/f4/f785020090fb050e7fb6d34b780f2231f302609dc964672f72bfaeb59a28/pywin32-310-cp313-cp313-win_arm64.whl", hash = "sha256:e308f831de771482b7cf692a1f308f8fca701b2d8f9dde6cc440c7da17e47b33", size = 8458152, upload-time = "2025-03-17T00:56:07.819Z" }, - { url = "https://files.pythonhosted.org/packages/a2/cd/d09d434630edb6a0c44ad5079611279a67530296cfe0451e003de7f449ff/pywin32-310-cp39-cp39-win32.whl", hash = "sha256:851c8d927af0d879221e616ae1f66145253537bbdd321a77e8ef701b443a9a1a", size = 8848099, upload-time = "2025-03-17T00:55:42.415Z" }, - { url = "https://files.pythonhosted.org/packages/93/ff/2a8c10315ffbdee7b3883ac0d1667e267ca8b3f6f640d81d43b87a82c0c7/pywin32-310-cp39-cp39-win_amd64.whl", hash = "sha256:96867217335559ac619f00ad70e513c0fcf84b8a3af9fc2bba3b59b97da70475", size = 9602031, upload-time = "2025-03-17T00:55:44.512Z" }, ] [[package]] @@ -3422,7 +2678,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/e5/9714def18c3a411809771a3fbcec70bffa764b9675afb00048a620fca604/pywinpty-2.0.15-cp312-cp312-win_amd64.whl", hash = "sha256:83a8f20b430bbc5d8957249f875341a60219a4e971580f2ba694fbfb54a45ebc", size = 1405243, upload-time = "2025-02-03T21:56:52.476Z" }, { url = "https://files.pythonhosted.org/packages/fb/16/2ab7b3b7f55f3c6929e5f629e1a68362981e4e5fed592a2ed1cb4b4914a5/pywinpty-2.0.15-cp313-cp313-win_amd64.whl", hash = "sha256:ab5920877dd632c124b4ed17bc6dd6ef3b9f86cd492b963ffdb1a67b85b0f408", size = 1405020, upload-time = "2025-02-03T21:56:04.753Z" }, { url = "https://files.pythonhosted.org/packages/7c/16/edef3515dd2030db2795dbfbe392232c7a0f3dc41b98e92b38b42ba497c7/pywinpty-2.0.15-cp313-cp313t-win_amd64.whl", hash = "sha256:a4560ad8c01e537708d2790dbe7da7d986791de805d89dd0d3697ca59e9e4901", size = 1404151, upload-time = "2025-02-03T21:55:53.628Z" }, - { url = "https://files.pythonhosted.org/packages/47/96/90fa02f19b1eff7469ad7bf0ef8efca248025de9f1d0a0b25682d2aacf68/pywinpty-2.0.15-cp39-cp39-win_amd64.whl", hash = "sha256:d261cd88fcd358cfb48a7ca0700db3e1c088c9c10403c9ebc0d8a8b57aa6a117", size = 1405302, upload-time = "2025-02-03T21:55:40.394Z" }, ] [[package]] @@ -3467,15 +2722,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, - { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" }, - { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" }, - { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" }, - { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload-time = "2024-08-06T20:33:34.157Z" }, - { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload-time = "2024-08-06T20:33:35.84Z" }, - { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload-time = "2024-08-06T20:33:37.501Z" }, - { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload-time = "2024-08-06T20:33:39.389Z" }, - { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593, upload-time = "2024-08-06T20:33:46.63Z" }, - { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" }, ] [[package]] @@ -3495,8 +2741,8 @@ name = "pyzmq" version = "27.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.10' and implementation_name == 'pypy' and sys_platform == 'darwin') or (implementation_name == 'pypy' and platform_machine == 'arm64' and sys_platform == 'darwin')" }, - { name = "cffi", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.10' and implementation_name == 'pypy' and platform_machine != 'arm64') or (implementation_name == 'pypy' and sys_platform != 'darwin')" }, + { name = "cffi", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "implementation_name == 'pypy' and sys_platform == 'darwin'" }, + { name = "cffi", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "implementation_name == 'pypy' and sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f1/06/50a4e9648b3e8b992bef8eb632e457307553a89d294103213cfd47b3da69/pyzmq-27.0.0.tar.gz", hash = "sha256:b1f08eeb9ce1510e6939b6e5dcd46a17765e2333daae78ecf4606808442e52cf", size = 280478, upload-time = "2025-06-13T14:09:07.087Z" } wheels = [ @@ -3539,16 +2785,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/39/dc2db178c26a42228c5ac94a9cc595030458aa64c8d796a7727947afbf55/pyzmq-27.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:20d5cb29e8c5f76a127c75b6e7a77e846bc4b655c373baa098c26a61b7ecd0ef", size = 1885199, upload-time = "2025-06-13T14:07:57.166Z" }, { url = "https://files.pythonhosted.org/packages/c7/21/dae7b06a1f8cdee5d8e7a63d99c5d129c401acc40410bef2cbf42025e26f/pyzmq-27.0.0-cp313-cp313t-win32.whl", hash = "sha256:a20528da85c7ac7a19b7384e8c3f8fa707841fd85afc4ed56eda59d93e3d98ad", size = 575439, upload-time = "2025-06-13T14:07:58.959Z" }, { url = "https://files.pythonhosted.org/packages/eb/bc/1709dc55f0970cf4cb8259e435e6773f9946f41a045c2cb90e870b7072da/pyzmq-27.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d8229f2efece6a660ee211d74d91dbc2a76b95544d46c74c615e491900dc107f", size = 639933, upload-time = "2025-06-13T14:08:00.777Z" }, - { url = "https://files.pythonhosted.org/packages/19/dc/95210fe17e5d7dba89bd663e1d88f50a8003f296284731b09f1d95309a42/pyzmq-27.0.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:100f6e5052ba42b2533011d34a018a5ace34f8cac67cb03cfa37c8bdae0ca617", size = 1330656, upload-time = "2025-06-13T14:08:17.414Z" }, - { url = "https://files.pythonhosted.org/packages/d3/7e/63f742b578316258e03ecb393d35c0964348d80834bdec8a100ed7bb9c91/pyzmq-27.0.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:bf6c6b061efd00404b9750e2cfbd9507492c8d4b3721ded76cb03786131be2ed", size = 906522, upload-time = "2025-06-13T14:08:18.945Z" }, - { url = "https://files.pythonhosted.org/packages/1f/bf/f0b2b67f5a9bfe0fbd0e978a2becd901f802306aa8e29161cb0963094352/pyzmq-27.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee05728c0b0b2484a9fc20466fa776fffb65d95f7317a3419985b8c908563861", size = 863545, upload-time = "2025-06-13T14:08:20.386Z" }, - { url = "https://files.pythonhosted.org/packages/87/0e/7d90ccd2ef577c8bae7f926acd2011a6d960eea8a068c5fd52b419206960/pyzmq-27.0.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7cdf07fe0a557b131366f80727ec8ccc4b70d89f1e3f920d94a594d598d754f0", size = 666796, upload-time = "2025-06-13T14:08:21.836Z" }, - { url = "https://files.pythonhosted.org/packages/4f/6d/ca8007a313baa73361778773aef210f4902e68f468d1f93b6c8b908fabbd/pyzmq-27.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:90252fa2ff3a104219db1f5ced7032a7b5fc82d7c8d2fec2b9a3e6fd4e25576b", size = 1655599, upload-time = "2025-06-13T14:08:23.343Z" }, - { url = "https://files.pythonhosted.org/packages/46/de/5cb4f99d6c0dd8f33d729c9ebd49af279586e5ab127e93aa6ef0ecd08c4c/pyzmq-27.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ea6d441c513bf18c578c73c323acf7b4184507fc244762193aa3a871333c9045", size = 2034119, upload-time = "2025-06-13T14:08:26.369Z" }, - { url = "https://files.pythonhosted.org/packages/d0/8d/57cc90c8b5f30a97a7e86ec91a3b9822ec7859d477e9c30f531fb78f4a97/pyzmq-27.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ae2b34bcfaae20c064948a4113bf8709eee89fd08317eb293ae4ebd69b4d9740", size = 1891955, upload-time = "2025-06-13T14:08:28.39Z" }, - { url = "https://files.pythonhosted.org/packages/24/f5/a7012022573188903802ab75b5314b00e5c629228f3a36fadb421a42ebff/pyzmq-27.0.0-cp39-cp39-win32.whl", hash = "sha256:5b10bd6f008937705cf6e7bf8b6ece5ca055991e3eb130bca8023e20b86aa9a3", size = 568497, upload-time = "2025-06-13T14:08:30.089Z" }, - { url = "https://files.pythonhosted.org/packages/9b/f3/2a4b2798275a574801221d94d599ed3e26d19f6378a7364cdfa3bee53944/pyzmq-27.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:00387d12a8af4b24883895f7e6b9495dc20a66027b696536edac35cb988c38f3", size = 629315, upload-time = "2025-06-13T14:08:31.877Z" }, - { url = "https://files.pythonhosted.org/packages/da/eb/386a70314f305816142d6e8537f5557e5fd9614c03698d6c88cbd6c41190/pyzmq-27.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:4c19d39c04c29a6619adfeb19e3735c421b3bfee082f320662f52e59c47202ba", size = 559596, upload-time = "2025-06-13T14:08:33.357Z" }, { url = "https://files.pythonhosted.org/packages/09/6f/be6523a7f3821c0b5370912ef02822c028611360e0d206dd945bdbf9eaef/pyzmq-27.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:656c1866505a5735d0660b7da6d7147174bbf59d4975fc2b7f09f43c9bc25745", size = 835950, upload-time = "2025-06-13T14:08:35Z" }, { url = "https://files.pythonhosted.org/packages/c6/1e/a50fdd5c15018de07ab82a61bc460841be967ee7bbe7abee3b714d66f7ac/pyzmq-27.0.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74175b9e12779382432dd1d1f5960ebe7465d36649b98a06c6b26be24d173fab", size = 799876, upload-time = "2025-06-13T14:08:36.849Z" }, { url = "https://files.pythonhosted.org/packages/88/a1/89eb5b71f5a504f8f887aceb8e1eb3626e00c00aa8085381cdff475440dc/pyzmq-27.0.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8c6de908465697a8708e4d6843a1e884f567962fc61eb1706856545141d0cbb", size = 567400, upload-time = "2025-06-13T14:08:38.95Z" }, @@ -3559,11 +2795,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/ba/64af397e0f421453dc68e31d5e0784d554bf39013a2de0872056e96e58af/pyzmq-27.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14fe7aaac86e4e93ea779a821967360c781d7ac5115b3f1a171ced77065a0174", size = 567400, upload-time = "2025-06-13T14:08:46.855Z" }, { url = "https://files.pythonhosted.org/packages/63/87/ec956cbe98809270b59a22891d5758edae147a258e658bf3024a8254c855/pyzmq-27.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ad0562d4e6abb785be3e4dd68599c41be821b521da38c402bc9ab2a8e7ebc7e", size = 747031, upload-time = "2025-06-13T14:08:48.419Z" }, { url = "https://files.pythonhosted.org/packages/be/8a/4a3764a68abc02e2fbb0668d225b6fda5cd39586dd099cee8b2ed6ab0452/pyzmq-27.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:9df43a2459cd3a3563404c1456b2c4c69564daa7dbaf15724c09821a3329ce46", size = 544726, upload-time = "2025-06-13T14:08:49.903Z" }, - { url = "https://files.pythonhosted.org/packages/03/f6/11b2a6c8cd13275c31cddc3f89981a1b799a3c41dec55289fa18dede96b5/pyzmq-27.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:39ddd3ba0a641f01d8f13a3cfd4c4924eb58e660d8afe87e9061d6e8ca6f7ac3", size = 835944, upload-time = "2025-06-13T14:08:59.189Z" }, - { url = "https://files.pythonhosted.org/packages/73/34/aa39076f4e07ae1912fa4b966fe24e831e01d736d4c1c7e8a3aa28a555b5/pyzmq-27.0.0-pp39-pypy39_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:8ca7e6a0388dd9e1180b14728051068f4efe83e0d2de058b5ff92c63f399a73f", size = 799869, upload-time = "2025-06-13T14:09:00.758Z" }, - { url = "https://files.pythonhosted.org/packages/65/f3/81ed6b3dd242408ee79c0d8a88734644acf208baee8666ecd7e52664cf55/pyzmq-27.0.0-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2524c40891be6a3106885a3935d58452dd83eb7a5742a33cc780a1ad4c49dec0", size = 758371, upload-time = "2025-06-13T14:09:02.461Z" }, - { url = "https://files.pythonhosted.org/packages/e1/04/dac4ca674764281caf744e8adefd88f7e325e1605aba0f9a322094b903fa/pyzmq-27.0.0-pp39-pypy39_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a56e3e5bd2d62a01744fd2f1ce21d760c7c65f030e9522738d75932a14ab62a", size = 567393, upload-time = "2025-06-13T14:09:04.037Z" }, - { url = "https://files.pythonhosted.org/packages/51/8b/619a9ee2fa4d3c724fbadde946427735ade64da03894b071bbdc3b789d83/pyzmq-27.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:096af9e133fec3a72108ddefba1e42985cb3639e9de52cfd336b6fc23aa083e9", size = 544715, upload-time = "2025-06-13T14:09:05.579Z" }, ] [[package]] @@ -3766,19 +2997,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/d9/3f0f105420fecd18551b678c9a6ce60bd23986098b252a56d35781b3e7e9/rpds_py-0.26.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1851f429b822831bd2edcbe0cfd12ee9ea77868f8d3daf267b189371671c80e", size = 554886, upload-time = "2025-07-01T15:55:52.541Z" }, { url = "https://files.pythonhosted.org/packages/6b/c5/347c056a90dc8dd9bc240a08c527315008e1b5042e7a4cf4ac027be9d38a/rpds_py-0.26.0-cp314-cp314t-win32.whl", hash = "sha256:7bdb17009696214c3b66bb3590c6d62e14ac5935e53e929bcdbc5a495987a84f", size = 219027, upload-time = "2025-07-01T15:55:53.874Z" }, { url = "https://files.pythonhosted.org/packages/75/04/5302cea1aa26d886d34cadbf2dc77d90d7737e576c0065f357b96dc7a1a6/rpds_py-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f14440b9573a6f76b4ee4770c13f0b5921f71dde3b6fcb8dabbefd13b7fe05d7", size = 232821, upload-time = "2025-07-01T15:55:55.167Z" }, - { url = "https://files.pythonhosted.org/packages/fb/74/846ab687119c9d31fc21ab1346ef9233c31035ce53c0e2d43a130a0c5a5e/rpds_py-0.26.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:7a48af25d9b3c15684059d0d1fc0bc30e8eee5ca521030e2bffddcab5be40226", size = 372786, upload-time = "2025-07-01T15:55:56.512Z" }, - { url = "https://files.pythonhosted.org/packages/33/02/1f9e465cb1a6032d02b17cd117c7bd9fb6156bc5b40ffeb8053d8a2aa89c/rpds_py-0.26.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0c71c2f6bf36e61ee5c47b2b9b5d47e4d1baad6426bfed9eea3e858fc6ee8806", size = 358062, upload-time = "2025-07-01T15:55:58.084Z" }, - { url = "https://files.pythonhosted.org/packages/2a/49/81a38e3c67ac943907a9711882da3d87758c82cf26b2120b8128e45d80df/rpds_py-0.26.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d815d48b1804ed7867b539236b6dd62997850ca1c91cad187f2ddb1b7bbef19", size = 381576, upload-time = "2025-07-01T15:55:59.422Z" }, - { url = "https://files.pythonhosted.org/packages/14/37/418f030a76ef59f41e55f9dc916af8afafa3c9e3be38df744b2014851474/rpds_py-0.26.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:84cfbd4d4d2cdeb2be61a057a258d26b22877266dd905809e94172dff01a42ae", size = 397062, upload-time = "2025-07-01T15:56:00.868Z" }, - { url = "https://files.pythonhosted.org/packages/47/e3/9090817a8f4388bfe58e28136e9682fa7872a06daff2b8a2f8c78786a6e1/rpds_py-0.26.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fbaa70553ca116c77717f513e08815aec458e6b69a028d4028d403b3bc84ff37", size = 516277, upload-time = "2025-07-01T15:56:02.672Z" }, - { url = "https://files.pythonhosted.org/packages/3f/3a/1ec3dd93250fb8023f27d49b3f92e13f679141f2e59a61563f88922c2821/rpds_py-0.26.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39bfea47c375f379d8e87ab4bb9eb2c836e4f2069f0f65731d85e55d74666387", size = 402604, upload-time = "2025-07-01T15:56:04.453Z" }, - { url = "https://files.pythonhosted.org/packages/f2/98/9133c06e42ec3ce637936263c50ac647f879b40a35cfad2f5d4ad418a439/rpds_py-0.26.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1533b7eb683fb5f38c1d68a3c78f5fdd8f1412fa6b9bf03b40f450785a0ab915", size = 383664, upload-time = "2025-07-01T15:56:05.823Z" }, - { url = "https://files.pythonhosted.org/packages/a9/10/a59ce64099cc77c81adb51f06909ac0159c19a3e2c9d9613bab171f4730f/rpds_py-0.26.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c5ab0ee51f560d179b057555b4f601b7df909ed31312d301b99f8b9fc6028284", size = 415944, upload-time = "2025-07-01T15:56:07.132Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f1/ae0c60b3be9df9d5bef3527d83b8eb4b939e3619f6dd8382840e220a27df/rpds_py-0.26.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e5162afc9e0d1f9cae3b577d9c29ddbab3505ab39012cb794d94a005825bde21", size = 558311, upload-time = "2025-07-01T15:56:08.484Z" }, - { url = "https://files.pythonhosted.org/packages/fb/2b/bf1498ebb3ddc5eff2fe3439da88963d1fc6e73d1277fa7ca0c72620d167/rpds_py-0.26.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:43f10b007033f359bc3fa9cd5e6c1e76723f056ffa9a6b5c117cc35720a80292", size = 587928, upload-time = "2025-07-01T15:56:09.946Z" }, - { url = "https://files.pythonhosted.org/packages/b6/eb/e6b949edf7af5629848c06d6e544a36c9f2781e2d8d03b906de61ada04d0/rpds_py-0.26.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e3730a48e5622e598293eee0762b09cff34dd3f271530f47b0894891281f051d", size = 554554, upload-time = "2025-07-01T15:56:11.775Z" }, - { url = "https://files.pythonhosted.org/packages/0a/1c/aa0298372ea898620d4706ad26b5b9e975550a4dd30bd042b0fe9ae72cce/rpds_py-0.26.0-cp39-cp39-win32.whl", hash = "sha256:4b1f66eb81eab2e0ff5775a3a312e5e2e16bf758f7b06be82fb0d04078c7ac51", size = 220273, upload-time = "2025-07-01T15:56:13.273Z" }, - { url = "https://files.pythonhosted.org/packages/b8/b0/8b3bef6ad0b35c172d1c87e2e5c2bb027d99e2a7bc7a16f744e66cf318f3/rpds_py-0.26.0-cp39-cp39-win_amd64.whl", hash = "sha256:519067e29f67b5c90e64fb1a6b6e9d2ec0ba28705c51956637bac23a2f4ddae1", size = 231627, upload-time = "2025-07-01T15:56:14.853Z" }, { url = "https://files.pythonhosted.org/packages/ef/9a/1f033b0b31253d03d785b0cd905bc127e555ab496ea6b4c7c2e1f951f2fd/rpds_py-0.26.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3c0909c5234543ada2515c05dc08595b08d621ba919629e94427e8e03539c958", size = 373226, upload-time = "2025-07-01T15:56:16.578Z" }, { url = "https://files.pythonhosted.org/packages/58/29/5f88023fd6aaaa8ca3c4a6357ebb23f6f07da6079093ccf27c99efce87db/rpds_py-0.26.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c1fb0cda2abcc0ac62f64e2ea4b4e64c57dfd6b885e693095460c61bde7bb18e", size = 359230, upload-time = "2025-07-01T15:56:17.978Z" }, { url = "https://files.pythonhosted.org/packages/6c/6c/13eaebd28b439da6964dde22712b52e53fe2824af0223b8e403249d10405/rpds_py-0.26.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84d142d2d6cf9b31c12aa4878d82ed3b2324226270b89b676ac62ccd7df52d08", size = 382363, upload-time = "2025-07-01T15:56:19.977Z" }, @@ -3802,58 +3020,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d5/a6/33b1fc0c9f7dcfcfc4a4353daa6308b3ece22496ceece348b3e7a7559a09/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2abe21d8ba64cded53a2a677e149ceb76dcf44284202d737178afe7ba540c1eb", size = 559439, upload-time = "2025-07-01T15:56:48.549Z" }, { url = "https://files.pythonhosted.org/packages/71/2d/ceb3f9c12f8cfa56d34995097f6cd99da1325642c60d1b6680dd9df03ed8/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:4feb7511c29f8442cbbc28149a92093d32e815a28aa2c50d333826ad2a20fdf0", size = 588380, upload-time = "2025-07-01T15:56:50.086Z" }, { url = "https://files.pythonhosted.org/packages/c8/ed/9de62c2150ca8e2e5858acf3f4f4d0d180a38feef9fdab4078bea63d8dba/rpds_py-0.26.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e99685fc95d386da368013e7fb4269dd39c30d99f812a8372d62f244f662709c", size = 555334, upload-time = "2025-07-01T15:56:51.703Z" }, - { url = "https://files.pythonhosted.org/packages/7e/78/a08e2f28e91c7e45db1150813c6d760a0fb114d5652b1373897073369e0d/rpds_py-0.26.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a90a13408a7a856b87be8a9f008fff53c5080eea4e4180f6c2e546e4a972fb5d", size = 373157, upload-time = "2025-07-01T15:56:53.291Z" }, - { url = "https://files.pythonhosted.org/packages/52/01/ddf51517497c8224fb0287e9842b820ed93748bc28ea74cab56a71e3dba4/rpds_py-0.26.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3ac51b65e8dc76cf4949419c54c5528adb24fc721df722fd452e5fbc236f5c40", size = 358827, upload-time = "2025-07-01T15:56:54.963Z" }, - { url = "https://files.pythonhosted.org/packages/4d/f4/acaefa44b83705a4fcadd68054280127c07cdb236a44a1c08b7c5adad40b/rpds_py-0.26.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59b2093224a18c6508d95cfdeba8db9cbfd6f3494e94793b58972933fcee4c6d", size = 382182, upload-time = "2025-07-01T15:56:56.474Z" }, - { url = "https://files.pythonhosted.org/packages/e9/a2/d72ac03d37d33f6ff4713ca4c704da0c3b1b3a959f0bf5eb738c0ad94ea2/rpds_py-0.26.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f01a5d6444a3258b00dc07b6ea4733e26f8072b788bef750baa37b370266137", size = 397123, upload-time = "2025-07-01T15:56:58.272Z" }, - { url = "https://files.pythonhosted.org/packages/74/58/c053e9d1da1d3724434dd7a5f506623913e6404d396ff3cf636a910c0789/rpds_py-0.26.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6e2c12160c72aeda9d1283e612f68804621f448145a210f1bf1d79151c47090", size = 516285, upload-time = "2025-07-01T15:57:00.283Z" }, - { url = "https://files.pythonhosted.org/packages/94/41/c81e97ee88b38b6d1847c75f2274dee8d67cb8d5ed7ca8c6b80442dead75/rpds_py-0.26.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb28c1f569f8d33b2b5dcd05d0e6ef7005d8639c54c2f0be824f05aedf715255", size = 402182, upload-time = "2025-07-01T15:57:02.587Z" }, - { url = "https://files.pythonhosted.org/packages/74/74/38a176b34ce5197b4223e295f36350dd90713db13cf3c3b533e8e8f7484e/rpds_py-0.26.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1766b5724c3f779317d5321664a343c07773c8c5fd1532e4039e6cc7d1a815be", size = 384436, upload-time = "2025-07-01T15:57:04.125Z" }, - { url = "https://files.pythonhosted.org/packages/e4/21/f40b9a5709d7078372c87fd11335469dc4405245528b60007cd4078ed57a/rpds_py-0.26.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b6d9e5a2ed9c4988c8f9b28b3bc0e3e5b1aaa10c28d210a594ff3a8c02742daf", size = 417039, upload-time = "2025-07-01T15:57:05.608Z" }, - { url = "https://files.pythonhosted.org/packages/02/ee/ed835925731c7e87306faa80a3a5e17b4d0f532083155e7e00fe1cd4e242/rpds_py-0.26.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:b5f7a446ddaf6ca0fad9a5535b56fbfc29998bf0e0b450d174bbec0d600e1d72", size = 559111, upload-time = "2025-07-01T15:57:07.371Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/d6e9e686b8ffb6139b82eb1c319ef32ae99aeb21f7e4bf45bba44a760d09/rpds_py-0.26.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:eed5ac260dd545fbc20da5f4f15e7efe36a55e0e7cf706e4ec005b491a9546a0", size = 588609, upload-time = "2025-07-01T15:57:09.319Z" }, - { url = "https://files.pythonhosted.org/packages/e5/96/09bcab08fa12a69672716b7f86c672ee7f79c5319f1890c5a79dcb8e0df2/rpds_py-0.26.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:582462833ba7cee52e968b0341b85e392ae53d44c0f9af6a5927c80e539a8b67", size = 555212, upload-time = "2025-07-01T15:57:10.905Z" }, - { url = "https://files.pythonhosted.org/packages/2c/07/c554b6ed0064b6e0350a622714298e930b3cf5a3d445a2e25c412268abcf/rpds_py-0.26.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:69a607203441e07e9a8a529cff1d5b73f6a160f22db1097211e6212a68567d11", size = 232048, upload-time = "2025-07-01T15:57:12.473Z" }, -] - -[[package]] -name = "scipy" -version = "1.13.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10' and platform_machine == 'arm64' and sys_platform == 'darwin'", - "python_full_version < '3.10' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.10' and platform_machine != 'arm64' and sys_platform == 'darwin') or (python_full_version < '3.10' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.10' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ae/00/48c2f661e2816ccf2ecd77982f6605b2950afe60f60a52b4cbbc2504aa8f/scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c", size = 57210720, upload-time = "2024-05-23T03:29:26.079Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/59/41b2529908c002ade869623b87eecff3e11e3ce62e996d0bdcb536984187/scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca", size = 39328076, upload-time = "2024-05-23T03:19:01.687Z" }, - { url = "https://files.pythonhosted.org/packages/d5/33/f1307601f492f764062ce7dd471a14750f3360e33cd0f8c614dae208492c/scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f", size = 30306232, upload-time = "2024-05-23T03:19:09.089Z" }, - { url = "https://files.pythonhosted.org/packages/c0/66/9cd4f501dd5ea03e4a4572ecd874936d0da296bd04d1c45ae1a4a75d9c3a/scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989", size = 33743202, upload-time = "2024-05-23T03:19:15.138Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ba/7255e5dc82a65adbe83771c72f384d99c43063648456796436c9a5585ec3/scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f", size = 38577335, upload-time = "2024-05-23T03:19:21.984Z" }, - { url = "https://files.pythonhosted.org/packages/49/a5/bb9ded8326e9f0cdfdc412eeda1054b914dfea952bda2097d174f8832cc0/scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94", size = 38820728, upload-time = "2024-05-23T03:19:28.225Z" }, - { url = "https://files.pythonhosted.org/packages/12/30/df7a8fcc08f9b4a83f5f27cfaaa7d43f9a2d2ad0b6562cced433e5b04e31/scipy-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54", size = 46210588, upload-time = "2024-05-23T03:19:35.661Z" }, - { url = "https://files.pythonhosted.org/packages/b4/15/4a4bb1b15bbd2cd2786c4f46e76b871b28799b67891f23f455323a0cdcfb/scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9", size = 39333805, upload-time = "2024-05-23T03:19:43.081Z" }, - { url = "https://files.pythonhosted.org/packages/ba/92/42476de1af309c27710004f5cdebc27bec62c204db42e05b23a302cb0c9a/scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326", size = 30317687, upload-time = "2024-05-23T03:19:48.799Z" }, - { url = "https://files.pythonhosted.org/packages/80/ba/8be64fe225360a4beb6840f3cbee494c107c0887f33350d0a47d55400b01/scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299", size = 33694638, upload-time = "2024-05-23T03:19:55.104Z" }, - { url = "https://files.pythonhosted.org/packages/36/07/035d22ff9795129c5a847c64cb43c1fa9188826b59344fee28a3ab02e283/scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa", size = 38569931, upload-time = "2024-05-23T03:20:01.82Z" }, - { url = "https://files.pythonhosted.org/packages/d9/10/f9b43de37e5ed91facc0cfff31d45ed0104f359e4f9a68416cbf4e790241/scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59", size = 38838145, upload-time = "2024-05-23T03:20:09.173Z" }, - { url = "https://files.pythonhosted.org/packages/4a/48/4513a1a5623a23e95f94abd675ed91cfb19989c58e9f6f7d03990f6caf3d/scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b", size = 46196227, upload-time = "2024-05-23T03:20:16.433Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7b/fb6b46fbee30fc7051913068758414f2721003a89dd9a707ad49174e3843/scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1", size = 39357301, upload-time = "2024-05-23T03:20:23.538Z" }, - { url = "https://files.pythonhosted.org/packages/dc/5a/2043a3bde1443d94014aaa41e0b50c39d046dda8360abd3b2a1d3f79907d/scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d", size = 30363348, upload-time = "2024-05-23T03:20:29.885Z" }, - { url = "https://files.pythonhosted.org/packages/e7/cb/26e4a47364bbfdb3b7fb3363be6d8a1c543bcd70a7753ab397350f5f189a/scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627", size = 33406062, upload-time = "2024-05-23T03:20:36.012Z" }, - { url = "https://files.pythonhosted.org/packages/88/ab/6ecdc526d509d33814835447bbbeedbebdec7cca46ef495a61b00a35b4bf/scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884", size = 38218311, upload-time = "2024-05-23T03:20:42.086Z" }, - { url = "https://files.pythonhosted.org/packages/0b/00/9f54554f0f8318100a71515122d8f4f503b1a2c4b4cfab3b4b68c0eb08fa/scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16", size = 38442493, upload-time = "2024-05-23T03:20:48.292Z" }, - { url = "https://files.pythonhosted.org/packages/3e/df/963384e90733e08eac978cd103c34df181d1fec424de383cdc443f418dd4/scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949", size = 45910955, upload-time = "2024-05-23T03:20:55.091Z" }, - { url = "https://files.pythonhosted.org/packages/7f/29/c2ea58c9731b9ecb30b6738113a95d147e83922986b34c685b8f6eefde21/scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5", size = 39352927, upload-time = "2024-05-23T03:21:01.95Z" }, - { url = "https://files.pythonhosted.org/packages/5c/c0/e71b94b20ccf9effb38d7147c0064c08c622309fd487b1b677771a97d18c/scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24", size = 30324538, upload-time = "2024-05-23T03:21:07.634Z" }, - { url = "https://files.pythonhosted.org/packages/6d/0f/aaa55b06d474817cea311e7b10aab2ea1fd5d43bc6a2861ccc9caec9f418/scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004", size = 33732190, upload-time = "2024-05-23T03:21:14.41Z" }, - { url = "https://files.pythonhosted.org/packages/35/f5/d0ad1a96f80962ba65e2ce1de6a1e59edecd1f0a7b55990ed208848012e0/scipy-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d", size = 38612244, upload-time = "2024-05-23T03:21:21.827Z" }, - { url = "https://files.pythonhosted.org/packages/8d/02/1165905f14962174e6569076bcc3315809ae1291ed14de6448cc151eedfd/scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c", size = 38845637, upload-time = "2024-05-23T03:21:28.729Z" }, - { url = "https://files.pythonhosted.org/packages/3e/77/dab54fe647a08ee4253963bcd8f9cf17509c8ca64d6335141422fe2e2114/scipy-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2", size = 46227440, upload-time = "2024-05-23T03:21:35.888Z" }, ] [[package]] @@ -3861,12 +3027,12 @@ name = "scipy" version = "1.15.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version == '3.10.*' and sys_platform == 'darwin'", - "python_full_version == '3.10.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.10.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.10.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { 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/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } wheels = [ @@ -3977,8 +3143,8 @@ name = "secretstorage" version = "3.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography", marker = "(python_full_version < '3.10' and platform_machine != 'arm64') or sys_platform != 'darwin'" }, - { name = "jeepney", marker = "(python_full_version < '3.10' and platform_machine != 'arm64') or sys_platform != 'darwin'" }, + { name = "cryptography", marker = "sys_platform != 'darwin'" }, + { name = "jeepney", marker = "sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739, upload-time = "2022-08-13T16:22:46.976Z" } wheels = [ @@ -4059,18 +3225,15 @@ version = "0.28.0" source = { editable = "." } dependencies = [ { name = "defusedxml" }, - { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "matplotlib", version = "3.10.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "matplotlib" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "opencv-python" }, { name = "pillow" }, { name = "pydeprecate" }, { name = "pyyaml" }, { name = "requests" }, - { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scipy", version = "1.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "tqdm" }, ] @@ -4092,8 +3255,7 @@ dev = [ { name = "jupytext" }, { name = "nbconvert" }, { name = "notebook" }, - { name = "pre-commit", version = "4.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pre-commit", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "tox" }, @@ -4143,7 +3305,7 @@ dev = [ ] docs = [ { name = "mike", specifier = ">=2" }, - { name = "mkdocs-git-committers-plugin-2", marker = "python_full_version >= '3.9' and python_full_version < '4'", specifier = ">=2.4.1" }, + { name = "mkdocs-git-committers-plugin-2", marker = "python_full_version >= '3.10' and python_full_version < '4'", specifier = ">=2.4.1" }, { name = "mkdocs-git-revision-date-localized-plugin", specifier = ">=1.2.4" }, { name = "mkdocs-jupyter", specifier = ">=0.24.3" }, { name = "mkdocs-material", extras = ["imaging"], specifier = ">=9.7" }, @@ -4157,7 +3319,7 @@ version = "0.18.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ptyprocess", marker = "os_name != 'nt'" }, - { name = "pywinpty", marker = "(python_full_version < '3.10' and os_name == 'nt' and platform_machine != 'arm64' and sys_platform == 'darwin') or (os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "pywinpty", marker = "(os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, { name = "tornado" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701, upload-time = "2024-03-12T14:34:39.026Z" } @@ -4241,8 +3403,7 @@ dependencies = [ { name = "cachetools" }, { name = "chardet" }, { name = "colorama" }, - { name = "filelock", version = "3.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "filelock", version = "3.20.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "filelock" }, { name = "packaging" }, { name = "platformdirs" }, { name = "pluggy" }, @@ -4283,7 +3444,6 @@ version = "6.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "id" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" }, { name = "packaging" }, { name = "readme-renderer" }, @@ -4358,8 +3518,7 @@ version = "20.36.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, - { name = "filelock", version = "3.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "filelock", version = "3.20.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "filelock" }, { name = "platformdirs" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] @@ -4386,13 +3545,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, - { url = "https://files.pythonhosted.org/packages/05/52/7223011bb760fce8ddc53416beb65b83a3ea6d7d13738dde75eeb2c89679/watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8", size = 96390, upload-time = "2024-11-01T14:06:49.325Z" }, - { url = "https://files.pythonhosted.org/packages/9c/62/d2b21bc4e706d3a9d467561f487c2938cbd881c69f3808c43ac1ec242391/watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a", size = 88386, upload-time = "2024-11-01T14:06:50.536Z" }, - { url = "https://files.pythonhosted.org/packages/ea/22/1c90b20eda9f4132e4603a26296108728a8bfe9584b006bd05dd94548853/watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c", size = 89017, upload-time = "2024-11-01T14:06:51.717Z" }, { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, - { url = "https://files.pythonhosted.org/packages/5b/79/69f2b0e8d3f2afd462029031baafb1b75d11bb62703f0e1022b2e54d49ee/watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa", size = 87903, upload-time = "2024-11-01T14:06:57.052Z" }, - { url = "https://files.pythonhosted.org/packages/e2/2b/dc048dd71c2e5f0f7ebc04dd7912981ec45793a03c0dc462438e0591ba5d/watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e", size = 88381, upload-time = "2024-11-01T14:06:58.193Z" }, { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" },