From 315a360b6f67e0f200e452021cfe663cd29ef01e Mon Sep 17 00:00:00 2001 From: asgersvenning Date: Fri, 4 Apr 2025 11:30:52 +0200 Subject: [PATCH 01/41] Remove some desynced test file pointers --- src/flat_bug/tests/assets/pyramid_tps_1.pt | 1 - src/flat_bug/tests/assets/pyramid_tps_2.pt | 1 - src/flat_bug/tests/assets/pyramid_tps_3.pt | 1 - src/flat_bug/tests/assets/pyramid_tps_4.pt | 1 - src/flat_bug/tests/assets/pyramid_tps_5.pt | 1 - src/flat_bug/tests/assets/single_scale_tps_1.pt | 1 - 6 files changed, 6 deletions(-) delete mode 100644 src/flat_bug/tests/assets/pyramid_tps_1.pt delete mode 100644 src/flat_bug/tests/assets/pyramid_tps_2.pt delete mode 100644 src/flat_bug/tests/assets/pyramid_tps_3.pt delete mode 100644 src/flat_bug/tests/assets/pyramid_tps_4.pt delete mode 100644 src/flat_bug/tests/assets/pyramid_tps_5.pt delete mode 100644 src/flat_bug/tests/assets/single_scale_tps_1.pt diff --git a/src/flat_bug/tests/assets/pyramid_tps_1.pt b/src/flat_bug/tests/assets/pyramid_tps_1.pt deleted file mode 100644 index 5943933..0000000 --- a/src/flat_bug/tests/assets/pyramid_tps_1.pt +++ /dev/null @@ -1 +0,0 @@ -ERDA Pointer \ No newline at end of file diff --git a/src/flat_bug/tests/assets/pyramid_tps_2.pt b/src/flat_bug/tests/assets/pyramid_tps_2.pt deleted file mode 100644 index 5943933..0000000 --- a/src/flat_bug/tests/assets/pyramid_tps_2.pt +++ /dev/null @@ -1 +0,0 @@ -ERDA Pointer \ No newline at end of file diff --git a/src/flat_bug/tests/assets/pyramid_tps_3.pt b/src/flat_bug/tests/assets/pyramid_tps_3.pt deleted file mode 100644 index 3241d37..0000000 --- a/src/flat_bug/tests/assets/pyramid_tps_3.pt +++ /dev/null @@ -1 +0,0 @@ -ERDA Pointer diff --git a/src/flat_bug/tests/assets/pyramid_tps_4.pt b/src/flat_bug/tests/assets/pyramid_tps_4.pt deleted file mode 100644 index 3241d37..0000000 --- a/src/flat_bug/tests/assets/pyramid_tps_4.pt +++ /dev/null @@ -1 +0,0 @@ -ERDA Pointer diff --git a/src/flat_bug/tests/assets/pyramid_tps_5.pt b/src/flat_bug/tests/assets/pyramid_tps_5.pt deleted file mode 100644 index 3241d37..0000000 --- a/src/flat_bug/tests/assets/pyramid_tps_5.pt +++ /dev/null @@ -1 +0,0 @@ -ERDA Pointer diff --git a/src/flat_bug/tests/assets/single_scale_tps_1.pt b/src/flat_bug/tests/assets/single_scale_tps_1.pt deleted file mode 100644 index 5943933..0000000 --- a/src/flat_bug/tests/assets/single_scale_tps_1.pt +++ /dev/null @@ -1 +0,0 @@ -ERDA Pointer \ No newline at end of file From b0d6974cc1631330d144f59b90a824de98037855 Mon Sep 17 00:00:00 2001 From: asgersvenning Date: Mon, 7 Apr 2025 10:25:22 +0200 Subject: [PATCH 02/41] Fixed error on saving with SVG due to invalid execution flow logic in TensorPredictions.plot --- src/flat_bug/predictor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/flat_bug/predictor.py b/src/flat_bug/predictor.py index 80a9d45..ef6f253 100644 --- a/src/flat_bug/predictor.py +++ b/src/flat_bug/predictor.py @@ -586,7 +586,8 @@ def plot( params.pop("self", None) if outpath not in [None, ""] and outpath.lower().endswith(".svg"): retval = self._plot_svg(**params) - retval = self._plot_image(**params) + else: + retval = self._plot_image(**params) if retval is None: return outpath return retval From 6f9aee66719ba5d800b86eeedb45cee99a15a861 Mon Sep 17 00:00:00 2001 From: asgersvenning Date: Mon, 7 Apr 2025 16:44:42 +0200 Subject: [PATCH 03/41] Fix --long_format inference --- src/bin/fb_predict.py | 2 +- src/flat_bug/predictor.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/bin/fb_predict.py b/src/bin/fb_predict.py index 8763d99..b0ea160 100644 --- a/src/bin/fb_predict.py +++ b/src/bin/fb_predict.py @@ -291,7 +291,7 @@ def predict( identifier = UUID, #str(uuid.uuid4()), ) if not result_directory is None: - json_files = [f for f in glob.glob(os.path.join(glob.escape(result_directory), "*.json"))] + json_files = [f for f in glob.glob(os.path.join(glob.escape(metadata if isinstance(metadata, str) else result_directory), f"*{os.path.splitext(os.path.basename(f))[0]}*.json"))] assert len(json_files) == 1 all_json_results.append(json_files[0]) if isVideo and overviews: diff --git a/src/flat_bug/predictor.py b/src/flat_bug/predictor.py index ef6f253..00706dd 100644 --- a/src/flat_bug/predictor.py +++ b/src/flat_bug/predictor.py @@ -1077,9 +1077,6 @@ def save( if prediction_directory_is_used: if not os.path.exists(prediction_directory): os.makedirs(prediction_directory) - else: - # If the prediction directory is not used set it to None - return None # Save overview if overview: @@ -1129,7 +1126,7 @@ def save( # Serialize the data to the metadata path self.serialize(outpath=metadata_path, identifier=identifier) - return prediction_directory + return prediction_directory if prediction_directory_is_used else None def _process_batch( image : torch.Tensor, From ab5361bd6f91a027c1167ea4623eca8d02f3b3cc Mon Sep 17 00:00:00 2001 From: asgersvenning Date: Fri, 6 Jun 2025 15:42:52 +0200 Subject: [PATCH 04/41] Stricter version requirement for ultralytics dependency due to a breaking change in 8.3.125 --- pyproject.toml | 2 +- src/bin/fb_predict.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7f7d88f..31c16d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ classifiers = [ dependencies = [ "torch>=2.2.0", "torchvision>=0.17.0", - "ultralytics>=8.2.16", + "ultralytics>=8.2.16,<=8.3.124", "shapely>=2.0.2", "scikit-optimize>=0.10.1" ] diff --git a/src/bin/fb_predict.py b/src/bin/fb_predict.py index b0ea160..873a62f 100644 --- a/src/bin/fb_predict.py +++ b/src/bin/fb_predict.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -""" +r""" Inference CLI script for ``flatbug``. A comprehensive CLI API for ``flatbug`` inference with support for hyperparameter configuration, flexible input parsing, output format specification, and hardware specification. From f59cec5fcd67d88eca21001e293118b758857221 Mon Sep 17 00:00:00 2001 From: asgersvenning Date: Fri, 6 Jun 2025 15:46:04 +0200 Subject: [PATCH 05/41] Change README install guide from git clone with SSH to HTTPS (re: #121) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fcddfb2..4256172 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ micromamba install flat-bug -c conda-forge #### Source/development Or a development version can be installed from source by cloning this repository: ```sh -git clone git@github.com:darsa-group/flat-bug.git +git clone https://github.com/darsa-group/flat-bug.git cd flat-bug pip install -e . ``` From 72f407aa78ab2886467368db2d95fa6864936658 Mon Sep 17 00:00:00 2001 From: asgersvenning Date: Wed, 9 Jul 2025 16:15:17 +0200 Subject: [PATCH 06/41] Remove tile and image tensors from intermediate `Results` objects to avoid memory-leak and OOM on large images --- src/flat_bug/yolo_helpers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/flat_bug/yolo_helpers.py b/src/flat_bug/yolo_helpers.py index 5ddaba8..f76203d 100644 --- a/src/flat_bug/yolo_helpers.py +++ b/src/flat_bug/yolo_helpers.py @@ -1,3 +1,4 @@ +from argparse import Namespace from typing import List, Optional, Tuple, Union import numpy as np @@ -132,7 +133,7 @@ def merge_tile_results( raise NotImplementedError("'Probs' not implemented yet") if not all([r.keypoints is None for r in results]): raise NotImplementedError("'Keypoints' not implemented yet") - return ResultsWithTiles(tiles=tile_indices, orig_img=orig_img, path=path, names=names, boxes=boxes, masks=masks, polygons=polygons, probs=None, keypoints=None) + return ResultsWithTiles(tiles=tile_indices, orig_img=Namespace(shape=orig_img.shape), path=path, names=names, boxes=boxes, masks=masks, polygons=polygons, probs=None, keypoints=None) def stack_masks( masks : List["Masks"], @@ -417,5 +418,5 @@ def postprocess( boxes = boxes[~too_small] masks = masks[~too_small] pred[:, :4] = boxes - results.append({"orig_img" : imgs[i].clone().permute(1,2,0), "path" : "", "names" : ["insect"], "boxes" : pred[:, :6], "masks" : masks}) + results.append({"orig_img" : Namespace(shape=imgs[i].permute(1,2,0).shape), "path" : "", "names" : ["insect"], "boxes" : pred[:, :6], "masks" : masks}) return results \ No newline at end of file From 8e23f7f53e8fc3bc3af50fa8aadc611ca5bdd587 Mon Sep 17 00:00:00 2001 From: Quentin Geissmann Date: Wed, 8 Oct 2025 19:07:18 +0200 Subject: [PATCH 07/41] work on #134 --- prototypes/mask_refiner/fb_refine.py | 225 +++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 prototypes/mask_refiner/fb_refine.py diff --git a/prototypes/mask_refiner/fb_refine.py b/prototypes/mask_refiner/fb_refine.py new file mode 100644 index 0000000..f3d6dfd --- /dev/null +++ b/prototypes/mask_refiner/fb_refine.py @@ -0,0 +1,225 @@ +import json +import argparse +import os +import cv2 as cv +import numpy as np + + +# fixme, resume should continue on the same "run folder" +def main(): + args_parse = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) + args_parse.add_argument("-d", "--data-dir", dest="data_dir", + help="The directory containing the prepared data (i.e., the output of `fb_prepare.py`", + type=str) + + args_parse.add_argument("-c", "--config-file", dest="config_file", + help="A YAML-formatted config file that overrides the default training meta-parameters", + default=None) + args_parse.add_argument("-r", "--resume", dest="resume", + help="resume training", + action='store_true') + + args, extra = args_parse.parse_known_args() + +# def best_new_contour(cnt): + +def best_yolo_contour(pts, roi_padded, conf_thresh=0.25): + """ + Choose the best YOLOv8 polygon (on roi_padded) by IoU against `pts` (N,2). + Uses polygon IoU via shapely if available; otherwise falls back to mask IoU. + Returns an (M,2) int32 array of the best polygon in ROI coordinates. + """ + + # --- get global YOLOv8 model --- + + H, W = roi_padded.shape[:2] + pts = np.asarray(pts, dtype=np.float32) + if pts.ndim != 2 or pts.shape[1] != 2 or len(pts) < 3: + return pts.astype(np.int32) + + # --- try shapely for polygon IoU --- + try: + from shapely.geometry import Polygon + from shapely.errors import TopologicalError + use_shapely = True + except Exception: + use_shapely = False + + # helper: polygon IoU with shapely + def _poly_iou_shapely(a_xy, b_xy): + try: + pa = Polygon(a_xy).buffer(0) # buffer(0) fixes minor self-intersections + pb = Polygon(b_xy).buffer(0) + if not pa.is_valid or not pb.is_valid: + return 0.0 + inter = pa.intersection(pb).area + union = pa.union(pb).area + return float(inter / union) if union > 0 else 0.0 + except TopologicalError: + return 0.0 + + # fallback: mask IoU if shapely not available + def _poly_iou_mask(a_xy, b_xy): + am = np.zeros((H, W), dtype=np.uint8) + bm = np.zeros((H, W), dtype=np.uint8) + a_int = np.round(a_xy).astype(np.int32) + b_int = np.round(b_xy).astype(np.int32) + a_int[:, 0] = np.clip(a_int[:, 0], 0, W - 1) + a_int[:, 1] = np.clip(a_int[:, 1], 0, H - 1) + b_int[:, 0] = np.clip(b_int[:, 0], 0, W - 1) + b_int[:, 1] = np.clip(b_int[:, 1], 0, H - 1) + cv.fillPoly(am, [a_int], 1) + cv.fillPoly(bm, [b_int], 1) + inter = np.logical_and(am, bm).sum() + union = np.logical_or(am, bm).sum() + return float(inter / union) if union > 0 else 0.0 + + # --- run YOLOv8 --- + try: + res = yolo(roi_padded, verbose=False)[0] + except Exception: + return pts.astype(np.int32) + + # no masks predicted + if not hasattr(res, "masks") or res.masks is None or getattr(res.masks, "xy", None) is None: + return pts.astype(np.int32) + + # confidences (aligned with masks) + try: + confs = res.boxes.conf.detach().cpu().numpy() + except Exception: + confs = np.ones(len(res.masks.xy), dtype=np.float32) + + # choose IoU function + iou_fn = _poly_iou_shapely if use_shapely else _poly_iou_mask + + best_iou = -1.0 + best_poly = None + + # ground-truth polygon (float) + gt = pts + + for i, poly in enumerate(res.masks.xy): + if i < len(confs) and confs[i] < conf_thresh: + continue + if poly is None or len(poly) < 3: + continue + + poly = np.asarray(poly, dtype=np.float32) + + # clip to image bounds to be safe (also helps mask fallback) + poly[:, 0] = np.clip(poly[:, 0], 0, W - 1) + poly[:, 1] = np.clip(poly[:, 1], 0, H - 1) + + iou = iou_fn(gt, poly) + if iou > best_iou: + best_iou = iou + best_poly = poly + + if best_poly is None or best_iou <= 0.0: + return pts.astype(np.int32) + + return np.round(best_poly).astype(np.int32) + + +def refine_instance(im, box, cnt, cls): + h, w = im.shape[:2] + x1, y1, x2, y2 = box + + # --- 1️⃣ Expand box by 10% --- + box_w = x2 - x1 + box_h = y2 - y1 + expand_x = int(0.1 * box_w) + expand_y = int(0.1 * box_h) + + x1_exp, y1_exp = x1 - expand_x, y1 - expand_y + x2_exp, y2_exp = x2 + expand_x, y2 + expand_y + + # --- 2️⃣ Clip to image bounds --- + x1_clip, y1_clip = max(x1_exp, 0), max(y1_exp, 0) + x2_clip, y2_clip = min(x2_exp, w), min(y2_exp, h) + + # --- 3️⃣ Extract valid region --- + roi = im[y1_clip:y2_clip, x1_clip:x2_clip] + + # White background of expanded size + roi_h, roi_w = y2_exp - y1_exp, x2_exp - x1_exp + white_bg = np.ones((roi_h, roi_w, 3), dtype=np.uint8) * 255 + + y_offset = y1_clip - y1_exp + x_offset = x1_clip - x1_exp + white_bg[y_offset:y_offset + roi.shape[0], x_offset:x_offset + roi.shape[1]] = roi + + # --- 4️⃣ Contour alignment (relative to expanded ROI) --- + xs, ys = cnt[0], cnt[1] + # assert xs.shape == ys.shape, "xs and ys must have same length" + cnt_np = np.stack([xs, ys], axis=1).astype(np.float32) + + cnt_np[:, 0] -= x1_exp + cnt_np[:, 1] -= y1_exp + + # --- 5️⃣ Scale so that longest side = 1024 --- + long_side = max(roi_w, roi_h) + scale = 1024 / long_side + new_w = int(roi_w * scale) + new_h = int(roi_h * scale) + + roi_scaled = cv.resize(white_bg, (new_w, new_h), interpolation=cv.INTER_LINEAR) + cnt_scaled = cnt_np * scale + + # --- 6️⃣ Pad to 1024×1024 with white margins --- + pad_x = (1024 - new_w) // 2 + pad_y = (1024 - new_h) // 2 + + roi_padded = np.ones((1024, 1024, 3), dtype=np.uint8) * 255 + roi_padded[pad_y:pad_y + new_h, pad_x:pad_x + new_w] = roi_scaled + + # Adjust contour for padding (relative to final ROI) + cnt_final = cnt_scaled + np.array([pad_x, pad_y], dtype=np.float32) + + # --- 7️⃣ Keep mapping info to reconstruct original coordinates --- + transform_info = { + "x1_exp": x1_exp, + "y1_exp": y1_exp, + "scale": scale, + "pad_x": pad_x, + "pad_y": pad_y + } + + + pts = np.array(cnt_final, dtype=np.int32).reshape(-1, 2) + + refined_candidate = best_yolo_contour(pts, roi_padded) + + cv.polylines(roi_padded, [pts], isClosed=True, color=(0, 0, 255), thickness=2) + cv.polylines(roi_padded, [refined_candidate], isClosed=True, color=(255, 0, 0), thickness=2) + cv.imshow("test", roi_padded) + cv.waitKey(-1) + + + +def refine_file(path): + with open(path, 'r') as f: + data = json.load(f) + + dir_name = os.path.dirname(path) + parent_image_rel_path = data["image_path"] + # parent_image_abs_path = os.path.join(dir_name, parent_image_rel_path) + parent_image_abs_path = os.path.join( parent_image_rel_path) + assert os.path.isfile(parent_image_abs_path), parent_image_abs_path + + im = cv.imread(parent_image_abs_path) + + + for box, cnt, cls in zip(data["boxes"], data["contours"], data["classes"]): + refine_instance(im, box, cnt, cls) + + +if __name__ == "__main__": + # main() + from ultralytics import YOLO + from ultralytics.engine.results import Results + model_file = "flat_bug_S.pt" + result_file = "data/metadata_mask-refiner-test_UUID_ChangeThisTEMPORARY.json" + yolo = YOLO(model_file, "segment", verbose=True) + refine_file(result_file) From 22acf67a859faa856f5e72ef89479ab4639beb09 Mon Sep 17 00:00:00 2001 From: Quentin Geissmann Date: Sun, 12 Oct 2025 13:09:18 +0200 Subject: [PATCH 08/41] improved --- prototypes/mask_refiner/fb_refine.py | 176 +++++++++++++-------------- 1 file changed, 85 insertions(+), 91 deletions(-) diff --git a/prototypes/mask_refiner/fb_refine.py b/prototypes/mask_refiner/fb_refine.py index f3d6dfd..53e66c5 100644 --- a/prototypes/mask_refiner/fb_refine.py +++ b/prototypes/mask_refiner/fb_refine.py @@ -1,8 +1,11 @@ import json import argparse import os + +import cv2 import cv2 as cv import numpy as np +from torchgen.gen_functionalization_type import return_from_mutable_noop_redispatch # fixme, resume should continue on the same "run folder" @@ -23,103 +26,94 @@ def main(): # def best_new_contour(cnt): -def best_yolo_contour(pts, roi_padded, conf_thresh=0.25): - """ - Choose the best YOLOv8 polygon (on roi_padded) by IoU against `pts` (N,2). - Uses polygon IoU via shapely if available; otherwise falls back to mask IoU. - Returns an (M,2) int32 array of the best polygon in ROI coordinates. - """ - - # --- get global YOLOv8 model --- - - H, W = roi_padded.shape[:2] - pts = np.asarray(pts, dtype=np.float32) - if pts.ndim != 2 or pts.shape[1] != 2 or len(pts) < 3: - return pts.astype(np.int32) - - # --- try shapely for polygon IoU --- - try: - from shapely.geometry import Polygon - from shapely.errors import TopologicalError - use_shapely = True - except Exception: - use_shapely = False - - # helper: polygon IoU with shapely - def _poly_iou_shapely(a_xy, b_xy): - try: - pa = Polygon(a_xy).buffer(0) # buffer(0) fixes minor self-intersections - pb = Polygon(b_xy).buffer(0) - if not pa.is_valid or not pb.is_valid: - return 0.0 - inter = pa.intersection(pb).area - union = pa.union(pb).area - return float(inter / union) if union > 0 else 0.0 - except TopologicalError: - return 0.0 - - # fallback: mask IoU if shapely not available - def _poly_iou_mask(a_xy, b_xy): - am = np.zeros((H, W), dtype=np.uint8) - bm = np.zeros((H, W), dtype=np.uint8) - a_int = np.round(a_xy).astype(np.int32) - b_int = np.round(b_xy).astype(np.int32) - a_int[:, 0] = np.clip(a_int[:, 0], 0, W - 1) - a_int[:, 1] = np.clip(a_int[:, 1], 0, H - 1) - b_int[:, 0] = np.clip(b_int[:, 0], 0, W - 1) - b_int[:, 1] = np.clip(b_int[:, 1], 0, H - 1) - cv.fillPoly(am, [a_int], 1) - cv.fillPoly(bm, [b_int], 1) - inter = np.logical_and(am, bm).sum() - union = np.logical_or(am, bm).sum() - return float(inter / union) if union > 0 else 0.0 - - # --- run YOLOv8 --- - try: - res = yolo(roi_padded, verbose=False)[0] - except Exception: - return pts.astype(np.int32) - - # no masks predicted - if not hasattr(res, "masks") or res.masks is None or getattr(res.masks, "xy", None) is None: - return pts.astype(np.int32) - - # confidences (aligned with masks) - try: - confs = res.boxes.conf.detach().cpu().numpy() - except Exception: - confs = np.ones(len(res.masks.xy), dtype=np.float32) - # choose IoU function - iou_fn = _poly_iou_shapely if use_shapely else _poly_iou_mask - best_iou = -1.0 - best_poly = None +def _iou_from_masks(a, b): + inter = np.logical_and(a, b).sum() + union = np.logical_or(a, b).sum() + return float(inter) / float(union) if union > 0 else 0.0 - # ground-truth polygon (float) - gt = pts +def yolo_ensemble_contour(image_bgr, cnt, conf_threshold=0.25, iou_threshold=0.1, mask_threshold=1): - for i, poly in enumerate(res.masks.xy): - if i < len(confs) and confs[i] < conf_thresh: - continue - if poly is None or len(poly) < 3: + + # get global YOLOv8 model + + H, W = image_bgr.shape[:2] + accum = np.zeros((H, W), dtype=np.float32) + original_mask = np.zeros_like(accum).astype(np.uint8) + cv.fillPoly(original_mask, [cnt.astype(np.int32)], 1) + n=4 + for i in range(n): + print(i) + if i ==0: + image_in = np.copy(image_bgr) + elif i == 1: + image_in = cv.rotate(image_bgr, cv.ROTATE_90_CLOCKWISE) + elif i == 2: + image_in = cv.flip(image_bgr, 0) + elif i == 3: + image_in = cv.flip(cv.rotate(image_bgr, cv.ROTATE_90_CLOCKWISE), 0) + else: + raise ValueError("aug_idx must be in {0,1,2,3}") + + # image_in =cv.medianBlur(image_bgr,i*2+1) + res = yolo(image_in, verbose=False)[0] + + # no masks predicted this run + if not hasattr(res, "masks") or res.masks is None or getattr(res.masks, "data", None) is None: continue - poly = np.asarray(poly, dtype=np.float32) + confs = res.boxes.conf.detach().cpu().numpy() + masks_t = res.masks.data # torch.Tensor [num, h, w] + num_masks = masks_t.shape[0] + + valid_masks = [] + mask_areas = [] - # clip to image bounds to be safe (also helps mask fallback) - poly[:, 0] = np.clip(poly[:, 0], 0, W - 1) - poly[:, 1] = np.clip(poly[:, 1], 0, H - 1) + for j in range(num_masks): + if confs is not None and j < len(confs) and confs[j] < conf_threshold: + continue - iou = iou_fn(gt, poly) - if iou > best_iou: - best_iou = iou - best_poly = poly + m = masks_t[j].detach().cpu().numpy().astype(np.float32) # float mask (h, w) in [0,1] - if best_poly is None or best_iou <= 0.0: - return pts.astype(np.int32) + iou = _iou_from_masks(m, original_mask) + print(j, iou) + if iou < iou_threshold : + continue + valid_masks.append(m) + mask_areas.append(np.sum(m)) + + if len(mask_areas) == 0: + continue - return np.round(best_poly).astype(np.int32) + m = valid_masks[np.argmax(mask_areas)] + + if i == 0: + m = m + elif i == 1: + m = cv.rotate(m, cv.ROTATE_90_COUNTERCLOCKWISE) + elif i == 2: + m = cv.flip(m, 0) + elif i == 3: + m = cv.rotate(cv.flip(m,0), cv.ROTATE_90_COUNTERCLOCKWISE) + else: + raise ValueError("aug_idx must be in {0,1,2,3}") + print (i,m.shape) + accum += m + + frac = accum / float(n) + cv2.imshow("test2", frac) + final_mask = (frac >= mask_threshold).astype(np.uint8) * 255 + if np.count_nonzero(final_mask) == 0: + return cnt + # find largest contour + cnts, _ = cv.findContours(final_mask, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE) + if not cnts: + return None + + best = max(cnts, key=cv.contourArea) # largest by area + best = cv.approxPolyDP(best, 0.0005 * cv.arcLength(best, True), True) + return best def refine_instance(im, box, cnt, cls): @@ -129,8 +123,8 @@ def refine_instance(im, box, cnt, cls): # --- 1️⃣ Expand box by 10% --- box_w = x2 - x1 box_h = y2 - y1 - expand_x = int(0.1 * box_w) - expand_y = int(0.1 * box_h) + expand_x = int(0.2 * box_w) + expand_y = int(0.2 * box_h) x1_exp, y1_exp = x1 - expand_x, y1 - expand_y x2_exp, y2_exp = x2 + expand_x, y2 + expand_y @@ -189,7 +183,7 @@ def refine_instance(im, box, cnt, cls): pts = np.array(cnt_final, dtype=np.int32).reshape(-1, 2) - refined_candidate = best_yolo_contour(pts, roi_padded) + refined_candidate = yolo_ensemble_contour( roi_padded, pts) cv.polylines(roi_padded, [pts], isClosed=True, color=(0, 0, 255), thickness=2) cv.polylines(roi_padded, [refined_candidate], isClosed=True, color=(255, 0, 0), thickness=2) @@ -220,6 +214,6 @@ def refine_file(path): from ultralytics import YOLO from ultralytics.engine.results import Results model_file = "flat_bug_S.pt" - result_file = "data/metadata_mask-refiner-test_UUID_ChangeThisTEMPORARY.json" + result_file = "data/metadata_mask-refiner-test3_UUID_ChangeThisTEMPORARY.json" yolo = YOLO(model_file, "segment", verbose=True) refine_file(result_file) From f6caa92e63e5bdfb0194477cb38a26f8b38e8708 Mon Sep 17 00:00:00 2001 From: asgersvenning Date: Mon, 27 Apr 2026 21:02:11 +0200 Subject: [PATCH 09/41] Refactor formatting and type annotation to pass pylance and ruff --- .github/workflows/run-tests.yml | 2 +- docs/flat-bug.ipynb | 56 +- docs/source/conf.py | 2 +- pyproject.toml | 37 +- scripts/experiments/best_train/orchestrate.py | 12 +- .../compare_backbone_sizes/orchestrate.py | 13 +- scripts/experiments/compare_models.py | 96 +- scripts/experiments/experiment_helpers.py | 118 +- .../orchestrate.py | 16 +- .../orchestrate.py | 23 +- scripts/experiments/test_experiment_runner.py | 7 +- ..._cv_and_finetuning_orchestrate_modified.py | 16 +- .../manuscript/figure_tiles/make_figure.py | 9 +- .../visualize_pyramid.py | 6 +- .../statistics/helpers/flatbug_helpers.R | 2 +- scripts/manuscript/time_flatbug.py | 9 +- src/bin/fb_clone_data.py | 76 +- src/bin/fb_eval.py | 75 +- src/bin/fb_predict.py | 171 +- src/bin/fb_prepare_data.py | 78 +- src/bin/fb_train.py | 31 +- src/bin/fb_tune.py | 41 +- src/flat_bug/__init__.py | 16 +- src/flat_bug/augmentations.py | 210 +- src/flat_bug/coco_utils.py | 114 +- src/flat_bug/config.py | 158 +- src/flat_bug/datasets.py | 190 +- src/flat_bug/eval_utils.py | 409 +-- src/flat_bug/geometric.py | 193 +- src/flat_bug/nms.py | 452 ++- src/flat_bug/predictor.py | 860 +++-- src/flat_bug/tests/__init__.py | 1 + src/flat_bug/tests/generate_model_outputs.py | 13 +- src/flat_bug/tests/remote_lfs_fallback.py | 12 +- src/flat_bug/tests/restore_assets.py | 7 +- src/flat_bug/tests/test_augmentations.py | 78 +- src/flat_bug/tests/test_config.py | 65 +- src/flat_bug/tests/test_dataset.py | 34 +- src/flat_bug/tests/test_predictor.py | 114 +- src/flat_bug/trainers.py | 191 +- src/flat_bug/yolo_helpers.py | 396 ++- utils/pre-annotate.py | 5 +- uv.lock | 3031 +++++++++++++++++ 43 files changed, 5754 insertions(+), 1691 deletions(-) create mode 100644 uv.lock diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index d6a3a52..f127124 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -24,7 +24,7 @@ jobs: strategy: matrix: - python-version: ["3.11", "3.12"] + python-version: ["3.11", "3.12", "3.13", "3.14"] steps: - run: echo "The job was automatically triggered by a ${{ github.event_name }} event." diff --git a/docs/flat-bug.ipynb b/docs/flat-bug.ipynb index 9211afa..11d3b07 100644 --- a/docs/flat-bug.ipynb +++ b/docs/flat-bug.ipynb @@ -44,9 +44,9 @@ "\n", "import re\n", "\n", + "\n", "def find_and_replace_in_file(file_path, search_pattern, replacement_text):\n", - " \"\"\"\n", - " Find and replace text in a Python (.py) file using regex.\n", + " \"\"\"Find and replace text in a Python (.py) file using regex.\n", "\n", " :param file_path: Path to the .py file\n", " :param search_pattern: Regex pattern to search for\n", @@ -54,7 +54,7 @@ " \"\"\"\n", " try:\n", " # Read the file content\n", - " with open(file_path, 'r', encoding='utf-8') as file:\n", + " with open(file_path, encoding='utf-8') as file:\n", " content = file.read()\n", "\n", " # Replace using regex\n", @@ -90,22 +90,25 @@ "\n", "# fix the package path to sys.path\n", "import sys\n", + "\n", "sys.path.append(\"/content/flat-bug/src\")\n", "\n", "# Localization implementation\n", "\n", - "import os, glob, json, io, zipfile, base64, uuid, re, tempfile\n", - "\n", - "from urllib.request import urlretrieve\n", - "from copy import deepcopy\n", - "from typing import List, Tuple, Union, Optional\n", - "from tqdm import tqdm\n", + "import base64\n", + "import glob\n", + "import io\n", + "import json\n", + "import os\n", + "import tempfile\n", + "import uuid\n", + "import zipfile\n", "\n", "import numpy as np\n", - "import torch\n", - "\n", "import rawpy\n", + "import torch\n", "from PIL import Image\n", + "from tqdm import tqdm\n", "\n", "from flat_bug.predictor import Predictor, TensorPredictions\n", "\n", @@ -136,7 +139,7 @@ " else:\n", " return 'unknown'\n", "\n", - "def get_images(input_path_dir_globs : Union[str, List[str]]) -> List[str]:\n", + "def get_images(input_path_dir_globs : str | list[str]) -> list[str]:\n", " if isinstance(input_path_dir_globs, str):\n", " input_path_dir_globs = [input_path_dir_globs]\n", " images = []\n", @@ -145,7 +148,7 @@ " case 'image':\n", " images.append(path)\n", " case 'txt':\n", - " with open(path, 'r') as f:\n", + " with open(path) as f:\n", " paths = [path.strip() for path in f.readlines() if len(path.strip()) > 0]\n", " images.extend(get_images(paths))\n", " case 'dir':\n", @@ -181,7 +184,7 @@ " def __repr__(self) -> str:\n", " return f\"Base64Image({self.path})\"\n", "\n", - "def parse_image(images : Optional[Union[np.ndarray, bytes, str, Union[List[Union[np.ndarray, bytes, str]], Tuple[Union[np.ndarray, bytes, str]]]]], device : Union[torch.device, str]=\"cpu\"):\n", + "def parse_image(images : np.ndarray | bytes | str | list[np.ndarray | bytes | str] | tuple[np.ndarray | bytes | str] | None, device : torch.device | str=\"cpu\"):\n", " # Cases:\n", " # List: Recursively parse each image\n", " if isinstance(images, (list, tuple)):\n", @@ -214,7 +217,7 @@ "def generate_uuid() -> str:\n", " return str(uuid.uuid4())[::3]\n", "\n", - "def save_file(content : str, name : str, dir : str, ext : str, identifier : Optional[str]=None, dtype : str=\"text\") -> str:\n", + "def save_file(content : str, name : str, dir : str, ext : str, identifier : str | None=None, dtype : str=\"text\") -> str:\n", " # Is the data raw bytes or text?\n", " if \"text\" in dtype:\n", " dtype = \"\"\n", @@ -231,7 +234,7 @@ " # Return the path\n", " return path\n", "\n", - "def zip_files(files : List[str], name : str, dir : str, identifier : Optional[str]=None) -> str:\n", + "def zip_files(files : list[str], name : str, dir : str, identifier : str | None=None) -> str:\n", " # If the UUID is not specified, generate a new one\n", " if identifier is None:\n", " identifier = generate_uuid()\n", @@ -250,7 +253,7 @@ "\n", "# Model definition\n", "class Localizer(Predictor):\n", - " def predict(self, images : Optional[Union[np.ndarray, bytes, str, Union[List[Union[np.ndarray, bytes, str]], Tuple[Union[np.ndarray, bytes, str]]]]], do_plot : bool | List[bool]=False, include_crops : bool=False, outdir : str=\"output\") -> dict:\n", + " def predict(self, images : np.ndarray | bytes | str | list[np.ndarray | bytes | str] | tuple[np.ndarray | bytes | str] | None, do_plot : bool | list[bool]=False, include_crops : bool=False, outdir : str=\"output\") -> dict:\n", " # Initialize the data\n", " data = {\n", " \"uuids\": [],\n", @@ -335,7 +338,7 @@ " if \"dtype\" in args and args[\"dtype\"] is not None:\n", " dtype = getattr(torch, args[\"dtype\"])\n", "\n", - " if not \"output\" in args or args[\"output\"] is None:\n", + " if \"output\" not in args or args[\"output\"] is None:\n", " outdir = tempfile.mkdtemp()\n", " else:\n", " outdir = args[\"output\"]\n", @@ -360,12 +363,10 @@ "# Gradio app.\n", "\n", "\n", - "import os, json\n", - "\n", - "from typing import List, Tuple, Optional, Union\n", + "import json\n", "\n", - "import numpy as np\n", "import gradio as gr\n", + "import numpy as np\n", "\n", "# from localize import Localizer, ImageEncoder, save_file, zip_files, get_defaults, Base64Image\n", "\n", @@ -380,14 +381,14 @@ " return out\n", "\n", "# Define the postprocessing function\n", - "def postprocess(out : dict) -> List[Tuple[str, str]]:\n", - " \"\"\"\n", - " Postprocess the output of the model.\n", + "def postprocess(out : dict) -> list[tuple[str, str]]:\n", + " \"\"\"Postprocess the output of the model.\n", "\n", " Arguments:\n", " out: The output of the model.\n", "\n", " Returns: A list of tuples with the JSON string and paths to saved images.\n", + "\n", " \"\"\"\n", " n = len(out[\"predictions\"])\n", " json_files = [save_file(json.dumps([out[k][i] for k in out], cls=ImageEncoder), name=\"instances\", dir=\"output\", ext=\"json\", identifier=out[\"uuids\"][i], dtype=\"text\") for i in range(n)]\n", @@ -409,9 +410,9 @@ " model = gr.State(get_model)\n", "\n", " # Define the localization function\n", - " def localize(images : Optional[Union[np.ndarray, bytes, str, Union[List[Union[np.ndarray, bytes, str]], Tuple[Union[np.ndarray, bytes, str]]]]],\n", + " def localize(images : np.ndarray | bytes | str | list[np.ndarray | bytes | str] | tuple[np.ndarray | bytes | str] | None,\n", " # do_plot : bool=False) -> Tuple[List[str], List[str]]:\n", - " ) -> Tuple[List[str], List[str]]:\n", + " ) -> tuple[list[str], list[str]]:\n", " if not os.path.exists(\"output\"):\n", " os.makedirs(\"output\")\n", " # predictions = model.value().predict(images, do_plot=do_plot, include_crops=True, outdir=\"output\")\n", @@ -439,6 +440,7 @@ " )\n", "\n", "from IPython.display import clear_output\n", + "\n", "clear_output(wait=True)\n", "demo.launch(inbrowser=True)\n" ] diff --git a/docs/source/conf.py b/docs/source/conf.py index 2b78a6c..2ced5ee 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -50,7 +50,7 @@ def copy_and_adjust_readme(): os.makedirs(dest_dir, exist_ok=True) - with open(readme_path, "r") as file: + with open(readme_path) as file: content = file.read() # Find and copy images while adjusting paths in README content diff --git a/pyproject.toml b/pyproject.toml index 6db50ae..346136a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,3 @@ -[build-system] -requires = ["setuptools>=68.2.2"] -build-backend = "setuptools.build_meta" - [project] name = "flat-bug" version = "1.0.0" @@ -14,6 +10,10 @@ readme = "README.md" requires-python = ">=3.11" classifiers = [ "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Operating System :: OS Independent", ] dependencies = [ @@ -35,10 +35,9 @@ cloud_datasets = [ "cvat_sdk>=2.47" ] jupyter = [ - "jupyter>=1.0.0" + "jupyter>=1.0.0", ] - [project.urls] "Homepage" = "https://github.com/darsa-group/flat-bug" "Bug Tracker" = "https://github.com/darsa-group/flat-bug/issues" @@ -50,3 +49,29 @@ fb_evaluate = "bin.fb_eval:main" fb_tune = "bin.fb_tune:main" fb_prepare_data = "bin.fb_prepare_data:main" fb_clone_data = "bin.fb_clone_data:main" + +[dependency-groups] +dev = [ + "flat-bug[notebook,erda,cloud_datasets]", +] + +[tool.ruff] +target-version = "py311" +line-length = 140 +extend-exclude = [ + "utils", + "scripts", + "src/bin/fb_tune.py", + "examples", + "docs" +] + + +[tool.ruff.lint] +# E: pycodestyle, F: Pyflakes, UP: pyupgrade, I: isort (sorts imports cleanly) +select = ["E", "F", "UP", "I", "D"] +ignore = ["D105"] + +[build-system] +requires = ["uv_build>=0.10.7,<0.11.0"] +build-backend = "uv_build" \ No newline at end of file diff --git a/scripts/experiments/best_train/orchestrate.py b/scripts/experiments/best_train/orchestrate.py index 648def2..b2852ef 100644 --- a/scripts/experiments/best_train/orchestrate.py +++ b/scripts/experiments/best_train/orchestrate.py @@ -1,6 +1,14 @@ -import os, sys +import os +import sys + sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "..")) -from scripts.experiments.experiment_helpers import set_default_config, get_config, get_cmd_args, read_slurm_params, ExperimentRunner +from scripts.experiments.experiment_helpers import ( + ExperimentRunner, + get_cmd_args, + get_config, + read_slurm_params, + set_default_config, +) BASE_NAME = "best_train" BASE_PATH = os.path.dirname(__file__) diff --git a/scripts/experiments/compare_backbone_sizes/orchestrate.py b/scripts/experiments/compare_backbone_sizes/orchestrate.py index 204f5e5..a942107 100644 --- a/scripts/experiments/compare_backbone_sizes/orchestrate.py +++ b/scripts/experiments/compare_backbone_sizes/orchestrate.py @@ -1,7 +1,14 @@ -import os, sys +import os +import sys + sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "..")) -from scripts.experiments.experiment_helpers import set_default_config, get_config, get_cmd_args, read_slurm_params, ExperimentRunner -from typing import List +from scripts.experiments.experiment_helpers import ( + ExperimentRunner, + get_cmd_args, + get_config, + read_slurm_params, + set_default_config, +) BASE_NAME = "fb_compare_backbone_sizes" BASE_PATH = os.path.dirname(__file__) diff --git a/scripts/experiments/compare_models.py b/scripts/experiments/compare_models.py index 7620024..b4712fe 100644 --- a/scripts/experiments/compare_models.py +++ b/scripts/experiments/compare_models.py @@ -1,51 +1,63 @@ -import os, sys -import glob, argparse, re +import argparse +import csv +import glob +import os +import re import subprocess -import csv, yaml +import sys +from typing import Any -from typing import Union, Optional, List, Tuple, Dict, Any +import yaml sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..")) -from scripts.experiments.experiment_helpers import run_command, remove_directory, split_by_sample, parse_unknown_arguments, read_slurm_params, ExperimentRunner, ZipOrDirectory from flat_bug.datasets import get_datasets from flat_bug.eval_utils import pretty_print_csv +from scripts.experiments.experiment_helpers import ( + ExperimentRunner, + ZipOrDirectory, + parse_unknown_arguments, + read_slurm_params, + remove_directory, + run_command, + split_by_sample, +) RESULT_DIR = os.path.join(os.path.dirname(__file__), "results") def eval_model( weights : str, - config : Optional[str], + config : str | None, directory : str, output_directory : str, local_directory : str=None, - tmp_directory : Optional[str]=None, - device : Optional[str]=None, - pattern : Optional[str]=None, + tmp_directory : str | None=None, + device : str | None=None, + pattern : str | None=None, store_all : bool=False, dry_run : bool = False, execute : bool=True, strict : bool=True ) -> str: - """ - Evaluate a model on a dataset. + """Evaluate a model on a dataset. Args: - weights (str): The path to the weights file. - config (str): The path to the config file. Should be a YAML file either ending in '.yaml' or '.yml'. - directory (str): The directory where the data is located and where the results will be saved the directory should have a 'reference' directory with the ground truth json in 'instances_default.json' and the matching images'. - output_directory (str): The path to the output directory where the results will be saved. If not supplied, it is assumed to be the same as the directory. Defaults to None. - local_directory (str, optional): The path to the local directory where the ground truth json is located. If not supplied, it is assumed to be the same as the directory. Defaults to None. - tmp_directory (str, optional): Path to a temporary directory to use as the output directory, where the contents will then be copied to the actual output directory at the end of the evaluation. - device (str, optional): The PyTorch device string to use for inference. If not supplied, it is assumed to be cuda:0. Defaults to None. - pattern (str, optional): The regex pattern to use for selecting the inference files. If not supplied, it is assumed to be the default pattern. Defaults to None. - store_all (bool, optional): If set, all results will be saved. Defaults to False. - dry_run (bool, optional): If set, the evaluation will not be run. If the command would be executed it is printed instead. Defaults to False. - execute (bool, optional): If set, the evaluation will be run, otherwise the command will be returned. Defaults to True. - strict (bool, optional): If set, the files and directories must exist when this function is called. Defaults to True. + weights: The path to the weights file. + config: The path to the config file. Should be a YAML file either ending in '.yaml' or '.yml'. + directory: The directory where the data is located and where the results will be saved the directory should have a 'reference' directory with the ground truth json in 'instances_default.json' and the matching images'. + output_directory: The path to the output directory where the results will be saved. If not supplied, it is assumed to be the same as the directory. Defaults to None. + local_directory: The path to the local directory where the ground truth json is located. If not supplied, it is assumed to be the same as the directory. Defaults to None. + tmp_directory: Path to a temporary directory to use as the output directory, where the contents will then be copied to the actual output directory at the end of the evaluation. + device: The PyTorch device string to use for inference. If not supplied, it is assumed to be cuda:0. Defaults to None. + pattern: The regex pattern to use for selecting the inference files. If not supplied, it is assumed to be the default pattern. Defaults to None. + store_all: If set, all results will be saved. Defaults to False. + dry_run: If set, the evaluation will not be run. If the command would be executed it is printed instead. Defaults to False. + execute: If set, the evaluation will be run, otherwise the command will be returned. Defaults to True. + strict: If set, the files and directories must exist when this function is called. Defaults to True. Returns: str: The (executed) command (to run). + """ # Fix paths weights, config, directory, output_directory, local_directory, tmp_directory = [ @@ -68,7 +80,7 @@ def eval_model( assert os.path.exists(local_directory), f"Local directory not found: {local_directory}" do_transfer_results = False dst_dir = output_directory - if not tmp_directory is None: + if tmp_directory is not None: if not os.path.isdir(tmp_directory) and strict: if os.path.exists(tmp_directory): raise FileExistsError(f"Specified {tmp_directory} already exists, and is not a directory.") @@ -139,8 +151,8 @@ def eval_model( def eval_model_wrapper( params : dict, execute : bool=True, - device : Optional[str]=None, - dry_run : Optional[bool]=None + device : str | None=None, + dry_run : bool | None=None ) -> str: if device is not None: params.pop("device", None) @@ -149,15 +161,15 @@ def eval_model_wrapper( print("Executing evaluation as dry run.") return eval_model(**params, execute=execute, device=device) -def get_weights_in_directory(directory : str) -> List[str]: - """ - Get the weights file in the directory. The weights may be stored in arbitrarily nested subdirectories. +def get_weights_in_directory(directory : str) -> list[str]: + """Get the weights file in the directory. The weights may be stored in arbitrarily nested subdirectories. Args: - directory (str): The directory where weights are located. + directory: The directory where weights are located. Returns: str: The path to the best weight file. + """ # Check if the directory exists assert os.path.exists(directory) and os.path.isdir(directory), f"Directory not found: {directory}" @@ -173,12 +185,12 @@ def get_weights_in_directory(directory : str) -> List[str]: return sorted(weight_files, key=os.path.getmtime) -def get_gpus() -> List[str]: - """ - Get the available GPUs. +def get_gpus() -> list[str]: + """Get the available GPUs. Returns: list: The available GPUs. + """ # Get the GPU information gpu_info = subprocess.Popen("nvidia-smi --query-gpu=index --format=csv,noheader,nounits", shell=True, stdout=subprocess.PIPE).stdout.read().decode("utf-8") @@ -187,23 +199,23 @@ def get_gpus() -> List[str]: return gpus def combine_result_csvs( - result_directories : List[str], + result_directories : list[str], dst_path : str, dry_run : bool=False ) -> str: - """ - Combines the result CSVs in the result directories. The result CSVs are assumed to be in the 'results' subdirectory of the result directories and named 'results.csv'. + """Combines the result CSVs in the result directories. The result CSVs are assumed to be in the 'results' subdirectory of the result directories and named 'results.csv'. The function simply creates a new csv file in the new directory with the combined results. The combined results contains all the rows from all the result CSVs, with a new column added: 'model'. The 'model' column is populated with the name of the model directory. Args: - result_directories (list of str): A list of result directories. - dst_path (str): The path of the combined results output CSV. + result_directories: A list of result directories. + dst_path: The path of the combined results output CSV. Returns: str: The path to the combined results CSV. + """ # Check if the destination is a directory if os.path.isdir(dst_path): @@ -274,7 +286,7 @@ def __str__(self): def __repr__(self): return str(self) -def combine_result_csvs_wrapper(args : Union[List, Tuple, Dict], execute : bool=True, device : Any=None, **kwargs) -> str: +def combine_result_csvs_wrapper(args : list | tuple | dict, execute : bool=True, device : Any=None, **kwargs) -> str: if isinstance(args, (list, tuple)): deferred_call = DeferredCall(combine_result_csvs, *args) elif isinstance(args, dict): @@ -313,7 +325,7 @@ def combine_result_csvs_wrapper(args : Union[List, Tuple, Dict], execute : bool= f"Error parsing extra arguments: `{' '.join(extra)}`. {e}\n\n" f"{arg_parse.format_help()}" ) - if not args.output is None and not args.soft: + if args.output is not None and not args.soft: assert os.path.exists(args.output) and os.path.isdir(args.output), f'Output directory not found: {args.output}' RESULT_DIR = args.output @@ -362,7 +374,7 @@ def combine_result_csvs_wrapper(args : Union[List, Tuple, Dict], execute : bool= # Get the result directory path for the current model and weight file this_weight_id_subdir = os.path.join(args.name, os.path.basename(model_directory), id) this_result_dir = os.path.join(RESULT_DIR, this_weight_id_subdir) - if not args.temporary_output_directory is None: + if args.temporary_output_directory is not None: this_tmp_output_dir = os.path.join(os.path.expanduser(args.temporary_output_directory), this_weight_id_subdir) else: this_tmp_output_dir = None @@ -395,7 +407,7 @@ def combine_result_csvs_wrapper(args : Union[List, Tuple, Dict], execute : bool= all_result_directories = [] [all_result_directories.extend(dirs) for dirs in result_directories.values()] - if not "job_name" in extra: + if "job_name" not in extra: extra.update({"job_name" : f'compare_models{"_" if args.name else ""}{args.name}'}) runner = ExperimentRunner(eval_model_wrapper, all_eval_params, devices=args.device, dry_run=args.dry_run, slurm=args.slurm, slurm_params=read_slurm_params(**extra)) diff --git a/scripts/experiments/experiment_helpers.py b/scripts/experiments/experiment_helpers.py index e12cde0..33405f5 100644 --- a/scripts/experiments/experiment_helpers.py +++ b/scripts/experiments/experiment_helpers.py @@ -11,8 +11,8 @@ import time import zipfile from argparse import Namespace -from typing import (IO, Any, Callable, Dict, Iterable, List, Optional, Self, - Tuple, Union) +from collections.abc import Callable, Iterable +from typing import IO, Any, Self import submitit import torch @@ -58,10 +58,10 @@ def set_default_config(config : str): DEFAULT_CONFIG = config assert os.path.exists(DEFAULT_CONFIG), f"Default config file not found: {DEFAULT_CONFIG}" -def get_config() -> Dict[str, Any]: +def get_config() -> dict[str, Any]: if DEFAULT_CONFIG == "": raise RuntimeError("The default config file has not been set. Use `experiment__helpers.set_default_config()` to set it.") - with open(DEFAULT_CONFIG, "r") as conf: + with open(DEFAULT_CONFIG) as conf: config = yaml.load(conf, Loader=yaml.FullLoader) if PROJECT_DIR != "": config["project"] = PROJECT_DIR @@ -82,8 +82,8 @@ def print_and_sleep(text : str): time.sleep(3) def run_command( - command : Union[str | Callable], - python_binary : Optional[str]=None + command : str | Callable, + python_binary : str | None=None ) -> bool: if callable(command): command = command() @@ -115,8 +115,7 @@ def remove_directory( directory : str, recursive : bool=False ): - """ - Safely removes a directory containing files, no nested directories. + """Safely removes a directory containing files, no nested directories. """ if not os.path.exists(directory): return @@ -137,9 +136,8 @@ def remove_directory( SAMPLE_SANITIZE_PATTERN = re.compile(r"^[^_]+_(.+)(_heatmap|_matches|\.csv)") -def split_by_sample(files : List[str]) -> Dict[str, List[str]]: - """ - Splits the files by sample. +def split_by_sample(files : list[str]) -> dict[str, list[str]]: + """Splits the files by sample. """ samples = {} for file in files: @@ -226,7 +224,7 @@ def open(self, path : str, mode : str = "r", *args, **kwargs) -> IO[bytes]: if isinstance(self._zip, zipfile.ZipFile): mode = mode.replace("t", "") raw_file = self._zip.open(self._zip_prep_path(path), mode=mode, *args, **kwargs) - if not "b" in mode: + if "b" not in mode: return io.TextIOWrapper(raw_file) return raw_file else: @@ -251,17 +249,17 @@ def error(self, message : str): self.print_help(sys.stderr) self.exit(2, f"\n\n{self.prog}: error: {message}\n") -def parse_unknown_arguments(extra : List[str]) -> Dict[str, Any]: - """ - Parses unknown arguments from the command line. +def parse_unknown_arguments(extra : list[str]) -> dict[str, Any]: + """Parses unknown arguments from the command line. Unknown arguments must be named arguments in the form `--key value`, `-key value` or `key=value`. Args: - extra (List[str]): The list of extra arguments. + extra: The list of extra arguments. Returns: Dict[str, Any]: The parsed unknown arguments. + """ unknown_args = {} i = 0 @@ -288,9 +286,8 @@ def parse_unknown_arguments(extra : List[str]) -> Dict[str, Any]: i += 1 return unknown_args -def get_cmd_args(name : Optional[str] = None, additional_args : Optional[List[Tuple[List[str], Dict[str, Any]]]] = None) -> Tuple[Namespace, Dict[str, str]]: - """ - A simple wrapper for shared command line arguments and parsing between experiment orchestration scripts. +def get_cmd_args(name : str | None = None, additional_args : list[tuple[list[str], dict[str, Any]]] | None = None) -> tuple[Namespace, dict[str, str]]: + """A simple wrapper for shared command line arguments and parsing between experiment orchestration scripts. Command line arguments: -i, --datadir: The directory containing the data. @@ -304,6 +301,7 @@ def get_cmd_args(name : Optional[str] = None, additional_args : Optional[List[Tu Returns: argparse.Namespace: The parsed command line arguments. + """ args_parse = HelpfulArgumentParser(formatter_class=argparse.RawTextHelpFormatter) args_parse.add_argument("-i", "--datadir", help="The directory containing the data.", required=True) @@ -335,7 +333,7 @@ def get_cmd_args(name : Optional[str] = None, additional_args : Optional[List[Tu f"Error parsing extra arguments: `{' '.join(extra)}`. {e}\n\n" f"{args_parse.format_help()}" ) - if not args.extra is None: + if args.extra is not None: probable_desired_command = sys.executable + " " + " ".join([arg for arg in sys.argv if arg != "--do-not-specify-extra"]) raise ValueError( f"DO NOT ACTUALLY SPECIFY --do-not-specify-extra, just pass the extra arguments after known.\n\n" @@ -343,9 +341,9 @@ def get_cmd_args(name : Optional[str] = None, additional_args : Optional[List[Tu f"\t{probable_desired_command}\n\n" f"{args_parse.format_help()}" ) - if not args.output is None: + if args.output is not None: args.output = os.path.normpath(os.path.expanduser(args.output)) - if not args.output is None: + if args.output is not None: project_dir = args.output else: project_dir = os.path.abspath("./runs/segment") @@ -366,29 +364,29 @@ def get_cmd_args(name : Optional[str] = None, additional_args : Optional[List[Tu ) extra.update({"cpus_per_task" : n_workers}) - if not "job_name" in extra: + if "job_name" not in extra: extra.update({"job_name" : name if name is not None else "fb_unnamed_experiment"}) return args, extra def read_slurm_params( - path : Optional[str] = os.path.join(os.path.dirname(__file__), "default_slurm_params.yaml"), + path : str | None = os.path.join(os.path.dirname(__file__), "default_slurm_params.yaml"), **kwargs - ) -> Dict[str, Any]: - """ - Simple wrapper to read SLURM parameters from a YAML file, or use the default SLURM parameters if not supplied. + ) -> dict[str, Any]: + """Simple wrapper to read SLURM parameters from a YAML file, or use the default SLURM parameters if not supplied. Args: - path (Optional[str]): The path to the SLURM parameters YAML file. Default and None is "default_slurm_params.yaml" in the same directory as this script. + path: The path to the SLURM parameters YAML file. Default and None is "default_slurm_params.yaml" in the same directory as this script. **kwargs: Additional keyword arguments to pass to the SLURM parameters (e.g. partition). These will override the parameters in the YAML file if they are also present. Returns: Dict[str, Any]: The SLURM parameters. The keys are prefixed with 'slurm_', necessary for the submitit executor. + """ if path is None: path = os.path.join(os.path.dirname(__file__), "default_slurm_params.yaml") - with open(path, "r") as f: - params : Dict = yaml.safe_load(f) + with open(path) as f: + params : dict = yaml.safe_load(f) params.update(kwargs) ## THIS IS NOT NECESSARY AFTER SWITCHING FROM `submitit.AutoExecutor` TO `submitit.SlurmExecutor` @@ -414,11 +412,11 @@ def read_slurm_params( slurm_setup_path = os.path.join(os.path.dirname(__file__), "slurm_config", additional_params.pop("slurm_setup")) if not (isinstance(slurm_setup_path, str) and os.path.exists(slurm_setup_path)): raise FileNotFoundError(f"Invalid SLURM setup file specified: {slurm_setup_path}.") - with open(slurm_setup_path, "r") as f: + with open(slurm_setup_path) as f: slurm_setup_commands = f.read().strip().split("\n") if slurm_setup_commands[0] == 0: slurm_setup_commands.pop(0) - assert len(slurm_setup_commands) > 0, f"Empty SLURM setup file specified." + assert len(slurm_setup_commands) > 0, "Empty SLURM setup file specified." params["setup"] = additional_params.get("setup", []) + slurm_setup_commands # Submit additional parameters via the "additional_parameters" parameter @@ -428,24 +426,24 @@ def read_slurm_params( return params def do_yolo_train_run( - config : Dict, + config : dict, attempt_resume : bool=False, dry_run : bool=False, execute : bool=True, - device : Optional[Union[int, str, List[Union[int, str]]]]=None - ) -> Optional[str]: - """ - Wrapper for conducting a Flat-Bug YOLO training run, with `fb_train`. + device : int | str | list[int | str] | None=None + ) -> str | None: + """Wrapper for conducting a Flat-Bug YOLO training run, with `fb_train`. Args: - config (Dict): The configuration dictionary. - attempt_resume (bool): Whether to attempt to restart cancelled training runs with the same name and project (if they exist). - dry_run (bool): Whether to print the command without running it. Defaults to False. - execute (bool): Whether to run the command. Defaults to True. - device (Optional[Union[int, str]]): The GPU to use for the experiment. Defaults to None. + config: The configuration dictionary. + attempt_resume: Whether to attempt to restart cancelled training runs with the same name and project (if they exist). + dry_run: Whether to print the command without running it. Defaults to False. + execute: Whether to run the command. Defaults to True. + device: The GPU to use for the experiment. Defaults to None. Returns: Optional[str]: The (executed) command (to run) or None if training is already completed. + """ if DATA_DIR == "": raise RuntimeError("The data directory has not been set. Use `experiment_helpers.set_datadir()` to set it.") @@ -497,7 +495,7 @@ def do_yolo_train_run( print(f"Running experiment: {config['name']} with config:{ITEMIZE + ITEMIZE.join([f'{k}: {v}' for k, v in config.items()])}") command = f'fb_train -c "{config_path}" -d "{DATA_DIR}"' if attempt_resume: - command += f' -r' + command += ' -r' if execute: if dry_run: print(f'Dry run would executed: {command}') @@ -508,23 +506,22 @@ def do_yolo_train_run( class ExperimentRunner: def __init__( - self : Self, + self, experiment_fn : Callable = do_yolo_train_run, inputs : Iterable = [], - devices : Optional[Union[List[Union[int, str]], int, str]] = None, + devices : list[int | str] | int | str | None = None, slurm : bool=False, - slurm_params : Optional[Dict[str, Any]] = None, + slurm_params : dict[str, Any] | None = None, **kwargs ): - """ - A class to handle running multiple experiments, either sequentially, in parallel on multiple GPUs, or on a SLURM cluster. + """A class to handle running multiple experiments, either sequentially, in parallel on multiple GPUs, or on a SLURM cluster. Args: - experiment_fn (Callable): The function to run the experiment. Must accept a single element from inputs and a dictionary of keyword arguments, as well as the argument `execute` that defaults to True, which determines whether the function should execute the command or just return a bash command string. - inputs (Iterable): The inputs to the experiment function. - devices (Optional[Union[List[Union[int, str]], int, str]]): The GPU(s) to use for the experiments. Defaults to None. - slurm (bool): Whether to run the experiments on a SLURM cluster. Defaults to False. - slurm_params (Optional[Dict[str, Any]]): The parameters to pass to the SLURM executor. Defaults to None. + experiment_fn: The function to run the experiment. Must accept a single element from inputs and a dictionary of keyword arguments, as well as the argument `execute` that defaults to True, which determines whether the function should execute the command or just return a bash command string. + inputs: The inputs to the experiment function. + devices: The GPU(s) to use for the experiments. Defaults to None. + slurm: Whether to run the experiments on a SLURM cluster. Defaults to False. + slurm_params: The parameters to pass to the SLURM executor. Defaults to None. **kwargs: Additional keyword arguments to pass to the experiment function. Methods: @@ -545,6 +542,7 @@ def test_fn(input, execute=True, **kwargs): runner.run() runner.wait() ``` + """ self.experiment_fn = experiment_fn self.kwargs = kwargs @@ -562,8 +560,8 @@ def test_fn(input, execute=True, **kwargs): self.executor.update_parameters(**slurm_params) # Initialize consumer/job lists - self.consumer_threads : List[threading.Thread] = [] - self.slurm_jobs : List[submitit.Job] = [] + self.consumer_threads : list[threading.Thread] = [] + self.slurm_jobs : list[submitit.Job] = [] def __len__(self): return self._length @@ -599,14 +597,14 @@ def pretty_parse_slurm_results(job : submitit.Job) -> str: return "\n".join([line[:max_char_per_line] for line in lines]) @property - def slurm_job_ids(self : Self) -> List[str]: + def slurm_job_ids(self) -> list[str]: if not self.slurm: return [] else: return [job.job_id for job in self.slurm_jobs] @property - def slurm_job_id(self : Self) -> Optional[str]: + def slurm_job_id(self) -> str | None: ids = list(set([re.search(r"^(\d+)", job.job_id).group(1) for job in self.slurm_jobs])) if len(ids) == 0: return None @@ -614,7 +612,7 @@ def slurm_job_id(self : Self) -> Optional[str]: raise ValueError(f"Multiple job IDs found: {ids}") return ids[0] - def run(self : Self) -> Self: + def run(self) -> Self: # Check that the consumer threads and slurm jobs are empty assert not self.consumer_threads, "Consumer threads list is not empty." assert not self.slurm_jobs, "Slurm jobs list is not empty." @@ -651,7 +649,7 @@ def run(self : Self) -> Self: return self - def wait(self : Self) -> Self: + def wait(self) -> Self: # Wait for the consumer threads to finish [self.consumer_threads.pop().join() for _ in range(len(self.consumer_threads))] # Wait for the slurm jobs to finish @@ -659,7 +657,7 @@ def wait(self : Self) -> Self: return self - def complete(self : Self) -> Self: + def complete(self) -> Self: if self.slurm: # When using SLURM, the experiments are submitted as an array job and the script exits immediately, so we don't need to have a process alive for the duration of the experiments print(f"All (n={len(self)}) experiments submitted as SLURM array job.") diff --git a/scripts/experiments/leave_one_out_cv_and_finetuning/orchestrate.py b/scripts/experiments/leave_one_out_cv_and_finetuning/orchestrate.py index c1cdd9d..3d8caca 100644 --- a/scripts/experiments/leave_one_out_cv_and_finetuning/orchestrate.py +++ b/scripts/experiments/leave_one_out_cv_and_finetuning/orchestrate.py @@ -1,10 +1,20 @@ -import os, sys, re -sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "..")) -from scripts.experiments.experiment_helpers import DATASETS, set_default_config, get_config, get_cmd_args, read_slurm_params, ExperimentRunner +import os +import re +import sys +sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "..")) from collections import OrderedDict +from scripts.experiments.experiment_helpers import ( + DATASETS, + ExperimentRunner, + get_cmd_args, + get_config, + read_slurm_params, + set_default_config, +) + BASE_NAME = "fb_leave_one_out" BASE_PATH = os.path.dirname(__file__) DEFAULT_CONFIG = os.path.join(BASE_PATH, "default.yaml") diff --git a/scripts/experiments/leave_two_out_dataset_mapping/orchestrate.py b/scripts/experiments/leave_two_out_dataset_mapping/orchestrate.py index 79273f3..253faeb 100644 --- a/scripts/experiments/leave_two_out_dataset_mapping/orchestrate.py +++ b/scripts/experiments/leave_two_out_dataset_mapping/orchestrate.py @@ -1,24 +1,29 @@ -import os, sys -sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "..")) -from scripts.experiments.experiment_helpers import set_default_config, get_config, get_cmd_args, read_slurm_params, ExperimentRunner - -from typing import List +import os +import sys +sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "..")) from collections import OrderedDict +from scripts.experiments.experiment_helpers import ( + ExperimentRunner, + get_cmd_args, + get_config, + read_slurm_params, + set_default_config, +) + BASE_NAME = "fb_leave_two_out" BASE_PATH = os.path.dirname(__file__) DEFAULT_CONFIG = os.path.join(BASE_PATH, "default.yaml") set_default_config(DEFAULT_CONFIG) -def parse_include_datasets(path : str) -> List[str]: - """ - Parses the include datasets file. +def parse_include_datasets(path : str) -> list[str]: + """Parses the include datasets file. """ remove_comment = lambda line: line[:line.find("#")] if "#" in line else line - with open(path, "r") as f: + with open(path) as f: datasets = [remove_comment(line).strip() for line in f if not line.startswith("/")] return [dataset for dataset in datasets if dataset] diff --git a/scripts/experiments/test_experiment_runner.py b/scripts/experiments/test_experiment_runner.py index 7fffd25..9ca6f0c 100644 --- a/scripts/experiments/test_experiment_runner.py +++ b/scripts/experiments/test_experiment_runner.py @@ -1,6 +1,9 @@ -import os, sys +import os +import sys + sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..")) -from scripts.experiments.experiment_helpers import get_cmd_args, read_slurm_params, ExperimentRunner, run_command +from scripts.experiments.experiment_helpers import ExperimentRunner, get_cmd_args, read_slurm_params, run_command + def print_environment_status_for_debug(test_string : str, dry_run : bool=False, execute : bool=True, device=None): command = f""" diff --git a/scripts/manuscript/experiment/leave_one_out_cv_and_finetuning_orchestrate_modified.py b/scripts/manuscript/experiment/leave_one_out_cv_and_finetuning_orchestrate_modified.py index 7ff6fea..bbcad28 100644 --- a/scripts/manuscript/experiment/leave_one_out_cv_and_finetuning_orchestrate_modified.py +++ b/scripts/manuscript/experiment/leave_one_out_cv_and_finetuning_orchestrate_modified.py @@ -1,10 +1,20 @@ -import os, sys, re -sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "..")) -from scripts.experiments.experiment_helpers import DATASETS, set_default_config, get_config, get_cmd_args, read_slurm_params, ExperimentRunner +import os +import re +import sys +sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "..")) from collections import OrderedDict +from scripts.experiments.experiment_helpers import ( + DATASETS, + ExperimentRunner, + get_cmd_args, + get_config, + read_slurm_params, + set_default_config, +) + BASE_NAME = "fb_leave_one_out" BASE_PATH = os.path.dirname(__file__) DEFAULT_CONFIG = os.path.join(BASE_PATH, "default.yaml") diff --git a/scripts/manuscript/figure_tiles/make_figure.py b/scripts/manuscript/figure_tiles/make_figure.py index 1d8a574..8df877b 100644 --- a/scripts/manuscript/figure_tiles/make_figure.py +++ b/scripts/manuscript/figure_tiles/make_figure.py @@ -2,7 +2,6 @@ import json import math import os -from typing import List, Optional, Tuple import cv2 import numpy as np @@ -32,7 +31,7 @@ ## HELPERS -def match_examples(files : List[str], *filters): +def match_examples(files : list[str], *filters): matches = [] for this_filters in zip(*filters): for file in files: @@ -40,7 +39,7 @@ def match_examples(files : List[str], *filters): matches.append(file) return matches -def annotations_to_tensor_predictions(annotations : List[np.ndarray], offset : Tuple[int, int], image : torch.tensor, path : str) -> TensorPredictions: +def annotations_to_tensor_predictions(annotations : list[np.ndarray], offset : tuple[int, int], image : torch.tensor, path : str) -> TensorPredictions: c, h, w = image.shape if h != w: raise NotImplementedError("Plotting annotations is only implemented for square images. TODO: implement this.") @@ -85,7 +84,7 @@ def annotations_to_tensor_predictions(annotations : List[np.ndarray], offset : T tensor_predictions.image = image return tensor_predictions -def create_mosaic(ims : List[np.ndarray], spacing : int = 100, labels : Optional[List[str]]=None): +def create_mosaic(ims : list[np.ndarray], spacing : int = 100, labels : list[str] | None=None): n = len(ims) h, w, _ = ims[0].shape @@ -140,7 +139,7 @@ def create_label(ri : int, ci : int, label : str): model = Predictor("flat_bug_L.pt", device=device, dtype=torch.float16) example_df = pd.read_csv(os.path.join(THIS_DIR, "clean_flatbug_datasets.csv")) - with open(ANNOTATION_COCO, "r") as f: + with open(ANNOTATION_COCO) as f: annotations = split_annotations(json.load(f)) example_df["raw_file_path"] = match_examples( diff --git a/scripts/manuscript/pyramid_visualization/visualize_pyramid.py b/scripts/manuscript/pyramid_visualization/visualize_pyramid.py index 5c67708..80363ed 100644 --- a/scripts/manuscript/pyramid_visualization/visualize_pyramid.py +++ b/scripts/manuscript/pyramid_visualization/visualize_pyramid.py @@ -1,8 +1,6 @@ import math import os -from typing import List, Tuple -import cv2 import torch from matplotlib import pyplot as plt from matplotlib.patches import Rectangle @@ -15,7 +13,7 @@ from flat_bug.predictor import Predictor -def get_scales(model : Predictor, image : str, scale_increment : float=1/2, scale_before : float=1) -> List[float]: +def get_scales(model : Predictor, image : str, scale_increment : float=1/2, scale_before : float=1) -> list[float]: if isinstance(image, str): path : str = image image : torch.Tensor = read_image( @@ -81,7 +79,7 @@ def get_scales(model : Predictor, image : str, scale_increment : float=1/2, scal return scales, transformed_image -def get_tile_params(model : Predictor, image : torch.Tensor, scale : float) -> Tuple[List[Tuple[Tuple[int, int], Tuple[int, int]]], torch.Tensor]: +def get_tile_params(model : Predictor, image : torch.Tensor, scale : float) -> tuple[list[tuple[tuple[int, int], tuple[int, int]]], torch.Tensor]: orig_h, orig_w = image.shape[1:] w, h = orig_w, orig_h h_pad, w_pad = 0, 0 diff --git a/scripts/manuscript/statistics/helpers/flatbug_helpers.R b/scripts/manuscript/statistics/helpers/flatbug_helpers.R index c27882b..22974ce 100644 --- a/scripts/manuscript/statistics/helpers/flatbug_helpers.R +++ b/scripts/manuscript/statistics/helpers/flatbug_helpers.R @@ -284,7 +284,7 @@ get_data <- function(name, file="experiment_results_latex.tex") { if (!file.exists(file)) stop(str_c("Latex data file (", file, ") does not exist. Perhaps call `make_data_file`.")) lines <- read_lines(file) SE <- find_group(name, lines) - return(lines[(SE[1] + 1):(SE[2] - 1)]) + return:(SE[2] - 1)]) } write_data <- function(name, data, file="experiment_results_latex.tex") { diff --git a/scripts/manuscript/time_flatbug.py b/scripts/manuscript/time_flatbug.py index c7234fd..64e0181 100644 --- a/scripts/manuscript/time_flatbug.py +++ b/scripts/manuscript/time_flatbug.py @@ -42,7 +42,7 @@ def time_model(size : str, files : list[str], device="cuda:0" if torch.cuda.is_a timings[os.path.basename(path)] = (start.elapsed_time(end) / 1000, instances, tuple(image.shape[1:])) del image totals = list(map(sum, list(zip(*timings.values()))[:2])) - print('Found {} instances in {} images in {:.1f} seconds'.format(totals[1], len(tmpfiles), totals[0])) + print(f'Found {totals[1]} instances in {len(tmpfiles)} images in {totals[0]:.1f} seconds') del model, tmpfiles torch.cuda.empty_cache() @@ -52,8 +52,7 @@ def time_model(size : str, files : list[str], device="cuda:0" if torch.cuda.is_a ### Plotting functions # --- A simple lowess implementation using only NumPy --- def lowess(x, y, frac=0.3): - """ - A simple lowess smoother using a tricube weighting kernel. + """A simple lowess smoother using a tricube weighting kernel. x and y must be 1D arrays of the same length. frac is the fraction of points used for local regression. Returns an array of smoothed y-values. @@ -98,8 +97,7 @@ def plot_results(results: dict[str, tuple[float, int, tuple[int, int]]], scale_inv=lambda x: x ** 2, min_area=5, max_area=300): - """ - Plots the scatter points for one model. + """Plots the scatter points for one model. results: dict mapping keys to (seconds, instances, (height, width)) ax: matplotlib Axes object @@ -112,6 +110,7 @@ def plot_results(results: dict[str, tuple[float, int, tuple[int, int]]], x: array of x-values (sqrt(pixel area)) y: array of y-values (seconds) instances: list of instance counts + """ # Unpack data: seconds, instance count, (height, width) seconds, instances, dimensions = list(zip(*results.values())) diff --git a/src/bin/fb_clone_data.py b/src/bin/fb_clone_data.py index dda75c7..20148b6 100644 --- a/src/bin/fb_clone_data.py +++ b/src/bin/fb_clone_data.py @@ -1,14 +1,17 @@ -from zipfile import ZipFile -import hashlib -import boto3 -from cvat_sdk import make_client -from botocore.config import Config -import yaml +# noqa: D100 import argparse +import hashlib +import json +import os from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path -import os, json, shutil +from zipfile import ZipFile + +import boto3 +import yaml +from botocore.config import Config from botocore.exceptions import ClientError +from cvat_sdk import make_client try: from tqdm import tqdm @@ -16,6 +19,9 @@ except Exception: _HAVE_TQDM = False +# TODO: fixme +# ruff: disable[D103] + secrets_structure = """ cvat: @@ -41,14 +47,12 @@ # ------------------ Load secrets from YAML ------------------ def load_secrets_yaml(path): - with open(path, "r") as f: + with open(path) as f: return yaml.safe_load(f) # ------------------ Helpers ------------------ def safe_segment(name: str) -> str: - """ - Make a filesystem-safe folder name (keep common chars; replace others with underscore). - """ + """Make a filesystem-safe folder name (keep common chars; replace others with underscore).""" allowed = "-_.() abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" cleaned = "".join(c if c in allowed else "_" for c in name).strip() # avoid empty folder names @@ -66,10 +70,11 @@ def md5_file(path: Path, chunk=1024 * 1024) -> str: return h.hexdigest() def task_is_completed(task) -> bool: - """ + """Check if a task is done. + Consider a task completed if either: - - task.status == 'completed', OR - - all of its jobs are in state == 'completed' + - task.status == 'completed', OR + - all of its jobs are in state == 'completed' """ try: if getattr(task, "status", None) == "completed": @@ -99,9 +104,7 @@ def build_s3_client(s3_access_key, s3_secret_key, s3_region, s3_endpoint): ) def list_s3_objects_with_prefix(s3, bucket: str, prefix: str): - """ - Yield dicts with 'Key', 'Size', 'ETag' (no quotes), and 'LastModified'. - """ + """Yield dicts with 'Key', 'Size', 'ETag' (no quotes), and 'LastModified'.""" continuation = None while True: kwargs = {"Bucket": bucket, "Prefix": prefix} @@ -129,7 +132,8 @@ def ensure_parent(path: Path): # # ------------------ COCO Export ------------------ def export_coco_annotations_for_task(task, output_json_path: Path, s3_prefix): - """ + """Export COCO for task. + Export task dataset (COCO 1.0, annotations only) to a temp zip, then extract the COCO annotations json to output_json_path. """ @@ -200,8 +204,7 @@ def sync_s3_prefix_to_local( local_root: Path, delete_extraneous: bool = True, ) -> bool: - """ - Incremental sync of s3://bucket/prefix -> local_root. + """Incremental sync of s3://bucket/prefix -> local_root. - Skips files whose cached ETag matches upstream ETag. - If no sidecar exists: when upstream ETag looks like a single-part MD5 @@ -307,8 +310,7 @@ def _process_one_task(task_id: int, s3_bucket: str, s3_prefix_root: str, target_dir: Path): - """Runs in a thread. Returns (task_id, ok, msg).""" - + """Runs in a thread. Returns (task_id, ok, msg).""" # noqa: D401 CVAT_HOST = cfg_cvat.get("host", "https://app.cvat.ai") USERNAME = cfg_cvat["username"] PASSWORD = cfg_cvat["password"] @@ -359,17 +361,25 @@ def _process_one_task(task_id: int, def main(): args_parse = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) - args_parse.add_argument("-s", "--secrets-file", dest="secrets_file", - help="A YAML files containing credentials for s3 and cvat. It has the following structure" - f"{secrets_structure}" - ) - - args_parse.add_argument("-o", "--output-dir", dest="output_dir", - help="The output directory where all subdatasets are stored. Each subdirectory is a coco dataset, with a JSON file and a list of images") - - args_parse.add_argument("-f", "--force", dest="delete_target_before", - help="Delete output directory before, this avoids duplicating data etc", - action="store_true") + args_parse.add_argument( + "-s", "--secrets-file", dest="secrets_file", + help=( + "A YAML files containing credentials for s3 and cvat. It has the following structure" + f"{secrets_structure}" + )) + + args_parse.add_argument( + "-o", "--output-dir", dest="output_dir", + help=( + "The output directory where all subdatasets are stored. " + "Each subdirectory is a coco dataset, with a JSON file and a list of images" + )) + + args_parse.add_argument( + "-f", "--force", dest="delete_target_before", + help="Delete output directory before, this avoids duplicating data etc", + action="store_true" + ) args = args_parse.parse_args() option_dict = vars(args) diff --git a/src/bin/fb_eval.py b/src/bin/fb_eval.py index 7a16595..792f174 100644 --- a/src/bin/fb_eval.py +++ b/src/bin/fb_eval.py @@ -1,5 +1,6 @@ -""" -Custom ``flatbug`` evaluation script. Requires prediction to have been run already, and ground truth labels should be supplied in COCO format. +"""Custom ``flatbug`` evaluation script. + +Requires prediction to have been run already, and ground truth labels should be supplied in COCO format. For end-to-end use the script `scripts/eval/end_to_end_eval.sh` (requires ``R`` for summary statistics and figures). @@ -40,9 +41,12 @@ from flat_bug.config import DEFAULT_CFG, read_cfg from flat_bug.eval_utils import compare_groups +# TODO: fixme +# ruff: disable[D103] + def load_json(file : str): - with open(file, "r") as f: + with open(file) as f: return json.load(f) # Wrapper function to call compare_groups with a single parameter dictionary for multiprocessing @@ -58,18 +62,50 @@ def main(): # iou_match_threshold = 0.1 parser = argparse.ArgumentParser(description='Evaluate predictions') - parser.add_argument('-p', '--predictions', type=str, help='Path or pattern to the predictions files', required=True) - parser.add_argument('-g', '--ground_truth', type=str, help='Path to the ground truth file', required=True) - parser.add_argument('-I', '--image_directory', type=str, help='Path to the image directory', required=True) - parser.add_argument('-o', '--output_directory', type=str, help='Path to the output directory', required=True) - parser.add_argument('--config', type=str, help='Path to the configuration file', required=False) - parser.add_argument('-P', '--plot', action="store_true", help='Plot the matches and the IoU matrix') - parser.add_argument('-b', '--no_boxes', action="store_false", help='Do not plot the bounding boxes') - parser.add_argument('-c', '--coco_predictions', action="store_true", help='Whether the predictions are already in a COCO format (legacy)') - parser.add_argument('-s', '--scale', type=float, default=1, help='Scale of the output images. Defaults to 1. Lower is faster.') - parser.add_argument('-n', type=int, default=-1, help='Number of images to process. Defaults to -1 (all images)') - parser.add_argument('--workers', type=int, default=8, help='Number of workers to use for the evaluation. Defaults to 8.') - parser.add_argument('--combine', action="store_true", help='Combine the results into a single CSV file') + parser.add_argument( + '-p', '--predictions', type=str, + help='Path or pattern to the predictions files', required=True + ) + parser.add_argument( + '-g', '--ground_truth', type=str, + help='Path to the ground truth file', required=True + ) + parser.add_argument( + '-I', '--image_directory', type=str, + help='Path to the image directory', required=True + ) + parser.add_argument( + '-o', '--output_directory', type=str, + help='Path to the output directory', required=True + ) + parser.add_argument( + '--config', type=str, + help='Path to the configuration file', required=False + ) + parser.add_argument( + '-P', '--plot', action="store_true", + help='Plot the matches and the IoU matrix' + ) + parser.add_argument( + '-b', '--no_boxes', action="store_false", + help='Do not plot the bounding boxes' + ) + parser.add_argument( + '-c', '--coco_predictions', action="store_true", + help='Whether the predictions are already in a COCO format (legacy)' + ) + parser.add_argument( + '-s', '--scale', type=float, default=1, help='Scale of the output images. Defaults to 1. Lower is faster.' + ) + parser.add_argument( + '-n', type=int, default=-1, help='Number of images to process. Defaults to -1 (all images)' + ) + parser.add_argument( + '--workers', type=int, default=8, help='Number of workers to use for the evaluation. Defaults to 8.' + ) + parser.add_argument( + '--combine', action="store_true", help='Combine the results into a single CSV file' + ) args = parser.parse_args() @@ -118,14 +154,14 @@ def main(): f'[{missing_pred_diff_formatted} {", ..." if len(pred_diff_keys) > show else ""}] and {len(pred_diff_keys) - show} more' ) if len(shared_keys) == 0: - raise ValueError(f'No images in common between the ground truth and the predictions') + raise ValueError('No images in common between the ground truth and the predictions') shared_keys = sorted(shared_keys) if args.n != -1: logger.info(f'Skipping the evaluation of {len(shared_keys) - args.n} images') shared_keys = shared_keys[:args.n] if len(shared_keys) == 0: - raise ValueError(f'No images to evaluate') + raise ValueError('No images to evaluate') if len(shared_keys) < args.workers: args.workers = min(args.workers, len(shared_keys)) logger.info(f"Warning: More workers than images, reducing the number of workers to {args.workers}") @@ -164,7 +200,10 @@ def main(): "threshold" : iou_match_threshold } all_params.append(this_params) - for matches in tqdm(pool.imap_unordered(process_image, all_params), total=len(shared_keys), desc="Evaluating images", dynamic_ncols=True): + for matches in tqdm( + pool.imap_unordered(process_image, all_params), + total=len(shared_keys), desc="Evaluating images", dynamic_ncols=True + ): result_files += [matches] pool.close() pool.join() diff --git a/src/bin/fb_predict.py b/src/bin/fb_predict.py index 8569f47..5e5ba8d 100644 --- a/src/bin/fb_predict.py +++ b/src/bin/fb_predict.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -r""" -Inference CLI script for ``flatbug``. +r"""Inference CLI script for ``flatbug``. -A comprehensive CLI API for ``flatbug`` inference with support for hyperparameter configuration, flexible input parsing, output format specification, and hardware specification. +A comprehensive CLI API for ``flatbug`` inference with support for hyperparameter configuration, +flexible input parsing, output format specification, and hardware specification. Usage: ``fb_predict -i INPUT_PATH_OR_DIRECTORY -o OUTPUT_DIRECTORY [OPTIONS]`` @@ -16,7 +16,8 @@ -w MODEL_WEIGHTS, --model-weights MODEL_WEIGHTS The .pt file -p INPUT_PATTERN, --input-pattern INPUT_PATTERN - The pattern to match the images. Default is '[^/]*\.([jJ][pP][eE]{0,1}[gG]|[pP][nN][gG])$' i.e. jpg/jpeg/png case-insensitive. + The pattern to match the images. + Default is '[^/]*\.([jJ][pP][eE]{0,1}[gG]|[pP][nN][gG])$' i.e. jpg/jpeg/png case-insensitive. -n MAX_IMAGES, --max-images MAX_IMAGES Maximum number of images to process. Default is None. Truncates in alphabetical order. -R, --recursive Process images nested within subdirectories of the input. @@ -44,7 +45,6 @@ import os import re import uuid -from typing import Optional import torch from tqdm import tqdm @@ -55,6 +55,9 @@ from flat_bug.predictor import Predictor from flat_bug.predictor import _executor as prediction_executor +# TODO: fixme +# ruff: disable[D103] + def cli_args(): args_parse = argparse.ArgumentParser( @@ -65,35 +68,87 @@ def cli_args(): formatter_class=argparse.RawTextHelpFormatter ) - args_parse.add_argument("-i", "--input", type=str, dest="input", required=True, - help="A image file or a directory of image files") - args_parse.add_argument("-o", "--output", type=str, dest="output_dir", required=True, - help="The result directory") - args_parse.add_argument("-w", "--model-weights", type=str, dest="model_weights", default="flat_bug_M.pt", - help="The .pt file") - args_parse.add_argument("-p", "--input-pattern", type=str, dest="input_pattern", default=r"[^/]*\.([jJ][pP][eE]{0,1}[gG]|[pP][nN][gG])$", - help=r"The pattern to match the images. Default is '[^/]*\.([jJ][pP][eE]{0,1}[gG]|[pP][nN][gG])$' i.e. jpg/jpeg/png case-insensitive.") - args_parse.add_argument("-n", "--max-images", type=int, dest="max_images", default=None, - help="Maximum number of images to process. Default is None. Truncates in alphabetical order.") - args_parse.add_argument("-R", "--recursive", action="store_true", - help="Process images nested within subdirectories of the input.") - args_parse.add_argument("-s", "--scale-before", type=float, dest="scale_before", default=1.0, - help="Downscale the image before detection, but crops from the original image.") - args_parse.add_argument("--single-scale", action="store_true", help="Use single scale.") - args_parse.add_argument("-M", "--nms_metric", type=str, default=None, help="Overlap metric to use for NMS, if specified this will override the config. Default is 'IoU', currently only 'IoS' is also available.") - args_parse.add_argument("-g", "--device", "--gpu", type=str, default="auto", help="Which device to use for inference.") - args_parse.add_argument("-d", "--dtype", type=str, default=None, help="Which dtype to use for inference. Default is 'float16' for CUDA and 'float32' for CPU.") - args_parse.add_argument("-f", "--fast", action="store_true", help="Use fast mode.") - args_parse.add_argument("--config", type=str, default=None, help="The config file.") - args_parse.add_argument("--id", type=str, default=None, required=False, help="Identifier (ID) for prediction run.") - args_parse.add_argument("--no-crops", action="store_true", help="Do not save the crops.") - args_parse.add_argument("--no-overviews", action="store_true", help="Do not save the overviews.") - args_parse.add_argument("--no-metadata", action="store_true", help="Do not save the metadata.") - args_parse.add_argument("--only-overviews", action="store_true", help="Only save the overviews.") - args_parse.add_argument("--long-format", action="store_true", help="Use long format for storing results.") - args_parse.add_argument("-S", "--no-save", action="store_true", help="Do not save the results.") - args_parse.add_argument("-C", "--no-compiled-coco", action="store_true", help="Skip the production of a compiled COCO file (for all images).") - args_parse.add_argument("-v", "--verbose", action="store_true", help="Verbose mode.") + args_parse.add_argument( + "-i", "--input", type=str, dest="input", required=True, + help="A image file or a directory of image files" + ) + args_parse.add_argument( + "-o", "--output", type=str, dest="output_dir", required=True, + help="The result directory" + ) + args_parse.add_argument( + "-w", "--model-weights", type=str, dest="model_weights", default="flat_bug_M.pt", + help="The .pt file" + ) + args_parse.add_argument( + "-p", "--input-pattern", type=str, dest="input_pattern", default=r"[^/]*\.([jJ][pP][eE]{0,1}[gG]|[pP][nN][gG])$", + help=( + "The pattern to match the images. " + r"Default is '[^/]*\.([jJ][pP][eE]{0,1}[gG]|[pP][nN][gG])$' i.e. jpg/jpeg/png case-insensitive." + )) + args_parse.add_argument( + "-n", "--max-images", type=int, dest="max_images", default=None, + help="Maximum number of images to process. Default is None. Truncates in alphabetical order." + ) + args_parse.add_argument( + "-R", "--recursive", action="store_true", + help="Process images nested within subdirectories of the input." + ) + args_parse.add_argument( + "-s", "--scale-before", type=float, dest="scale_before", default=1.0, + help="Downscale the image before detection, but crops from the original image." + ) + args_parse.add_argument( + "--single-scale", action="store_true", help="Use single scale." + ) + args_parse.add_argument( + "-M", "--nms_metric", type=str, default=None, + help=( + "Overlap metric to use for NMS, if specified this will override the config. " + "Default is 'IoU', currently only 'IoS' is also available." + )) + args_parse.add_argument( + "-g", "--device", "--gpu", type=str, default="auto", + help="Which device to use for inference." + ) + args_parse.add_argument( + "-d", "--dtype", type=str, default=None, + help="Which dtype to use for inference. Default is 'float16' for CUDA and 'float32' for CPU." + ) + args_parse.add_argument( + "-f", "--fast", action="store_true", help="Use fast mode." + ) + args_parse.add_argument( + "--config", type=str, default=None, help="The config file." + ) + args_parse.add_argument( + "--id", type=str, default=None, required=False, help="Identifier (ID) for prediction run." + ) + args_parse.add_argument( + "--no-crops", action="store_true", help="Do not save the crops." + ) + args_parse.add_argument( + "--no-overviews", action="store_true", help="Do not save the overviews." + ) + args_parse.add_argument( + "--no-metadata", action="store_true", help="Do not save the metadata." + ) + args_parse.add_argument( + "--only-overviews", action="store_true", help="Only save the overviews." + ) + args_parse.add_argument( + "--long-format", action="store_true", help="Use long format for storing results." + ) + args_parse.add_argument( + "-S", "--no-save", action="store_true", help="Do not save the results." + ) + args_parse.add_argument( + "-C", "--no-compiled-coco", action="store_true", + help="Skip the production of a compiled COCO file (for all images)." + ) + args_parse.add_argument( + "-v", "--verbose", action="store_true", help="Verbose mode." + ) args = args_parse.parse_args() return vars(args) @@ -103,7 +158,7 @@ def predict( output_dir : str, model_weights : str, input_pattern : str=r"[^/]*\.([jJ][pP][eE]{0,1}[gG]|[pP][nN][gG])$", - max_images : Optional[int]=None, + max_images : int | None=None, recursive : bool=False, scale_before : float=1.0, single_scale : bool=False, @@ -111,8 +166,8 @@ def predict( device : str="auto", dtype : str=None, fast : bool=False, - config : Optional[str]=None, - id : Optional[str]=None, + config : str | None=None, + id : str | None=None, no_crops : bool=False, no_overviews : bool=False, no_metadata : bool=False, @@ -201,7 +256,11 @@ def predict( overviews = False elif only_overviews: if long_format: - raise ValueError("Cannot set both --only-overviews and --long-format. --only-overviews already saves in long format (although not the same file structure as --long-format).") + raise ValueError( + "Cannot set both --only-overviews and --long-format. " + "--only-overviews already saves in long format " + "(although not the same file structure as --long-format)." + ) overviews = output_dir crops = False metadata = False @@ -236,22 +295,40 @@ def predict( # Check for local file index local_file_index = os.path.join(os.getcwd(), output_dir, f"{input.replace(os.sep, '_')}_file_index.txt") if os.path.isfile(local_file_index): - with open(local_file_index, "r") as file: + with open(local_file_index) as file: file_index = [line.strip() for line in file.readlines()] io.cache["file_index"] = file_index file_iter = RemotePathIterator( io_handler = io, # These are basically network-performance parameters - batch_size = 64, # How many files to download at once (larger is faster, but more memory intensive) - batch_parallel = 10, # How many files are downloaded in parallel in during each batch (10 seems to be optimal for my connection, this is probably dependent on the amount of cores on the server) - max_queued_batches = 3, # This relates to how much pre-fetching is done, i.e. how many batches are queued before the download is paused. This can be as large as you want, the larger the less stuttering you will have, but requires more local *disk* (NOT RAM) space - n_local_files = 100 * 3 * 2, # This is parameter basically does the same as the one above, but it really needs to larger than batch_size * max_queued_batches, otherwise files will be deleted before they are used (This *will* result in an error). This parameter should probably be removed from the `pyRemoteData` package... - clear_local = False, # Are local files temporary? I.e. should they be deleted after use? TODO: This should also cause the previous argument to be ignored, and **never** delete files before internally + # How many files to download at once (larger is faster, but more memory intensive) + batch_size = 64, + # How many files are downloaded in parallel in during each batch (10 seems to be optimal for my connection, + # this is probably dependent on the amount of cores on the server) + batch_parallel = 10, + # This relates to how much pre-fetching is done, i.e. how many batches are queued before the download is paused. + # This can be as large as you want, the larger the less stuttering you will have, but requires more local *disk* (NOT RAM) space + max_queued_batches = 3, + # This is parameter basically does the same as the one above, + # but it really needs to larger than batch_size * max_queued_batches, + # otherwise files will be deleted before they are used (This *will* result in an error). + # This parameter should probably be removed from the `pyRemoteData` package... + n_local_files = 100 * 3 * 2, + # Are local files temporary? I.e. should they be deleted after use? + # TODO: This should also cause the previous argument to be ignored, and **never** delete files before internally + clear_local = False, # These parameters are all related to file-indexing and filtering on the remote server - override = False, # Should the file-index be re-generated? (has to be False if store is False - otherwise an error will be thrown) - store = False, # This is important if we do not want to add files to the remote server (i.e. we only want to read them), if this is True, then the function will "cache" the file list in the directory in a file in the remote directory called "file_index.txt" - pattern = input_pattern # r"^[^\/\.]+(\.jpg$|\.png$|\.jpeg$|\.JPG$|\.PNG$|\.JPEG)$", # TODO: Currently as a hack, we skip files in subdirectories i.e. files with a '/' in their name, this is not ideal, as they are still read from the remote server + # Should the file-index be re-generated? (has to be False if store is False - otherwise an error will be thrown) + override = False, + # This is important if we do not want to add files to the remote server (i.e. we only want to read them), + # if this is True, then the function will "cache" the file list in + # the directory in a file in the remote directory called ".file_index.txt" + store = False, + # r"^[^\/\.]+(\.jpg$|\.png$|\.jpeg$|\.JPG$|\.PNG$|\.JPEG)$", + # # TODO: Currently as a hack, we skip files in subdirectories + # i.e. files with a '/' in their name, this is not ideal, as they are still read from the remote server + pattern = input_pattern ) elif isVideo: import tempfile diff --git a/src/bin/fb_prepare_data.py b/src/bin/fb_prepare_data.py index 6729707..2f138b4 100644 --- a/src/bin/fb_prepare_data.py +++ b/src/bin/fb_prepare_data.py @@ -1,5 +1,5 @@ #!/usr/bin/env python - +# noqa: D100 import argparse import glob import hashlib @@ -12,6 +12,9 @@ import yaml from ultralytics.data.converter import convert_coco +# TODO: fixme +# ruff: disable[D103] + def collapse_in_parent_dir(child): assert os.path.isdir(child) @@ -105,23 +108,32 @@ def prepare_coco_file(source_file, image_list, out): def main(): args_parse = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) - args_parse.add_argument("-i", "--input-data", dest="coco_data_root", - help="A directory that contains subdirectories for each COCO sub-datasets." - "Each sub-dataset contains a single json file named 'instances_default.json' " - "and the associated images" - ) - - args_parse.add_argument("-o", "--output-dir", dest="prepared_data_target", - help="The output compiled YOLO dataset joining together all sub-datasets in a single dataset with the structure:" - f"{out_structure}") - - args_parse.add_argument("-p", "--validation-proportion", dest="validation_proportion", - help="the proportion of data allocated to the validation set, based on md5 (pseudorandom)", - default=0.15) - - args_parse.add_argument("-f", "--force", dest="delete_target_before", - help="Delete output directory before, this avoids duplicating data etc", - action="store_true") + args_parse.add_argument( + "-i", "--input-data", dest="coco_data_root", + help=( + "A directory that contains subdirectories for each COCO sub-datasets." + "Each sub-dataset contains a single json file named 'instances_default.json' " + "and the associated images" + )) + + args_parse.add_argument( + "-o", "--output-dir", dest="prepared_data_target", + help=( + "The output compiled YOLO dataset joining together" + f" all sub-datasets in a single dataset with the structure: {out_structure}" + )) + + args_parse.add_argument( + "-p", "--validation-proportion", dest="validation_proportion", + help="the proportion of data allocated to the validation set, based on md5 (pseudorandom)", + default=0.15 + ) + + args_parse.add_argument( + "-f", "--force", dest="delete_target_before", + help="Delete output directory before, this avoids duplicating data etc", + action="store_true" + ) args = args_parse.parse_args() option_dict = vars(args) @@ -164,7 +176,8 @@ def main(): try: coco_files = [f for f in sorted(glob.glob(os.path.join( source_dir, "*.json")))] - assert len(coco_files) == 1, os.path.join(source_dir, "*.json") #,"Multiple label files, only supporting one" + #,"Multiple label files, only supporting one" + assert len(coco_files) == 1, os.path.join(source_dir, "*.json") convert_coco(labels_dir=source_dir, save_dir=tmp_dir, use_segments=True) @@ -187,7 +200,10 @@ def main(): for f in sorted(glob.glob(os.path.join(tmp_dir,OUT_COCO_CONVERTER, "*.txt"))): basename_sans_ext = os.path.splitext(os.path.basename(f))[0] - image_matches = [i for i in images if basename_sans_ext == os.path.splitext(os.path.basename(i))[0]] + image_matches = [ + i for i in images + if basename_sans_ext == os.path.splitext(os.path.basename(i))[0] + ] n_matches = len(image_matches) if n_matches == 0: logging.warning("Missing image: " + f) @@ -217,18 +233,32 @@ def main(): training_files[im_basename] = new_bn_se + ".jpg" logging.info(f"{im_basename} -> {subset}") - shutil.move(f, os.path.join(tmp_dir, OUT_COCO_CONVERTER, os.path.join(subset, new_bn_se + ".txt"))) - shutil.copy(im_path, os.path.join(tmp_dir, OUT_COCO_CONVERTER_IMAGES, subset, new_bn_se + ".jpg")) + shutil.move( + f, + os.path.join(tmp_dir, OUT_COCO_CONVERTER, os.path.join(subset, new_bn_se + ".txt")) + ) + shutil.copy( + im_path, + os.path.join(tmp_dir, OUT_COCO_CONVERTER_IMAGES, subset, new_bn_se + ".jpg") + ) if len(validation_files) == 0: logging.warning(f"No validation files for {d}") else: - prepare_coco_file(coco_files[0], validation_files, os.path.join(tmp_dir, OUT_COCO_CONVERTER, "val", f"{d}"+JSON_FILE_BASENAME)) + prepare_coco_file( + coco_files[0], + validation_files, + os.path.join(tmp_dir, OUT_COCO_CONVERTER, "val", f"{d}"+JSON_FILE_BASENAME) + ) if len(validation_files) == 0: logging.warning(f"No train files for {d}") else: - prepare_coco_file(coco_files[0], training_files, os.path.join(tmp_dir, OUT_COCO_CONVERTER, "train", f"{d}"+JSON_FILE_BASENAME)) + prepare_coco_file( + coco_files[0], + training_files, + os.path.join(tmp_dir, OUT_COCO_CONVERTER, "train", f"{d}"+JSON_FILE_BASENAME) + ) collapse_in_parent_dir(os.path.join(tmp_dir, OUT_COCO_CONVERTER)) collapse_in_parent_dir(os.path.join(tmp_dir, OUT_COCO_CONVERTER_IMAGES)) diff --git a/src/bin/fb_train.py b/src/bin/fb_train.py index 742fd8d..9a08906 100644 --- a/src/bin/fb_train.py +++ b/src/bin/fb_train.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -""" -``flatbug`` training script. +"""``flatbug`` training script. -The ``flatbug`` training script uses a lightly modified YOLO training interface (https://docs.ultralytics.com/modes/train/), with a few additional parameters. +The ``flatbug`` training script uses a lightly modified YOLO training interface +(https://docs.ultralytics.com/modes/train/), with a few additional parameters. See `scripts/experiments/best_train/default.yaml` for an example training config. @@ -32,7 +32,7 @@ # fixme, resume should continue on the same "run folder" -def main(): +def main(): # noqa: D103 DEFAULT_CONF = { "batch": 8, "imgsz": 1024, @@ -75,14 +75,14 @@ def main(): if not key.startswith("--"): raise ValueError(f"Unknown argument: {key}\n" + args_parse.format_help()) key = key.removeprefix("--") - if not key in DEFAULT_CONF: + if key not in DEFAULT_CONF: raise ValueError(f"Unknown argument: {key}\n" + args_parse.format_help()) if key.startswith("fb_"): - raise ValueError(f"Options starting with 'fb_' should be specified in the config file, not as command line arguments") + raise ValueError("Options starting with 'fb_' should be specified in the config file, not as command line arguments") # fixme: probably unsafe... try: value = eval(value) - except: + except Exception: pass cli_overrides[key] = value @@ -93,7 +93,8 @@ def main(): option_dict["data_dir"] = os.path.abspath(os.path.normpath(option_dict["data_dir"])) assert os.path.isdir(option_dict["data_dir"]), f'Directory {option_dict["data_dir"]} not found.' - # I think this should be fixed by resolving the path before passing it to the trainer and setting DATASETS_DIR in the scope of ultralytics.data.utils + # I think this should be fixed by resolving the path before passing + # it to the trainer and setting DATASETS_DIR in the scope of ultralytics.data.utils # (see https://github.com/ultralytics/ultralytics/blob/588bbbe4aed122e3d24353856484148bc5ef05ad/ultralytics/data/utils.py#L301) # #fixme issue when providing new dataset path, sill using old one?! see when i used pollen data # settings.update({'datasets_dir': option_dict["data_dir"]}) @@ -116,17 +117,24 @@ def main(): # Update data directory and resume flag from the command line overrides["data"] = os.path.join(option_dict["data_dir"], "data.yaml") # OBS: This is a *very* cursed hack around the fact that ultralytics have decided that you cannot change the settings at runtime. - ultralytics_data_utils.DATASETS_DIR = Path(option_dict["data_dir"]) # We technically only need to change it here, but I'll change it both places for consistency + # We technically only need to change it here, but I'll change it both places for consistency + ultralytics_data_utils.DATASETS_DIR = Path(option_dict["data_dir"]) ultralytics_utils.DATASETS_DIR = Path(option_dict["data_dir"]) if option_dict["resume"]: - assert os.path.isfile(overrides["model"]), f"Trying to resume from a model that does not seem to be a valid file: {overrides['model']}" + assert os.path.isfile(overrides["model"]), ( + f"Trying to resume from a model that does not seem to be a valid file: {overrides['model']}" + ) overrides["resume"] = overrides["model"] if (old_optim := overrides.pop("optimizer", None)) is not None: - logger.warning(f"Ignored optimizer '{old_optim}' - YOLO does not support changing the optimizer while training.") + logger.warning( + f"Ignored optimizer '{old_optim}' - " + "YOLO does not support changing the optimizer while training." + ) else: overrides["resume"] = False + # ruff: disable[F841] - TODO: fixme, we don't actually support multiple DDP # This is just a hack to fix this: https://github.com/pytorch/pytorch/issues/37377 - only relevant for DDP if isinstance(overrides["device"], (tuple, list)) : num_devices = len(overrides["device"]) @@ -134,6 +142,7 @@ def main(): num_devices = len(overrides["device"].split(",")) else: num_devices = 1 # Fixme: Is this a real case, or just a type error? + # ruff: enable[F841] if isinstance(overrides["device"], (tuple, list)) or (isinstance(overrides["device"], str) and len(overrides["device"].split(",")) > 1): os.environ['MKL_THREADING_LAYER'] = 'GNU' os.environ['OMP_NUM_THREADS'] = str(overrides["workers"]) diff --git a/src/bin/fb_tune.py b/src/bin/fb_tune.py index b2e3362..2fb561c 100644 --- a/src/bin/fb_tune.py +++ b/src/bin/fb_tune.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -""" -DEPRECATED. +"""DEPRECATED. """ import argparse @@ -10,7 +9,6 @@ import os import random import re -from typing import Dict, List, Optional, Tuple, Union import numpy as np import torch @@ -23,8 +21,7 @@ from flat_bug.coco_utils import fb_to_coco, filter_coco, split_annotations from flat_bug.config import DEFAULT_CFG, read_cfg, write_cfg from flat_bug.datasets import get_datasets -from flat_bug.eval_utils import (best_confidence_threshold, compare_groups, - f1_score) +from flat_bug.eval_utils import best_confidence_threshold, compare_groups, f1_score from flat_bug.predictor import Predictor # Fixed ranges for the parameters during tuning - should probably be configurable @@ -37,39 +34,39 @@ # Class for scaling and unscaling the parameters - ensures that the parameters visible to the optimizer have equal dynamic ranges [0, 1] class Scaler: - def __init__(self, ranges : Dict[str, Tuple[int, Union[int, float]]]): + def __init__(self, ranges : dict[str, tuple[int, int | float]]): self.ranges = ranges self.scales = [(r[1] - r[0]) for r in ranges.values()] self.offsets = [r[0] for r in ranges.values()] - def scale(self, params : Union[list, np.ndarray]) -> list: - """ - Scales the parameters between 0 and 1. + def scale(self, params : list | np.ndarray) -> list: + """Scales the parameters between 0 and 1. scale(x) = (x - x_min) / (x_max - x_min) Args: - params (Union[list, np.ndarray]): The parameters to scale + params: The parameters to scale Returns: list: The scaled parameters + """ if not isinstance(params, list): params = params.tolist() value = [(p - o) / s for p, o, s in zip(params, self.offsets, self.scales)] return value - def unscale(self, params : Union[list, np.ndarray]) -> list: - """ - Unscales values between 0 and 1 to the original parameter ranges. + def unscale(self, params : list | np.ndarray) -> list: + """Unscales values between 0 and 1 to the original parameter ranges. unscale(x) = x * (x_max - x_min) + x_min Args: - params (Union[list, np.ndarray]): The scaled parameters, i.e. values between 0 and 1 + params: The scaled parameters, i.e. values between 0 and 1 Returns: list: The unscaled parameters + """ if not isinstance(params, list): params = params.tolist() @@ -77,7 +74,7 @@ def unscale(self, params : Union[list, np.ndarray]) -> list: return value class Tuner(Predictor): - def __init__(self, loader : torch.utils.data.DataLoader, default_cfg : dict, scale_before : Union[float, int], file_path : Optional[str], *args, **kwargs): + def __init__(self, loader : torch.utils.data.DataLoader, default_cfg : dict, scale_before : float | int, file_path : str | None, *args, **kwargs): self.loader = loader self.default_cfg = default_cfg self.scale_before = scale_before @@ -102,8 +99,7 @@ def __init__(self, loader : torch.utils.data.DataLoader, default_cfg : dict, sca self._init_score_threshold = self.SCORE_THRESHOLD def evaluate(self) -> float: - r""" - Evaluates the model on the dataset(s) and returns the cost. + r"""Evaluates the model on the dataset(s) and returns the cost. Cost is defined as the average of one minus the intersection over union (IoU) for all matches between labels and predicted instances. This includes both matched predictions, unmatched predictions (false positives), and unmatched labels (false negatives). Including false positives and negatives ensures that the model is penalized for missing instances as well as for predicting instances that are not present in the ground truth. @@ -120,8 +116,10 @@ def evaluate(self) -> float: False positives or negatives lead to either :math:`L_{i,p}` or :math:`P_{i,q}` being empty, which will result in a cost of 1 for that instance. A prediction perfectly matching a ground truth label will result in a cost of 0 for that instance. In reality, the cost is calculated as 1 minus the average IoU for each instance, this is equivalent to the above formula, but the code is a bit cleaner. + Returns: float: The cost of the model on the dataset(s), where 0 corresponds to exactly finding and matching all ground truth instances, and 1 corresponds to not finding any instances. + """ eval_results = {} for data in tqdm(self.loader, dynamic_ncols=True, leave=False, desc="Evaluating model "): @@ -185,8 +183,7 @@ def evaluate(self) -> float: return cost, c_f1, c_iou def update_score_threshold(self, cost, threshold): - """ - Potentially updates the score threshold based on the cost and the current best score threshold. + """Potentially updates the score threshold based on the cost and the current best score threshold. An update is applied if: - There are no prior costs @@ -260,10 +257,10 @@ def __repr__(self) -> str: class AnnotatedDataset(torch.utils.data.IterableDataset): FILES_PER_DATASET_PER_ITER = 1 - def __init__(self, files : list, annotations : dict, datasets_per_iter : Optional[int] = None, files_per_iter : Optional[int] = None): + def __init__(self, files : list, annotations : dict, datasets_per_iter : int | None = None, files_per_iter : int | None = None): self.files = [] # Create a dictionary with the base name of the images as keys and the annotations as values - self.annotations = split_annotations(filter_coco(json.load(open(annotations, "r")), area=32**2), strip_directories=True) + self.annotations = split_annotations(filter_coco(json.load(open(annotations)), area=32**2), strip_directories=True) # Add the files to the dataset if they are found in the annotations [self.files.append(file) if os.path.basename(file) in self.annotations else logging.warning(f"File {file} not found in the annotations!") for file in files] del files @@ -279,7 +276,7 @@ def __init__(self, files : list, annotations : dict, datasets_per_iter : Optiona self.DATASETS_PER_ITER = len(self.datasets) else: self.DATASETS_PER_ITER = min(datasets_per_iter, len(self.datasets)) - if not files_per_iter is None: + if files_per_iter is not None: self.FILES_PER_DATASET_PER_ITER = files_per_iter def __getitem__(self, idx): diff --git a/src/flat_bug/__init__.py b/src/flat_bug/__init__.py index deda736..5f20dcd 100644 --- a/src/flat_bug/__init__.py +++ b/src/flat_bug/__init__.py @@ -1,23 +1,23 @@ +# noqa: D104 import logging -import urllib.error +import os import urllib.parse import urllib.request -from typing import Optional -import os -from tqdm import tqdm +from tqdm.auto import tqdm REMOTE_REPOSITORY = "https://anon.erda.au.dk/share_redirect/Bb0CR1FHG6/" # GUI access: https://anon.erda.au.dk/cgi-sid/ls.py?share_id=Bb0CR1FHG6 # Thanks to: https://stackoverflow.com/a/53877507/19104786 -class DownloadProgressBar(tqdm): - def update_to(self, b : int=1, bsize : int=1, tsize : Optional[int]=None): +class DownloadProgressBar(tqdm): # noqa: D101 + def update_to(self, b : int=1, bsize : int=1, tsize : int | None=None): # noqa: D102 if tsize is not None: self.total = tsize self.update(b * bsize - self.n) -def download_from_repository(url : str, output_path : Optional[str]=None, strict : bool=True, progress : bool=True): +def download_from_repository(url : str, output_path : str | None=None, strict : bool=True, progress : bool=True): + """Download a file from the flatbug "repository.""" if output_path is None: output_path = url url = urllib.parse.quote(urllib.parse.urljoin(REMOTE_REPOSITORY, url), safe="/:") @@ -52,6 +52,6 @@ def download_from_repository(url : str, output_path : Optional[str]=None, strict logger = logging.getLogger(__name__) -def set_log_level(level): +def set_log_level(level): # noqa: D103 logger.setLevel(level) logger.info(f'Log level set to {level}') diff --git a/src/flat_bug/augmentations.py b/src/flat_bug/augmentations.py index 00be274..d1d2f5c 100644 --- a/src/flat_bug/augmentations.py +++ b/src/flat_bug/augmentations.py @@ -1,6 +1,7 @@ +"""Augmentations used for flatbug.""" import math import random -from typing import Dict, List, Optional, Self, Tuple, Union +from typing import cast, overload import cv2 import numpy as np @@ -20,39 +21,39 @@ def segment2box( width : int=640, height : int=640 ) -> np.ndarray: - """ - Convert 1 segment label to 1 box label, applying inside-image constraint, i.e. (xy1, xy2, ...) to (xyxy). + """Convert 1 segment label to 1 box label, applying inside-image constraint, i.e. (xy1, xy2, ...) to (xyxy). Args: - segment (`torch.Tensor`): the segment label - width (`int`, optional): OBS: Unused. The width of the image. Defaults to 640. - height (`int`, optional): OBS: Unused. The height of the image. Defaults to 640. + segment: the segment label + width: OBS: Unused. The width of the image. Defaults to 640. + height: OBS: Unused. The height of the image. Defaults to 640. Returns: - out (`np.ndarray`): the minimum and maximum x and y values of the segment. + The minimum and maximum x and y values of the segment (xyxy). + """ x, y = segment.T # segment xy - return np.array([x.min(), y.min(), x.max(), y.max()], dtype=segment.dtype) # xyxy + return np.array([x.min(), y.min(), x.max(), y.max()], dtype=segment.dtype) # type: ignore def apply_segments( segments : np.ndarray, M : np.ndarray - ) -> Tuple[np.ndarray, np.ndarray]: - """ - Apply affine to segments and generate new bboxes from segments. + ) -> tuple[np.ndarray, np.ndarray]: + """Apply affine to segments and generate new bboxes from segments. Args: - segments (`np.ndarray`): list of segments, [num_samples, 500, 2]. - M (`np.ndarray`): affine matrix. + segments: list of segments, [num_samples, 500, 2]. + M: affine matrix. Returns: - out (`Tuple[np.ndarray, np.ndarray]`): + out: * new_segments (`np.ndarray`): list of segments after affine, [num_samples, 500, 2]. * new_bboxes (`np.ndarray`): bboxes after affine, [N, 4]. + """ n, num = segments.shape[:2] if n == 0: - return [], segments + return np.empty(shape=(0,)), segments xy = np.ones((n * num, 3), dtype=segments.dtype) segments = segments.reshape(-1, 2) @@ -70,8 +71,19 @@ def low_res_inpaint( mask : np.ndarray, scale : int=6 ) -> np.ndarray: - """ - Performs inpainting on a low-resolution version of the image, and then copies the upsampled inpainted image back into the original image. + """Perform low resolution inpainting in-place. + + In-painting is done on a low-resolution copy of the image, + and then copies the upsampled inpainted image back into the original image. + + Args: + img: Image to be inpainted. + mask: Mask to specify inpainting area. + scale: Scale of the low-resolution copy used for inpainting. + + Returns: + The original (modified) instance. + """ # Create a low-res version of the image and mask lr_img = cv2.resize(img, (img.shape[1] // scale, img.shape[0] // scale)) @@ -87,32 +99,33 @@ def low_res_inpaint( def telea_inpaint_polys( img : np.ndarray, - polys : List[np.ndarray], - exclude_polys : Optional[List[np.ndarray]]=None, - downscale_factor : Union[int, float]=6, + polys : list[np.ndarray], + exclude_polys : list[np.ndarray] | None=None, + downscale_factor : int | float=6, **kwargs ) -> np.ndarray: - """ - Mutably inpaints the polygons in an image using the Fast Marching method by Alexandru Telea. + """Mutably inpaints the polygons in an image using the Fast Marching method by Alexandru Telea. - The inpainting algorithm is performed on a downsampled version of the image to speed up the process, and the inpainted results are then upsampled and pasted back into the original image. + The inpainting algorithm is performed on a downsampled version of the image to speed up the process, + and the inpainted results are then upsampled and pasted back into the original image. Args: - img (`np.ndarray`): The image to inpaint. - polys (`List[np.ndarray]`): A list of polygons to inpaint. - exclude_polys (`Optional[List[np.ndarray]]`, optional): A list of polygons to exclude from inpainting. Defaults to None. - downscale_factor (`Union[int, float]`, optional): The factor by which to downscale the image before inpainting. Defaults to 6. + img: The image to inpaint. + polys: A list of polygons to inpaint. + exclude_polys: A list of polygons to exclude from inpainting. Defaults to None. + downscale_factor: The factor by which to downscale the image before inpainting. Defaults to 6. **kwargs: Additional keyword arguments to pass to `cv2.drawContours`. Returns: - out (`np.ndarray`): The inpainted image. + The inpainted image. + """ # Type checking and sanitizing check_types(img, np.ndarray) if not ((img.ndim == 3 and img.shape[2] < 5) or img.ndim == 2): raise ValueError(f"img must be a 2D or 3D numpy array, of shape (H, W) or (H, W, C), got shape {img.shape}") check_types(polys, [list, np.ndarray]) - check_types(exclude_polys, ([list, np.ndarray], None)) + check_types(exclude_polys, ([list, np.ndarray], None)) # type: ignore if exclude_polys is None: exclude_polys = [] check_types(downscale_factor, (int, float)) @@ -170,16 +183,26 @@ def telea_inpaint_polys( # Return the inpainted image (not necessary, as the inpainting is done in-place) return img +@overload def inpaint_pad( - array : Union[torch.Tensor, np.ndarray], - padding : Union[int, Tuple[int, int], Tuple[int, int, int, int]] - ) -> Union[torch.Tensor, np.ndarray]: + array : torch.Tensor, + padding : int | tuple[int, int] | tuple[int, int, int, int] + ) -> torch.Tensor: ... +@overload +def inpaint_pad( + array : np.ndarray, + padding : int | tuple[int, int] | tuple[int, int, int, int] + ) -> np.ndarray: ... +def inpaint_pad( # noqa: D103 + array : torch.Tensor | np.ndarray, + padding : int | tuple[int, int] | tuple[int, int, int, int] + ) -> torch.Tensor | np.ndarray: # Ensure padding is a tuple (pad_top, pad_bottom, pad_left, pad_right) if isinstance(padding, int): padding = (padding, padding, padding, padding) elif isinstance(padding, (tuple, list)) and len(padding) == 2: - padding = list(padding) - padding = tuple(padding + padding) + lp = list(padding) + padding = cast(tuple[int, int, int, int], tuple(lp + lp)) elif isinstance(padding, (tuple, list)) and len(padding) == 4: pass else: @@ -236,20 +259,21 @@ def inpaint_pad( return padded_image -class InpaintPad: - def __init__(self, padding : Union[int, Tuple[int, int], Tuple[int, int, int, int]]): +class InpaintPad: # noqa: D101 + def __init__(self, padding : int | tuple[int, int] | tuple[int, int, int, int]): # noqa: D107 self.padding = padding - def __call__(self, tensor : torch.Tensor) -> torch.Tensor: + def __call__(self, tensor : torch.Tensor) -> torch.Tensor: # noqa: D102 return inpaint_pad(tensor, self.padding) -def remove_instances( - labels : Dict, - area_thr : Union[float, int]=1, - max_targets : Optional[int]=1000, +def remove_instances( # noqa: D103 + labels : dict, + area_thr : float | int=1, + max_targets : float | int | None=1000, min_size : int=0 - ) -> Dict: - instances : Instances = labels.pop("instances") + ) -> dict: + instances = cast(Instances, labels.pop("instances")) + assert instances.segments is not None imsize = labels["img"].shape[:2][::-1] if instances.normalized: @@ -272,7 +296,7 @@ def remove_instances( for i, s in enumerate(instances.segments): # Initiate overlap using bounding box x, y, w, h = bboxes[i] - bbox = box(x - w/2, y - h/2, x + w/2, y + h/2) + bbox = box(x - w/2, y - h/2, x + w/2, y + h/2) # type: ignore bbox_iarea = bbox.intersection(image_bbox).area area_ratios[i] = (bbox_iarea + eps) / (bbox.area + eps) if bbox.area > 0 and bbox_iarea > 0 else 0 if area_ratios[i] < area_thr: @@ -295,7 +319,7 @@ def remove_instances( ) if max_targets is not None and np.sum(valid) > max_targets: - n_remove, n_keep = np.sum(valid) - max_targets, max_targets + n_remove, n_keep = int(np.sum(valid) - max_targets), int(max_targets) valid[valid] &= np.random.permutation(np.array([True] * n_keep + [False] * n_remove, dtype=bool)) # here, we paint the edge cases (partially outside the image, using telea inpainting), @@ -352,7 +376,6 @@ def remove_instances( # x, y, w, h = bbox # x0, y0, x1, y1 = int(x - w / 2), int(y - h / 2), int(x + w / 2), int(y + h / 2) # cv2.rectangle(labels["img"], (x0, y0), (x1, y1), (255, 0, 0), 2) - instances.segments = instances.segments[valid_i] instances._bboxes.bboxes = bboxes[valid_i] instances.clip(*imsize) @@ -362,10 +385,10 @@ def remove_instances( # logger.info(labels) return labels -def scale_labels( - labels : Dict, +def scale_labels( # noqa: D103 + labels : dict, scale : float - ) -> Dict: + ) -> dict: orig_shape = labels["img"].shape[:2] # Scale the image labels["img"] = cv2.resize(labels["img"], (0, 0), fx=scale, fy=scale) @@ -377,20 +400,19 @@ def scale_labels( labels["instances"].denormalize(*new_shape[::-1]) return labels -class FlatBugRandomPerspective(RandomPerspective): +class FlatBugRandomPerspective(RandomPerspective): # noqa: D101 fill_value = (0, 0, 0) - def __init__(self, imgsz : int, *args, **kwargs): + def __init__(self, imgsz : int, *args, **kwargs): # noqa: D107 super().__init__(*args, **kwargs) self.imgsz = imgsz def affine_transform( - self : Self, + self, img : np.ndarray, - border : Tuple[int, int] - ) -> Tuple[np.ndarray, np.ndarray, float]: + border : tuple[int, int] + ) -> tuple[np.ndarray, np.ndarray, float]: """Center.""" - self.scale = self.imgsz / max(img.shape), 1 # fime hardcoded C = np.eye(3, dtype=np.float32) @@ -438,11 +460,11 @@ def affine_transform( return img, M, s def __call__(self, labels : dict): - """ - Affine images and targets. + """Affine images and targets. Args: - labels (dict): a dict of `bboxes`, `segments`, `keypoints`. + labels: a dict of `bboxes`, `segments`, `keypoints`. + """ if self.pre_transform and "mosaic_border" not in labels: labels = self.pre_transform(labels) @@ -466,6 +488,7 @@ def __call__(self, labels : dict): bboxes = self.apply_bboxes(instances.bboxes, M) segments = instances.segments + assert segments is not None keypoints = instances.keypoints # Update bboxes if there are segments. if len(segments): @@ -496,10 +519,12 @@ def __call__(self, labels : dict): return labels class Crop: + """Abstact crop-related augmentation.""" + bg_fill = (0, 0, 0) min_size = 0 # px - def __init__(self, imsize : Union[int, Tuple[int, int], List[int], np.ndarray]): + def __init__(self, imsize : int | tuple[int, int] | list[int] | np.ndarray): # noqa: D107 if isinstance(imsize, int): self._imsize = (imsize, imsize) elif isinstance(imsize, (tuple, list, np.ndarray)): @@ -513,14 +538,14 @@ def __init__(self, imsize : Union[int, Tuple[int, int], List[int], np.ndarray]): self._imsize = tuple([int(i) for i in self._imsize]) self.xsize, self.ysize = self._imsize - def crop_image( - self : Self, - labels : Dict, + def crop_image( # noqa: D102 + self, + labels : dict, start_x : int, start_y : int, size_x : int, size_y : int - ) -> Dict: + ) -> dict: img = labels["img"] orig_shape = img.shape h, w = img.shape[:2] @@ -557,7 +582,7 @@ def crop_image( # Fix label positions - instances : Instances = labels.pop("instances") + instances = cast(Instances, labels.pop("instances")) if instances._bboxes.format != "xywh": instances.convert_bbox(format="xywh") if instances.normalized: @@ -569,7 +594,8 @@ def crop_image( # positions in the cropped image instances._bboxes.add([x_offset, y_offset, 0, 0]) - + + assert instances.segments is not None for s in instances.segments: s[:, 0] += x_offset s[:, 1] += y_offset @@ -579,10 +605,14 @@ def crop_image( return labels def __call__(self, x): + """Abstract function. + + Should be implemented in subclasses. + """ raise NotImplementedError("This method should be implemented in a subclass") -class CenterCrop(Crop): - def __call__(self, labels : Dict) -> Dict: +class CenterCrop(Crop): # noqa: D101 + def __call__(self, labels : dict) -> dict: # noqa: D102 h, w = labels["img"].shape[:2] start_x = (w - self.xsize) // 2 @@ -590,11 +620,11 @@ def __call__(self, labels : Dict) -> Dict: return self.crop_image(labels, start_x, start_y, self.xsize, self.ysize) -class RandomCrop(Crop): - def __init__(self, *args, **kwargs): +class RandomCrop(Crop): # noqa: D101 + def __init__(self, *args, **kwargs): # noqa: D107 super().__init__(*args, **kwargs) - def __call__(self, labels : Dict) -> Dict: + def __call__(self, labels : dict) -> dict: # noqa: D102 # Get the initial image to target crop size ratio h, w = labels["img"].shape[:2] target_source_ratio_h = self.ysize / h @@ -617,7 +647,8 @@ def __call__(self, labels : Dict) -> Dict: else: target_size = max(int(w * scale), int(h * scale)) target_xsize, target_ysize = target_size, target_size - # Reset the scale such that when the labels/image are scaled after cropping the size is self.xsize, self.ysize (assuming these are equal) + # Reset the scale such that when the labels/image are + # scaled after cropping the size is self.xsize, self.ysize (assuming these are equal) scale = self.xsize / target_xsize # Calculate possible crop start positions @@ -639,42 +670,47 @@ def __call__(self, labels : Dict) -> Dict: return labels class FixInstances: + """A callable class that removes instances that are too small or which overlap less than a certain threshold with the image.""" + def __init__( self, - area_thr : Union[float, int], - max_targets : Optional[int], + area_thr : float | int, + max_targets : int | float | None, min_size : int ): - """" - A callable class that removes instances that are too small or which overlap less than a certain threshold with the image. + """"Instantiate. Args: - area_thr (`Union[float, int]`): The minimum proportion of the instance that must be within the image in order for it to be kept. - max_targets (`Optional[int]`): The maximum number of instances to keep. If there are more instances than this, a random subset of instances will be kept. If `None`, all instances will be kept. - min_size (`int`): The minimum size of the bounding box of the instance. Instances with a width or height less than this value will be removed. + area_thr: The minimum proportion of the instance that must be within the image in order for it to be kept. + max_targets: The maximum number of instances to keep. If there are more instances than this, + a random subset of instances will be kept. If `None`, all instances will be kept. + min_size: The minimum size of the bounding box of the instance. + Instances with a width or height less than this value will be removed. + """ self.area_thr = area_thr self.max_targets = max_targets if max_targets is None or max_targets > 0 else None self.min_size = min_size def __call__(self, labels : dict) -> dict: - """ - Performs instance fixing. + """Fix instances. Args: - labels (`dict`): Dictionary containing the instances. + labels: Dictionary containing the instances. + Returns: - out (`dict`): A dictionary containing the updated instances. + out: A dictionary containing the updated instances. + """ return remove_instances(labels, area_thr=self.area_thr, max_targets=self.max_targets, min_size=self.min_size) -class RandomColorInv(object): +class RandomColorInv: # noqa: D101 def __init__(self, p : float=0.5): - """ - Invert the colors of an image with a probability p. + """Invert the colors of an image with a probability p. Args: - p (`float`, optional): probability of inverting the colors. Defaults to 0.5 + p: probability of inverting the colors. Defaults to 0.5 + """ if p < 0: logger.warning("p should be in [0,1], got", p, "setting to 0") @@ -684,7 +720,7 @@ def __init__(self, p : float=0.5): p = 1 self.p = 1 - p - def __call__(self, labels : Dict) -> Dict: + def __call__(self, labels : dict) -> dict: # noqa: D102 img = labels['img'] if random.uniform(0, 1) > self.p: assert img.dtype == np.uint8 diff --git a/src/flat_bug/coco_utils.py b/src/flat_bug/coco_utils.py index 17aa611..37f0b91 100644 --- a/src/flat_bug/coco_utils.py +++ b/src/flat_bug/coco_utils.py @@ -1,9 +1,6 @@ -""" -Evaluation functions for FlatBug datasets. -""" +"""Evaluation functions for FlatBug datasets.""" import os -from typing import Dict, List, Optional, Tuple, Union import cv2 import numpy as np @@ -82,20 +79,19 @@ def fb_to_coco( - d: Dict, - coco: Dict - ) -> Dict: - """ - Converts a FlatBug dataset to a COCO dataset. + d: dict, + coco: dict + ) -> dict: + """Convert a FlatBug dataset to a COCO dataset. Args: - d (`dict`): FlatBug dataset. - coco (`dict`): An instantiated COCO dataset or an empty dictionary. + d: FlatBug dataset. + coco: An instantiated COCO dataset or an empty dictionary. Returns: - out (`dict`): COCO dataset. - """ + COCO dataset. + """ if len(coco) == 0: image_id = 1 object_id_offset = 0 @@ -134,10 +130,13 @@ def fb_to_coco( else: object_id_offset = coco["annotations"][-1]["id"] + 1 - boxes, contours, confs, classes, scales = d["boxes"], d["contours"], d["confs"], d["classes"], d["scales"] - identifier, image_path = d["identifier"], d["image_path"] - image_width, image_height, mask_width, mask_height = d["image_width"], d["image_height"], d["mask_width"], d[ - "mask_height"] + # classes, scales = d["classes"], d["scales"] + boxes, contours, confs = d["boxes"], d["contours"], d["confs"] + # identifier = d["identifier"] + image_path = d["image_path"] + image_width, image_height, mask_width, mask_height = ( + d["image_width"], d["image_height"], d["mask_width"], d["mask_height"] + ) # Image image = { @@ -154,7 +153,8 @@ def fb_to_coco( # Boxes, contours, confs, classes, scales for i in range(len(boxes)): - box, contour, conf, class_, scale = boxes[i], contours[i], confs[i], classes[i], scales[i] + box, contour, conf = boxes[i], contours[i], confs[i] + # class_, scale = classes[i], scales[i] x1, y1, x2, y2 = box x,y,w,h = x1, y1, x2 - x1, y2 - y1 box=[x,y,w,h] @@ -180,29 +180,29 @@ def fb_to_coco( return coco -def format_contour(c : List) -> np.ndarray: - """ - Formats a contour to the OpenCV format. +def format_contour(c : list) -> np.ndarray: + """Format a contour to the OpenCV format. Args: - c (`list`): Contour. + c: Contour. Returns: - out (`np.ndarray`): Formatted contour. + Formatted contour. + """ c = c[0] return np.array([[c[i], c[i + 1]] for i in range(0, len(c), 2)], dtype=np.int32) def contour_bbox(c: np.ndarray) -> np.ndarray: - """ - Calculates the bounding box of a contour. + """Calculate the bounding box of a contour. Args: - c (`np.ndarray`): Contour. + c: Contour. Returns: - oyut (`np.ndarray`): Bounding box. + oyut: Bounding box. + """ return np.array([c[:, 0].min(), c[:, 1].min(), c[:, 0].max(), c[:, 1].max()]) @@ -210,16 +210,16 @@ def contour_bbox(c: np.ndarray) -> np.ndarray: def split_annotations( coco: dict, strip_directories: bool = True - ) -> Dict[str, dict]: - """ - Splits COCO annotations by image ID. + ) -> dict[str, list]: + """Split COCO annotations by image ID. Args: - coco (dict): COCO dataset. - strip_directories (`bool`, optional): Flag to indicate whether only the basename of the images should be included in the result. Defaults to True. + coco: COCO dataset. + strip_directories: Flag to indicate whether only the basename of the images should be included in the result. Defaults to True. Returns: - out (`Dict[str, dict]`): Dict of COCO datasets, split by image ID and keyed by image name. + Dict of COCO datasets, split by image ID and keyed by image name. + """ img_id = np.array([i["image_id"] for i in coco["annotations"]]) ids = np.unique(np.array([i["id"] for i in coco["images"]])) @@ -240,28 +240,28 @@ def split_annotations( return result -def annotations_2_contours(annotations: Dict[str, dict]) -> Dict[str, List[np.array]]: - """ - Converts COCO annotations to contours. +def annotations_2_contours(annotations: dict[str, dict]) -> dict[str, list[np.ndarray]]: + """Convert COCO annotations to contours. Args: - annotations (`Dict[str, dict]`): COCO annotations. + annotations: COCO annotations. Returns: - out (`Dict[str, List[np.array]]`): Contours. + Contours. + """ return {k: [format_contour(i["segmentation"]) for i in v] for k, v in annotations.items()} -def contour_area(c: np.ndarray) -> np.ndarray: - """ - Calculates the area of a contour. +def contour_area(c: np.ndarray): + """Calculate the area of a contour. Args: - c (`np.ndarray`): Contour of shape (n, 2). + c: Contour of shape (n, 2). Returns: - out (`np.array[np.int32]`): Scalar area of the contour of shape (1,). + Scalar area of the contour of shape (1,). + """ # return cv2.contourArea(c) min_xy = c.min(axis=0) @@ -272,15 +272,15 @@ def contour_area(c: np.ndarray) -> np.ndarray: return np.sum(mask, dtype=np.int64) -def annotations_to_numpy(annotations: List[Dict[str, Union[int, List[int]]]]) -> Tuple[np.ndarray, np.ndarray]: - """ - Converts COCO annotations to NumPy arrays. +def annotations_to_numpy(annotations: list[dict[str, list[int]]]) -> tuple[np.ndarray, list[np.ndarray]]: + """Convert COCO annotations to NumPy arrays. Args: - annotations (`List[Dict[str, Union[int, List[int]]]]`): COCO annotations. + annotations: COCO annotations. Returns: - out (`Tuple[np.ndarray, np.ndarray]`): Bounding boxes and contours. + Bounding boxes and contours. + """ contours = [format_contour(i["segmentation"]) for i in annotations] bboxes = np.array([contour_bbox(c) for c in contours]) @@ -288,21 +288,21 @@ def annotations_to_numpy(annotations: List[Dict[str, Union[int, List[int]]]]) -> def filter_coco( coco : dict, - confidence : Optional[float]=None, - area : Optional[int]=None, + confidence : float | None=None, + area : int | None=None, verbose : bool=False ) -> dict: - """ - Filters COCO annotations by confidence. + """Filter COCO annotations by confidence. Args: - coco (`dict`): COCO dataset. - confidence (`Optional[float]`, optional): Confidence threshold. Defaults to None; no threshold. - area (`Optional[int]`, optional): Area threshold. Defaults to None; no threshold. - verbose (`bool`, optional): Verbose mode. Defaults to False. + coco: COCO dataset. + confidence: Confidence threshold. Defaults to None; no threshold. + area: Area threshold. Defaults to None; no threshold. + verbose: Verbose mode. Defaults to False. Returns: - out (`dict`): Filtered COCO dataset. + Filtered COCO dataset. + """ filtered_annotations = [] for a in coco["annotations"]: diff --git a/src/flat_bug/config.py b/src/flat_bug/config.py index ad2f115..851ae0f 100644 --- a/src/flat_bug/config.py +++ b/src/flat_bug/config.py @@ -1,6 +1,9 @@ +"""Configuration submodule for flatbug.""" import os from collections import OrderedDict -from typing import Any, Iterable, List, Union +from collections.abc import Iterable +from pathlib import Path +from typing import Any import yaml @@ -9,6 +12,7 @@ # Add support for OrderedDict in PyYAML yaml.add_representer(OrderedDict, lambda dumper, data: dumper.represent_dict(data.items()), Dumper=yaml.SafeDumper) +# ruff: disable[E501] CFG_PARAMS = [ "SCORE_THRESHOLD", "OVERLAP_THRESHOLD", @@ -68,28 +72,30 @@ "TILE_SIZE": 1024, "BATCH_SIZE": 16 } +# ruff: enable[E501] def get_type_def( obj : Any, tuple_list_interchangeable : bool=False - ) -> Union[Any, List[Any]]: - """ - Generates a dynamic type definition for an object. + ) -> Any | list[Any]: + """Generate a dynamic type definition for an object. The type definition schema is defined like this: - - If the object is a tuple or a list, the first element is the type of the object, and the second element is a list of type definitions for the elements of the object. + - If the object is a tuple or a list, the first element is the type of the object, + and the second element is a list of type definitions for the elements of the object. - If the object is not a tuple or a list, the type definition is the type of the object. For example; - the type definition for the object `(1, "A", True)` would be `[tuple, [int, str, bool]]`. - the type definition for the object `[[2, "B"], [3, "C"]]` would be `[list, [[list, [int, str]], [list, [int, str]]]]`. - Parameters: - obj (`Any`): The object to generate a type definition for. - tuple_list_interchangeable (`bool`, optional): If True, tuples and lists are considered interchangeable. Defaults to False. + Args: + obj: The object to generate a type definition for. + tuple_list_interchangeable: If True, tuples and lists are considered interchangeable. Defaults to False. Returns: - out (`Union[Any, List[Any]]`): The type definition for the object. + The type definition for the object. + """ if isinstance(obj, (tuple, list)): otype = type(obj) @@ -102,56 +108,73 @@ def get_type_def( def check_types( value : Any, - expected_type : Union[List[Any], Iterable[type], type], + expected_type : list[Any] | Iterable[type] | type, key : str="", strict : bool=True ) -> bool: - """ - Recursively check if the type of a value matches the expected type. + """Recursively check if the type of a value matches the expected type. - If the expected type is a list, the first element is the type of the value, and the second element is a list of types that the elements of the value match, a single type that all elements should match or a tuple/type of types that all elements should match any of. + If the expected type is a list, the first element is the type of the value, + and the second element is a list of types that the elements of the value match, + a single type that all elements should match or a tuple/type of types that all elements should match any of. Args: - value (`Any`): The value to check. - expected_type (`Union[List[Any], Iterable[type], type]`): The expected type of the value. - key (`str`, optional): Name of the value to use in error messages. Defaults to "\\". - strict (`bool`, optional): If True, raise an error if the check fails. Defaults to True. + value: The value to check. + expected_type: The expected type of the value. + key: Name of the value to use in error messages. Defaults to "". + strict: If True, raise an error if the check fails. Defaults to True. Returns: - out (`bool`): True if the check passes, and False if strict is False and the check fails. Raises an error otherwise. + True if the check passes, and False if strict is False and the check fails. Raises an error otherwise. Raises: ValueError: If the expected type list does not have exactly 2 elements. TypeError: If the expected type is not a list, an iterable or a 'type' object. TypeError: If the number of types in the list does not match the number of items in the value. TypeError: If the value does not match the expected type. + """ try: # If expected type is a list, recursively check the types of the elements if isinstance(expected_type, list): # Check that an expected type has been supplied for both the value and its elements if len(expected_type) != 2: - raise ValueError(f"Expected type list must have exactly 2 elements, got {len(expected_type)} for key: {key}.") + raise ValueError( + f"Expected type list must have exactly 2 elements, " + f"got {len(expected_type)} for key: {key}." + ) # Check that the value matches the expected type check_types(value, expected_type[0], key, strict) - # If the expected type of the elements is a list, each element of the value should match the corresponding element of the expected type list + # If the expected type of the elements is a list, + # each element of the value should match the corresponding element of the expected type list if isinstance(expected_type[1], list): # Check that the number of types in the list matches the number of items in the value if len(value) != len(expected_type[1]): - raise TypeError(f"Expected number of types ({len(expected_type[1])}) does not match number of items in value ({len(value)}) for key: {key}.") - # Check that each item in the value matches the corresponding type in the expected type list + raise TypeError( + f"Expected number of types ({len(expected_type[1])}) " + f"does not match number of items in value ({len(value)}) for key: {key}." + ) + # Check that each item in the value matches + # the corresponding type in the expected type list for item, et in zip(value, expected_type[1]): check_types(item, et, key, strict) - # If the expected type of the elements is a single type, each element of the value should match the expected type + # If the expected type of the elements is a single type, + # each element of the value should match the expected type elif isinstance(expected_type[1], type): for item in value: check_types(item, expected_type[1], key, strict) - # If the expected type of the elements is a tuple, each element of the value should match any of the types in the tuple + # If the expected type of the elements is a tuple, + # each element of the value should match any of the types in the tuple elif isinstance(expected_type[1], tuple): check_types(value, expected_type[1], key, strict) - # If the expected type of the elements is an iterable, the value should be an iterable and each element of the value should match the corresponding type in the expected type iterable + # If the expected type of the elements is an iterable, + # the value should be an iterable and each element of the value + # should match the corresponding type in the expected type iterable elif hasattr(expected_type[1], "__iter__") and hasattr(expected_type[1], "__len__"): - assert len(expected_type[1]) == len(value), f"Expected number of types ({len(expected_type[1])}) does not match number of items in value ({len(value)}) for key: {key}." + assert len(expected_type[1]) == len(value), ( + f"Expected number of types ({len(expected_type[1])}) " + f"does not match number of items in value ({len(value)}) for key: {key}." + ) errors = [] for item, et in zip(value, expected_type[1]): try: @@ -161,25 +184,31 @@ def check_types( if len(errors) != 0: raise TypeError("\n - ".join(errors)) else: - raise TypeError(f"Invalid expected type. Expected 'list', 'type', 'tuple' or an iterable got {type(expected_type[1])} for key: {key}.") + raise TypeError( + "Invalid expected type. Expected 'list', 'type', 'tuple' or an iterable " + f"got {type(expected_type[1])} for key: {key}." + ) # If the expected type is an iterable, check if the value is an instance of any of the types in the iterable elif hasattr(expected_type, "__iter__") and not isinstance(expected_type, type): if not any([check_types(value, e, key, False) for e in expected_type]): - raise TypeError(f"Expected one of {et}, got {type(value)} for key: {key}.") + raise TypeError(f"Expected one of {expected_type}, got {type(value)} for key: {key}.") # If the expected type is a 'type' object, check if the value is an instance of the type elif isinstance(expected_type, type): if not isinstance(value, expected_type): raise TypeError(f"Expected {expected_type}, got {type(value)} for key {key}.") # If the expected type is None, check if the value is None elif expected_type is None: - if not value is None: + if value is not None: raise TypeError(f"Expected None, got {type(value)} for key: {key}.") # If the expected type is typing.Any, pass everything elif expected_type is Any: pass # If the expected type is not a list, a iterable or a 'type' object raise an error else: - raise TypeError(f"Invalid expected type. Expected 'list', an iterable, 'type' or 'typing.Any' got {type(expected_type)} for key: {key}.") + raise TypeError( + "Invalid expected type. Expected 'list', an iterable, 'type' or 'typing.Any' " + f"got {type(expected_type)} for key: {key}." + ) # If no errors are raised, return True return True # If an error is raised, return False if strict is False, otherwise raise the error @@ -193,15 +222,15 @@ def check_cfg_types( cfg : dict, strict : bool = False ) -> bool: - """ - Check if the config is a dictionary and that the types of the values in the config dictionary are correct. + """Check if the config is a dictionary and that the types of the values in the config dictionary are correct. - Parameters: - cfg (`dict`): The config dictionary to check. - strict (`bool`, optional): If True, raise an error if a key is not recognized. Defaults to False. + Args: + cfg: The config dictionary to check. + strict: If True, raise an error if a key is not recognized. Defaults to False. Returns: - bool: True if all checks pass, raises an error otherwise. + True if all checks pass, raises an error otherwise. + """ # Check if cfg is a dictionary if not isinstance(cfg, dict): @@ -219,32 +248,36 @@ def check_cfg_types( return True def read_cfg( - path : Union[str, os.PathLike], + path : str | Path, strict : bool=False ) -> dict: - """ - Load and validate the config file. + """Load and validate the config file. Missing keys are replaced with default values. - Parameters: - config (`Union[str, os.PathLike]`): The path to the config file. - strict (`bool`, optional): If True, raise an error if a key is not recognized. Defaults to False. + Args: + path: The path to the config file. + strict: If True, raise an error if a key is not recognized. Defaults to False. Returns: - out (`dict`): The config dictionary. + The config dictionary. + """ # Check if config is a string or path-like object - if not isinstance(path, (str, os.PathLike)): - raise TypeError(f"Invalid config location. Expected str or os.PathLike, got {type(path)}.") + if not isinstance(path, (str, Path)): + raise TypeError(f"Invalid config location. Expected str or Path, got {type(path).__name__}.") # Check if the config file is a YAML file - if not (path.endswith(".yaml") or path.endswith(".yml")): + if isinstance(path, str): + path_name = path + else: + path_name = path.name + if not (path_name.endswith(".yaml") or path_name.endswith(".yml")): raise ValueError(f"Cannot read config. Expected YAML file, got {path}.") # Check if config file exists if not os.path.exists(path): raise FileNotFoundError(f"Config file {path} not found.") # Load config file - with open(path, "r") as f: + with open(path) as f: cfg = yaml.safe_load(f) # Type check config check_cfg_types(cfg, strict) @@ -257,25 +290,29 @@ def read_cfg( def write_cfg( cfg : dict, - path : Union[str, os.PathLike], + path : str | os.PathLike, overwrite : bool=False - ) -> Union[str, os.PathLike]: - """ - Save the config dictionary to a YAML file. + ) -> str | os.PathLike: + """Save the config dictionary to a YAML file. - Parameters: - cfg (`dict`): The config dictionary to save. - path (`Union[str, os.PathLike]`): The path to save the config file. - overwrite (`bool`, optional): If True, overwrite the file if it already exists. Defaults to False. + Args: + cfg: The config dictionary to save. + path: The path to save the config file. + overwrite: If True, overwrite the file if it already exists. Defaults to False. Returns: - out (`Union[str, os.PathLike]`): The path to the saved config file. + The path to the saved config file. + """ # Check if path is a string or path-like object - if not isinstance(path, (str, os.PathLike)): - raise TypeError(f"Invalid config location. Expected str or os.PathLike, got {type(path)}.") + if not isinstance(path, (str, Path)): + raise TypeError(f"Invalid config location. Expected str or Path, got {type(path)}.") + if isinstance(path, str): + path_name = path + else: + path_name = path.name # Check if path is a YAML file - if not (path.endswith(".yaml") or path.endswith(".yml")): + if not (path_name.endswith(".yaml") or path_name.endswith(".yml")): raise ValueError(f"Cannot save config. Expected YAML file, got {path}.") # Check if path exists if not overwrite and os.path.exists(path): @@ -297,7 +334,8 @@ def write_cfg( sorted_cfg[key] = cfg[key] # Save config file with open(path, "w") as f: - # OBS: will fail if not using yaml.SafeDumper (default with yaml.safe_dump). If another dumper is to be used, the representer for OrderedDict must be added manually. + # OBS: will fail if not using yaml.SafeDumper (default with yaml.safe_dump). + # If another dumper is to be used, the representer for OrderedDict must be added manually. yaml.safe_dump(sorted_cfg, f, sort_keys=False, default_flow_style=None) # Return the path to the saved config YAML file return path diff --git a/src/flat_bug/datasets.py b/src/flat_bug/datasets.py index abdde6e..d92cf23 100644 --- a/src/flat_bug/datasets.py +++ b/src/flat_bug/datasets.py @@ -1,9 +1,10 @@ +"""Modified YOLO dataset used for training flatbug.""" import os import re import stat import tempfile from pathlib import Path -from typing import Dict, List, Optional, Self, Tuple, Union +from typing import cast import cv2 import numpy as np @@ -13,64 +14,63 @@ from ultralytics.data.dataset import LOGGER from ultralytics.utils import IterableSimpleNamespace -from flat_bug.augmentations import (CenterCrop, FixInstances, - FlatBugRandomPerspective, RandomColorInv, - RandomCrop) +from flat_bug.augmentations import CenterCrop, FixInstances, FlatBugRandomPerspective, RandomColorInv, RandomCrop HELP_URL = 'See https://github.com/ultralytics/yolov5/wiki/Train-Custom-Data' IMG_FORMATS = 'bmp', 'dng', 'jpeg', 'jpg', 'mpo', 'png', 'tif', 'tiff', 'webp', 'pfm' # include image suffixes -def get_area(image_path): +def get_area(image_path): # noqa: D103 with Image.open(image_path) as image: return image.size[0] * image.size[1] -def calculate_image_weights(image_paths : List[str]) -> List[float]: - """ - Calculate normalized weights for each image based on the file sizes, - normalized by the minimum file size, so that the values are between 1 and infinity. +def calculate_image_weights(image_paths : list[str]) -> list[float]: + """Calculate normalized weights for each image based on the file sizes. + + Normalized by the minimum file size, so that the values are between 1 and infinity. Args: - image_paths (`List[str]`): List of image file paths. + image_paths: List of image file paths. Returns: - out (`List[float]`): normalized weights for each image. + normalized weights for each image. + """ file_sizes = [get_area(path) + 1 for path in image_paths] min_size = min(file_sizes) return [(size / min_size) for size in file_sizes] def reweight( - weights : List[float], - target_sum : Union[float, int] - ) -> List[float]: - """ - Reweights the provided list of weights so that their sum equals the target sum. + weights : list[float], + target_sum : float | int + ) -> list[float]: + """Reweights the provided list of weights so that their sum equals the target sum. Args: - weights (`List[float]`): List of weights to reweight. - target_sum (`Union[float, int]`): Desired sum of the weights. + weights: List of weights to reweight. + target_sum: Desired sum of the weights. Returns: - out (`List[float]`): Reweighted weights. + Reweighted weights. + """ sum_weights = sum(weights) return [max(round(w * target_sum / sum_weights), 1) for w in weights] def generate_indices( - weights : List[float], - target_size : Optional[int]=None - ) -> List[int]: - """ - Deterministically generates a list of indices based on the provided weights to oversample the items. + weights : list[float], + target_size : int | None=None + ) -> list[int]: + """Deterministically generates a list of indices based on the provided weights to oversample the items. Args: - weights (`List[float]`): List of weights for each item. - target_size (`Optional[int]`, optional): Desired size of the output list. If None, the size of the output is approximately the sum of the weights. + weights: List of weights for each item. + target_size: Desired size of the output list. If None, the size of the output is approximately the sum of the weights. Returns: - out (`List[int]`): List of indices to oversample the items. + List of indices to oversample the items. + """ - n = len(weights) + # n = len(weights) weights = [max(round(w), 1) for w in weights] indices = [] @@ -85,8 +85,8 @@ def generate_indices( return indices -def get_datasets(files : List[str]) -> Dict[str, List[str]]: - file_dataset = [re.match(r"[^_]+", os.path.basename(f)).group(0) for f in files] +def get_datasets(files : list[str]) -> dict[str, list[str]]: # noqa: D103 + file_dataset = [mtch.group(0) for f in files if (mtch := re.match(r"[^_]+", os.path.basename(f)))] datasets = list(set(file_dataset)) datasets = {d : [] for d in datasets} for file, fd in zip(files, file_dataset): @@ -95,22 +95,28 @@ def get_datasets(files : List[str]) -> Dict[str, List[str]]: def subset( self : "FlatBugYOLODataset", - n : Optional[int]=None, - pattern : Optional[str]=None + n : int | None=None, + pattern : str | None=None ): - """ - Subsets the dataset to the first 'n' elements that match the pattern. + """Subsets the dataset to the first 'n' elements that match the pattern. Args: - n (`Optional[int]`, optional): The number of elements to keep. Defaults to None; keep all. - pattern (`Optional[str]`, optional): A regex pattern to match the filenames. Defaults to None; match all. + self: A `FlatBugYOLODataset` instance. + n: The number of elements to keep. Defaults to None; keep all. + pattern: A regex pattern to match the filenames. Defaults to None; match all. + """ if pattern is None and (n is None or n == -1): return self - # Compile the regex pattern - pattern = re.compile(pattern) if pattern else None - # Create a match function that returns Truthy if the filename matches the pattern or the pattern is None - match_fn = (lambda x: pattern.search(os.path.basename(x))) if pattern else (lambda x: True) + if pattern is not None: + cp = re.compile(pattern) + def _match_pattern(x): + return bool(cp.search(os.path.basename(x))) + match_fn = _match_pattern + else: + def _match_all(_): + return True + match_fn = _match_all # Get the indices of the elements that match the pattern indices = [i for i, f in enumerate(self.im_files) if match_fn(f)] # If n is not None, keep only the first n elements @@ -119,9 +125,9 @@ def subset( # Subset the images self.im_files = [f for i, f in enumerate(self.im_files) if i in indices] -def hook_get_labels_with_subset( +def hook_get_labels_with_subset( # noqa: D103 obj : "FlatBugYOLODataset", - args : Dict + args : dict ): if not isinstance(args, dict): raise ValueError("args must be a dictionary") @@ -133,33 +139,38 @@ def subset_then_get(): return obj.get_labels() obj.get_labels = subset_then_get -class PrintNumInstances: - def __init__(self, title : str): +class PrintNumInstances: # noqa: D101 + def __init__(self, title : str): # noqa: D107 self.fmt = f'({"{num:>5}"}) ({"{imsize:^10}"}) | {title}' - def __call__(self, labels : Dict): + def __call__(self, labels : dict): # noqa: D102 n = len(labels["instances"]) if "instances" in labels else labels["masks"].max().item() print(self.fmt.format(num=n, imsize="x".join([str(d) for d in labels["img"].shape]))) return labels -def train_augmentation_pipeline( +def train_augmentation_pipeline( # noqa: D103 hyperparameters : IterableSimpleNamespace, image_size : int, - max_instances : Optional[int], + max_instances : int | float | None, min_size : int, use_segments : bool, use_keypoints : bool ) -> Compose: return Compose([ - RandomCrop(imsize=int(image_size * 1.5)), # Crop to slightly larger than needed for training - FlatBugRandomPerspective(imgsz=int(image_size * 1.5), degrees=180, translate=0, scale=0), # Affine transformation at same size as above - CenterCrop(image_size), # Crop to needed size + # Crop to slightly larger than needed for training + RandomCrop(imsize=int(image_size * 1.5)), + # Affine transformation at same size as above + FlatBugRandomPerspective(imgsz=int(image_size * 1.5), degrees=180, translate=0, scale=0), + # Crop to needed size + CenterCrop(image_size), RandomHSV(hgain=hyperparameters.hsv_h, sgain=hyperparameters.hsv_s, vgain=hyperparameters.hsv_v), RandomColorInv(p=0.25), RandomFlip(direction="vertical", p=hyperparameters.flipud), RandomFlip(direction="horizontal", p=hyperparameters.fliplr), - FixInstances(area_thr=0.975, max_targets=max_instances, min_size=min_size), # Remove instances outside crop - Format( # YOLO-native preprocessing + # Remove instances outside crop + FixInstances(area_thr=0.975, max_targets=max_instances, min_size=min_size), + # YOLO-native preprocessing + Format( bbox_format="xywh", normalize=True, return_mask=use_segments, @@ -170,7 +181,7 @@ def train_augmentation_pipeline( ), ]) -def validation_augmentation_pipeline( +def validation_augmentation_pipeline( # noqa: D103 image_size : int, min_size : int, use_segments : bool, @@ -191,15 +202,19 @@ def validation_augmentation_pipeline( ) ]) -class FlatBugYOLODataset(YOLODataset): - _min_size : int=32 # What is the minimum size of an instance to be considered (width or height in pixels after augmentations) - _oversample_factor : int=2 # How much do we allow the dataset to grow when oversampling - this is done to ensure larger images are not underrepresented +class FlatBugYOLODataset(YOLODataset): # noqa: D101 + + # What is the minimum size of an instance to be considered (width or height in pixels after augmentations) + _min_size : int=32 - def __init__( - self : Self, - max_instances : Optional[int], + # How much do we allow the dataset to grow when oversampling - this is done to ensure larger images are not underrepresented + _oversample_factor : int=2 + + def __init__( # noqa: D107 + self, + max_instances : int | float | None, classes : None=None, - subset_args : Optional[Dict]=None, + subset_args : dict | None=None, *args, **kwargs ): @@ -208,10 +223,13 @@ def __init__( if subset_args is not None: hook_get_labels_with_subset(self, subset_args) if "data" in kwargs: - if not "channels" in kwargs["data"]: + if "channels" not in kwargs["data"]: kwargs["data"]["channels"] = 3 super().__init__(classes=classes, *args, **kwargs) - self.sample_weights = [image_weight * len(label_i["cls"]) for label_i, image_weight in zip(self.labels, calculate_image_weights(self.im_files))] + self.sample_weights = [ + image_weight * len(label_i["cls"]) + for label_i, image_weight in zip(self.labels, calculate_image_weights(self.im_files)) + ] self.__indices = generate_indices(self.sample_weights, target_size=len(self.im_files) * self._oversample_factor) def _debug_write_loaded_images(self, out, index): @@ -223,18 +241,29 @@ def _debug_write_loaded_images(self, out, index): for k in range(bbs.shape[0]): x, y, w, h = bbs[k, :] n = cv2.rectangle(n, (x - w // 2, y - w // 2), (x + w // 2, y + h // 2), 255, 3) - cv2.imwrite("/tmp/test/%i-img.jpg" % index, n + m / 3) + cv2.imwrite(f"/tmp/test/{index}-img.jpg", n + m / 3) def load_image( - self : Self, - i : Union[int, slice] - ) -> Tuple[np.ndarray, Tuple[int, int], Tuple[int, int]]: + self, + i : int | slice + ) -> tuple[np.ndarray, tuple[int, int], tuple[int, int]]: + """Load an image. + + Args: + i: Image index. + + Returns: + im, hw_original, hw_resized + + """ # Loads 1 image from dataset index 'i', returns (im, resized hw) im, f, fn = self.ims[i], self.im_files[i], self.npy_files[i] + f = cast(Path, f) + fn = cast(Path, fn) if im is None: # not cached in RAM if fn.exists(): # load npy - im = np.load(fn) + im = cast(np.ndarray, np.load(fn)) else: # read image im = cv2.imread(f) # BGR @@ -243,12 +272,11 @@ def load_image( h0, w0 = im.shape[:2] # orig hw - return im, (h0, w0), im.shape[:2] # im, hw_original, hw_resized - # print("cached", f, self.im_hw0[i], self.im_hw[i]) - return self.ims[i], self.im_hw0[i], self.im_hw[i] # im, hw_original, hw_resized + return im, (h0, w0), im.shape[:2] # type: ignore + return self.ims[i], self.im_hw0[i], self.im_hw[i] # type: ignore - def build_transforms( - self : Self, + def build_transforms( # noqa: D102 + self, hyp : IterableSimpleNamespace ) -> Compose: return train_augmentation_pipeline( @@ -261,15 +289,14 @@ def build_transforms( ) def cache_labels( - self : Self, + self, path : Path=Path("./labels.cache") ): - """ - OBS: DO NOT USE THIS FUNCTION MANUALLY. - """ + """OBS: DO NOT USE THIS FUNCTION MANUALLY.""" LOGGER.warning("!! OBS !! ==>>== Flat-bug doesn't use the .cache-file! ==<<== !! OBS !!") - # To bypass the creation of .cache files we use a temporary dummy file, which is set to read-only, causing a check in ultralytics to bail on creating the file + # To bypass the creation of .cache files we use a temporary dummy file, which is set to read-only, + # causing a check in ultralytics to bail on creating the file tmp_file = tempfile.NamedTemporaryFile(delete=False) # The path passed to the superclass `cache_labels` method must be a pathlib.Path object unwriteable_tmp_path = Path(tmp_file.name) @@ -277,7 +304,8 @@ def cache_labels( # Change the file to read-only os.chmod(str(unwriteable_tmp_path), stat.S_IREAD) - # Before calling the superclass `cache_labels` method, we need to create a dummy `.cache.npy` file + # Before calling the superclass `cache_labels` method, + # we need to create a dummy `.cache.npy` file temporary_dummy_numpy_cache_file = unwriteable_tmp_path.with_suffix(".cache.npy") np.save(temporary_dummy_numpy_cache_file, np.array([0])) @@ -300,11 +328,11 @@ def __len__(self): def __getitem__(self, index): return self.transforms(self.get_image_and_label(self.__indices[index])) -class FlatBugYOLOValidationDataset(FlatBugYOLODataset): +class FlatBugYOLOValidationDataset(FlatBugYOLODataset): # noqa: D101 _resample_n : int= 5 - def build_transforms( - self : Self, + def build_transforms( # noqa: D102 + self, hyp : IterableSimpleNamespace ) -> Compose: return validation_augmentation_pipeline( diff --git a/src/flat_bug/eval_utils.py b/src/flat_bug/eval_utils.py index e274fd4..ed8a76a 100644 --- a/src/flat_bug/eval_utils.py +++ b/src/flat_bug/eval_utils.py @@ -1,24 +1,25 @@ +"""Utilities for flatbug evaluation.""" import csv import os import time -from typing import Any, Dict, List, Optional, Tuple, Union +from collections.abc import Sequence +from typing import Any import cv2 import numpy as np from flat_bug import logger -from flat_bug.coco_utils import (annotations_to_numpy, contour_area, - contour_bbox) +from flat_bug.coco_utils import annotations_to_numpy, contour_area, contour_bbox -def isfloat(num : str) -> bool: +def isfloat(num : str) -> bool: # noqa: D103 try: - num = float(num) - return not num.is_integer() + fnum = float(num) + return not fnum.is_integer() except Exception: return False -def ispath(path : str) -> bool: +def ispath(path : str) -> bool: # noqa: D103 return "/" in path or "\\" in path def format_cell( @@ -26,18 +27,18 @@ def format_cell( digits : int = 3, max_length : int = 30 ) -> str: - """ - Autoformat a cell for a table. + """Autoformat a cell for a table. Standardizes the number of decimals if the cell is coercible to a float, and truncates the cell if it exceeds the maximum length. Args: - cell (`str`): The cell to format. - digits (`int`, optional): Number of digits to display for floats. Default is 3. - max_length (`int`, optional): Maximum number of characters in the output string. Default is 30. OBS: Paths are not truncated. + cell: The cell to format. + digits: Number of digits to display for floats. Default is 3. + max_length: Maximum number of characters in the output string. Default is 30. OBS: Paths are not truncated. Returns: - out (`str`): The formatted cell string where `length <= max_length`. + The formatted cell string where `length <= max_length`. + """ if isfloat(cell): return f"{float(cell):.{digits}f}" @@ -48,20 +49,20 @@ def format_cell( return cell def format_row( - cells : List[Any], - widths : List[int], + cells : list[Any], + widths : list[int], align : str = "center" ) -> str: - """ - Format a row of a table. + """Format a row of a table. Args: - cells (`List[Any]`): The cells of the row. Elements should be compatible with f-strings (`__format__`). - widths (`List[int]`): The widths of each column. - align (`str`, optional): Alignment of the cell content within each column. Valid options are "center"/"left"/"right". Defaults to "center". + cells: The cells of the row. Elements should be compatible with f-strings (`__format__`). + widths: The widths of each column. + align: Alignment of the cell content within each column. Valid options are "center"/"left"/"right". Defaults to "center". Returns: - out (`str`): The formatted row. + The formatted row. + """ row = "|" for cell, width in zip(cells, widths): @@ -78,14 +79,15 @@ def pretty_print_csv( csv_file : str, delimiter : str = "," ): - """ - Pretty print the CSV file. + """Pretty print the CSV file. Args: - csv_file (`str`): The path to the CSV file. + csv_file: The path to the CSV file. + delimiter: Delimiter. + """ # Read the CSV file data - with open(csv_file, 'r') as file: + with open(csv_file) as file: csv_reader = csv.reader(file, delimiter=delimiter) try: headers = next(csv_reader) @@ -113,15 +115,15 @@ def bbox_intersect( b1 : np.ndarray, b2s : np.ndarray ) -> np.ndarray: - """ - Calculate the intersecting rectangle between two rectangles. The rectangles must be aligned with the axes. + """Calculate the intersecting rectangle between two rectangles. The rectangles must be aligned with the axes. Args: - b1 (`np.ndarray`): Bounding box 1. - b2s (`np.ndarray`): Bounding boxes 2. + b1: Bounding box 1. + b2s: Bounding boxes 2. Returns: - out (`np.ndarray`): Intersecting rectangles of shape (n, 4). + Intersecting rectangles of shape (n, 4). + """ if len(b2s.shape) == 1: b2s = b2s.copy().reshape(-1, 4) @@ -139,15 +141,15 @@ def bbox_intersect_area( b1 : np.ndarray, b2s : np.ndarray ) -> np.ndarray: - """ - Calculate the area of the intersecting rectangle between two rectangles. The rectangles must be aligned with the axes. + """Calculate the area of the intersecting rectangle between two rectangles. The rectangles must be aligned with the axes. Args: - b1 (`np.ndarray`): Bounding box 1. - b2s (`np.ndarray`): Bounding boxes 2. + b1: Bounding box 1. + b2s: Bounding boxes 2. Returns: - out (`np.ndarray`): Area of the intersecting rectangles of shape (n,). + Area of the intersecting rectangles of shape (n,). + """ if len(b2s.shape) == 1: b2s = b2s.copy().reshape(-1, 4) @@ -161,9 +163,8 @@ def contour_intersection( contour2: np.ndarray, box1: np.ndarray, box2: np.ndarray - ) -> np.ndarray: - """ - Calculates the intersection of two contours. + ): + """Calculate the intersection of two contours. Contours should be providedd as [x1, y1, x2, y2, ..., xn, yn] @@ -177,13 +178,14 @@ def contour_intersection( 7. Return the sum of the intersection mask. Args: - contour1 (`np.ndarray`): Contour 1. - contour2 (`np.ndarray`): Contour 2. - box1 (`np.ndarray`): Bounding box 1. - box2 (`np.ndarray`): Bounding box 2. + contour1: Contour 1. + contour2: Contour 2. + box1: Bounding box 1. + box2: Bounding box 2. Returns: - out (`np.ndarray`): Scalar intersection of the contours with type np.int64. + Scalar intersection of the contours with type np.int64. + """ # If any of the contours are empty, return 0 if len(contour1) < 2 or len(contour2) < 2: @@ -206,32 +208,31 @@ def contour_intersection( cv2.drawContours(mask1, [contour1], -1, 1, thickness=cv2.FILLED) cv2.drawContours(mask2, [contour2], -1, 1, thickness=cv2.FILLED) # Calculate the intersection - return (mask1 * mask2).sum(dtype=np.int64) + return float((mask1 * mask2).sum(dtype=np.int64)) def pairwise_contour_intersection( - contours1: List[np.ndarray], - contours2: Optional[List[np.ndarray]] = None, - bboxes1: Optional[np.ndarray] = None, - bboxes2: Optional[np.ndarray] = None, - areas1: Optional[np.ndarray] = None, - areas2: Optional[np.ndarray] = None - ) -> np.array: - """ - Calculates the pairwise intersection of two groups of contours. + contours1: list[np.ndarray], + contours2: list[np.ndarray] | None = None, + bboxes1: np.ndarray | None = None, + bboxes2: np.ndarray | None = None, + areas1: np.ndarray | None = None, + areas2: np.ndarray | None = None + ) -> np.ndarray: + """Calculate the pairwise intersection of two groups of contours. Args: - contours1 (`List[np.ndarray]`): Contours in group 1. - contours2 (`Optional[np.ndarray]`): Contours in group 2. If None provided, symmetric intersection is calculated for contours1 instead. Defaults to None. - areas1 (`Optional[np.ndarray]`): Areas of contours in group 1. Computed if not None. Defaults to None - areas2 (`Optional[np.ndarray]`): Areas of contours in group 2. Computed if not None. Defaults to None - bboxes1 (`Optional[np.ndarray]`): Bounding boxes of contours in group 1. Computed if not None. Defaults to None - bboxes2 (`Optional[np.ndarray]`): Bounding boxes of contours in group 2. Computed if not None. Defaults to None + contours1: Contours in group 1. + contours2: Contours in group 2. If None provided, symmetric intersection is calculated for contours1 instead. Defaults to None. + areas1: Areas of contours in group 1. Computed if not None. Defaults to None + areas2: Areas of contours in group 2. Computed if not None. Defaults to None + bboxes1: Bounding boxes of contours in group 1. Computed if not None. Defaults to None + bboxes2: Bounding boxes of contours in group 2. Computed if not None. Defaults to None Returns: - out (`np.ndarray`): Intersection matrix of shape (n, m). - """ + Intersection matrix of shape (n, m). + """ # If contours2 is not provided, set it to contours1 if contours2 is None: contours2 = contours1 @@ -263,30 +264,34 @@ def pairwise_contour_intersection( def match_geoms( - contours1: List[np.ndarray], - contours2: List[np.ndarray], + contours1: list[np.ndarray], + contours2: list[np.ndarray], threshold: float = 1 / 4, - iou_mat: Optional[np.ndarray] = None, - areas1: Optional[np.ndarray] = None, - areas2: Optional[np.ndarray] = None - ) -> Tuple[np.ndarray, int]: - """ - Matches geometries (polygons) in group 1 to geometries in group 2. + iou_mat: np.ndarray | None = None, + areas1: np.ndarray | None = None, + areas2: np.ndarray | None = None + ) -> tuple[np.ndarray, int]: + """Match geometries (polygons) in group 1 to geometries in group 2. Args: - contours1 (`List[np.ndarray]`): Geometries (polygons) in group 1. List of length N, where each element is a Xx2 array of contour coordinates. - contours2 (`List[np.ndarray]`): Geometries (polygons) in group 2. List of length M, where each element is a Xx2 array of contour coordinates. - threshold (`float`, optional): IoU threshold. Defaults to 1/4. - iou_mat (`Optional[np.ndarray]`, optional): IoU matrix of size NxM. Computed if None. Defaults to None. - areas1 (`Optional[np.ndarray]`, optional): Areas of polygons in group 1. Computed if None. Defaults to None. - areas2 (`Optional[np.ndarray]`, optional): Areas of polygons in group 2. Computed if None. Defaults to None. + contours1: Geometries (polygons) in group 1. List of length N, where each element is a Xx2 array of contour coordinates. + contours2: Geometries (polygons) in group 2. List of length M, where each element is a Xx2 array of contour coordinates. + threshold: IoU threshold. Defaults to 1/4. + iou_mat: IoU matrix of size NxM. Computed if None. Defaults to None. + areas1: Areas of polygons in group 1. Computed if None. Defaults to None. + areas2: Areas of polygons in group 2. Computed if None. Defaults to None. Returns: - out (`List[np.ndarray, int]`): Nx2 array of matched indices from group 1 and group 2, and the number of unmatched geometries in group 2. + Nx2 array of matched indices from group 1 and group 2, and the number of unmatched geometries in group 2. + """ # Calculate the number of contours in each group n = len(contours1) m = len(contours2) + if areas1 is None: + areas1 = np.array(list(map(contour_area, contours1))) + if areas2 is None: + areas2 = np.array(list(map(contour_area, contours1))) if iou_mat is None: # Calculate the IoU matrix intersections = pairwise_contour_intersection(contours1, contours2) @@ -334,32 +339,33 @@ def match_geoms( def plot_heatmap( mat: np.ndarray, - axis_labels: Optional[List[str]] = None, + axis_labels: Sequence[str] | None = None, breaks: int = 25, - dimensions: Optional[Tuple[int, int]] = None, - output_path: str = None, + dimensions: tuple[int, int] | None = None, + output_path: str | None = None, scale: float = 1 ): - """ - Plots a heatmap of a matrix using OpenCV. + """Plot a heatmap of a matrix using OpenCV. Args: - mat (`np.ndarray`): Matrix to plot. - axis_labels (`Optional[List[str]]`, optional): Axis labels. Defaults to None. - breaks (`int`, optional): Number of breaks on the colorbar. Defaults to 25. - dimensions (`Optional[Tuple[int, int]]`, optional): Dimensions of the output image. Defaults to None. - output_path (`str`, optional): Output path. Defaults to None. - scale (`float`, optional): Scale of the output image. Defaults to 1. + mat: Matrix to plot. + axis_labels: Axis labels. Defaults to None. + breaks: Number of breaks on the colorbar. Defaults to 25. + dimensions: Dimensions of the output image. Defaults to None. + output_path: Output path. Defaults to None. + scale: Scale of the output image. Defaults to 1. + """ if dimensions is None: - dimensions = tuple([m * 10 for m in mat.shape[::-1]]) + dimensions = tuple([m * 10 for m in mat.shape[::-1]]) # type: ignore min_dim = max(min(dimensions), 1) if min_dim < 1000: scale_dims = 1000 / min_dim - dimensions = tuple([int(d * scale_dims) for d in dimensions]) + dimensions = tuple([int(d * scale_dims) for d in dimensions]) # type: ignore if mat.shape[0] == 0 or mat.shape[1] == 0: logger.warning('Empty matrix. Cannot plot heatmap.') return + assert dimensions is not None # Create a colormap for viridis colormap = cv2.applyColorMap( @@ -385,12 +391,15 @@ def plot_heatmap( if cmin == cmax: nice_breaks = np.array([cmin]) else: - # Add semi-equally spaced numbers to the colorbar at "nice" values, "nice" values are defined as integer multiples of powers of 10 to the power of the maximum value - the integer rounded 10 logarithm of the number of breaks + # Add semi-equally spaced numbers to the colorbar at "nice" values, + # "nice" values are defined as integer multiples of powers of 10 + # to the power of the maximum value - the integer rounded 10 logarithm of the number of breaks raw_breaks = np.linspace(cmin, cmax, breaks) nice_multiple = 10 ** (np.log10(cmax) - np.ceil(np.log10(breaks))) nice_breaks = (raw_breaks / nice_multiple).round() * nice_multiple nice_breaks = nice_breaks[nice_breaks <= cmax] - # Ensure that the minimum and maximum values are included, and remove the breaks if they are within 1 "nice_multiple" of any other break + # Ensure that the minimum and maximum values are included, + # and remove the breaks if they are within 1 "nice_multiple" of any other break nice_breaks = nice_breaks[np.abs(nice_breaks - cmin) >= (nice_multiple * 0.9)] nice_breaks = nice_breaks[np.abs(nice_breaks - cmax) >= (nice_multiple * 0.9)] nice_breaks = np.concatenate([[cmin], nice_breaks, [cmax]]) @@ -465,7 +474,8 @@ def plot_heatmap( # First create the x-axis box x_axis_box = np.zeros((axis_box_size, dimensions[0] + colorbar_width, 3), dtype=np.uint8) + 255 - # Then create the y-axis box, remembering to take into account the extra vertical space taken up by the x-axis label. It is instantiated in the flipped orientation. + # Then create the y-axis box, remembering to take into account the extra vertical space taken up by the x-axis label. + # It is instantiated in the flipped orientation. y_axis_box = np.zeros((axis_box_size, dimensions[1] + axis_box_size, 3), dtype=np.uint8) + 255 # Calculate the midpoint on each box with respect to the heatmap x_label_width = cv2.getTextSize(x_label, cv2.FONT_HERSHEY_COMPLEX, axis_label_font_size, 3)[0][0] @@ -522,21 +532,22 @@ def plot_heatmap( def equal_spaced_cuts( k : int, - start : Union[float, int], - end : Union[float, int] + start : float | int, + end : float | int ) -> np.ndarray: - """ - Generate k equal spaced cuts between start and end. + """Generate k equal spaced cuts between start and end. - The edges are not included, and the distance between the left-most and right-most cut to the edges is half the distance between the cuts. + The edges are not included, and the distance between the left-most and + right-most cut to the edges is half the distance between the cuts. Args: - k (`int`): Number of cuts. - start (`float`): Start value. - end (`float`): End value. + k: Number of cuts. + start: Start value. + end: End value. Returns: - out (`np.ndarray`): Cuts. + Cuts. + """ return np.linspace(start + (end - start) / (k * 2), end - (end - start) / (k * 2), k) @@ -544,25 +555,28 @@ def equal_spaced_cuts( def plot_matches( matches: np.ndarray, contours1: list[np.ndarray], - contours2: List[np.ndarray], - group_labels: Optional[List[str]] = None, - image_path: Optional[str] = None, - output_path: Optional[str] = None, + contours2: list[np.ndarray], + group_labels: Sequence[str] | None = None, + image_path: str | None = None, + output_path: str | None = None, scale: float = 1, boxes: bool = True ): - """ - Plots the matches between two groups of contours using OpenCV. + """Plot the matches between two groups of contours using OpenCV. Args: - matches (`np.ndarray`): Matches between group 1 and 2. - contours1 (`List[np.ndarray]`): Contours of group 1. - contours2 (`List[np.ndarray]`): Contours of group 2. - group_labels (`Optional[List[str]]`, optional): Labels of the two groups (should have a length of 2). If None the groups are labelled as "1" and "2". Defaults to None. - image_path (`Optional[str]`, optional): Path to the image. If None the matches are plotted on a black background. Defaults to None. - output_path (`Optional[str]`, optional): Output path of plot. If None the rasterized result is displayed or returned as an array, depending in the context. Defaults to None. - scale (`float`, optional): Scale of the output image. Defaults to 1. - boxes (`bool`, optional): Flag to indicate whether to plot bounding boxes. Defaults to True. + matches: Matches between group 1 and 2. + contours1: Contours of group 1. + contours2: Contours of group 2. + group_labels: Labels of the two groups (should have a length of 2). If None the groups are labelled as "1" and "2". + Defaults to None. + image_path: Path to the image. If None the matches are plotted on a black background. Defaults to None. + output_path: Output path of plot. + If None the rasterized result is displayed or returned as an array, depending in the context. + Defaults to None. + scale: Scale of the output image. Defaults to 1. + boxes: Flag to indicate whether to plot bounding boxes. Defaults to True. + """ GROUP_COLORS = [(184, 126, 55), (28, 26, 228)] # Type check the input @@ -570,24 +584,24 @@ def plot_matches( raise ValueError(f'Expected matches to be a NumPy array, got {type(matches)}') for i, c1 in enumerate(contours1): if not isinstance(c1, np.ndarray): - raise ValueError(f'Expected contours1[{i}] to be a NumPy array, got {type(c1)}') + raise ValueError(f'Expected contours1[{i}] to be a NumPy array, got {type(c1).__name__}') for i, c2 in enumerate(contours2): if not isinstance(c2, np.ndarray): - raise ValueError(f'Expected contours2[{i}] to be a NumPy array, got {type(c2)}') - if not isinstance(group_labels, list) and not group_labels is None: + raise ValueError(f'Expected contours2[{i}] to be a NumPy array, got {type(c2).__name__}') + if not isinstance(group_labels, list) and group_labels is not None: raise ValueError(f'Expected group_labels to be a list or None, got {type(group_labels)}') elif isinstance(group_labels, list): - for i, l in enumerate(group_labels): - if not isinstance(l, str): - raise ValueError(f'Expected group_labels[{i}] to be a string, got {type(l)}') - elif isinstance(group_labels, None): + for i, lab in enumerate(group_labels): + if not isinstance(lab, str): + raise ValueError(f'Expected group_labels[{i}] to be a string, got {type(lab).__name__}') + elif group_labels is None: group_labels = ["1", "2"] - if not isinstance(image_path, str) and not image_path is None: - raise ValueError(f'Expected image_path to be a string or None, got {type(image_path)}') + if not isinstance(image_path, str) and image_path is not None: + raise ValueError(f'Expected image_path to be a string or None, got {type(image_path).__name__}') elif isinstance(image_path, str) and not os.path.exists(image_path): raise ValueError(f'Expected image_path to be a valid file, got {image_path}') - if not isinstance(output_path, str) and not output_path is None: - raise ValueError(f'Expected output_path to be a string or None, got {type(output_path)}') + if not isinstance(output_path, str) and output_path is not None: + raise ValueError(f'Expected output_path to be a string or None, got {type(output_path).__name__}') elif isinstance(output_path, str) and not os.path.exists(os.path.dirname(output_path)): raise ValueError(f'Output directory does not exist: {os.path.dirname(output_path)}') @@ -596,13 +610,14 @@ def plot_matches( if save_plot: # Check the output path extension _, out_ext = os.path.splitext(output_path) - if not out_ext in [".jpg", ".jpeg", ".JPG", ".JPEG"]: + if out_ext not in [".jpg", ".jpeg", ".JPG", ".JPEG"]: raise ValueError(f'Expected output path to have a .JPG/.jpg/.JPEG/.jpeg extension, got {out_ext}') # If the is image path is provided if isinstance(image_path, str): # Load the image image = cv2.imread(filename = image_path) + assert image is not None else: # Otherwise, create a blank image. The dimensions are dynamically calculated to fit the contours xmax, ymax = 0, 0 @@ -843,13 +858,13 @@ def plot_matches( compatible_display(image) -def compatible_display(image: np.array): +def compatible_display(image: np.ndarray): # noqa: D103 TIMEOUT = 5 # seconds # Check if the image is displayed in a Jupyter notebook if 'get_ipython' in globals(): # Only import the necessary modules if the image is displayed in a Jupyter notebook, ensures they are optional dependencies - import ipywidgets as widgets - from IPython.display import clear_output, display + import ipywidgets as widgets # type: ignore + from IPython.display import clear_output, display # type: ignore # Convert the image from BGR to RGB image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) @@ -892,16 +907,15 @@ def compare_groups( group1: list, group2: list, threshold: float=0.1, - group_labels: Optional[str]=["Ground Truth", "Predictions"], + group_labels: Sequence[str] | None=("Ground Truth", "Predictions"), plot: bool=False, plot_scale: float=1, plot_boxes: bool=True, - image_path: Optional[str]=None, - output_identifier: Optional[str]=None, - output_directory: Optional[str]=None - ) -> Union[str, Dict]: - """ - Compares group 1 to group 2. + image_path: str | None=None, + output_identifier: str | None=None, + output_directory: str | None=None + ) -> str | dict: + """Compare group 1 to group 2. Output is saved to a CSV file with the following columns: - Idx_1 (`int`) is the index of the geometry in group 1 @@ -910,24 +924,29 @@ def compare_groups( - contourArea_1 (`int`) is the area of the contour in group 1 - contourArea_2 (`int`) is the area of the contour in group 2, or 0 if there is no match - bbox_1 (`list[int, int, int, int : xmin, ymin, xmax, ymax]`) is the bounding box of the geometry in group 1 - - bbox_2 (`list[int, int, int, int : xmin, ymin, xmax, ymax]`) is the bounding box of the matched geometry in group 2, or an empty list if there is no match + - bbox_2 (`list[int, int, int, int : xmin, ymin, xmax, ymax]`) is the bounding box of the + matched geometry in group 2, or an empty list if there is no match - contour_1 (`list[list[int, int : x_i, y_i]]`) is the contour of the geometry in group 1 - - contour_2 (`list[list[int, int : x_i, y_i]]`) is the contour of the matched geometry in group 2, or an empty list if there is no match + - contour_2 (`list[list[int, int : x_i, y_i]]`) is the contour of the matched geometry in group 2, + or an empty list if there is no match Args: - group1 (`list`): Group 1. - group2 (`list`): Group 2. - threshold (`float`, optional): IoU threshold for matching elements between groups. Defaults to 0.1. - group_labels (`Optional[str]`, optional): Group labels. Defaults to `["Ground Truth", "Predictions"]`. - plot (`bool`, optional): Whether to plot the matches and the IoU matrix, usually this is much slower than simply comparing the groups. Defaults to False. - plot_scale (`float`, optional): Scale of the plot. Defaults to 1. Lower values will make the plot smaller, but may be faster. - plot_boxes (`bool`, optional): Whether to plot the bounding boxes. Defaults to True. - image_path (`Optional[str]`, optional): Path to the image. Defaults to None. - output_identifier (`Optional[str]`, optional): Output identifier. Defaults to None. - output_directory (`Optional[str]`, optional): Output directory. Defaults to None. + group1: Group 1. + group2: Group 2. + threshold: IoU threshold for matching elements between groups. Defaults to 0.1. + group_labels: Group labels. Defaults to `["Ground Truth", "Predictions"]`. + plot: Whether to plot the matches and the IoU matrix, + usually this is much slower than simply comparing the groups. Defaults to False. + plot_scale: Scale of the plot. Defaults to 1. Lower values will make the plot smaller, but may be faster. + plot_boxes: Whether to plot the bounding boxes. Defaults to True. + image_path: Path to the image. Defaults to None. + output_identifier: Output identifier. Defaults to None. + output_directory: Output directory. Defaults to None. Returns: - out (`Union[str, dict]`): Path to the CSV file or the data that would have been saved to the CSV file as a dictionary, where the keys are the column names and the values are the column values. + Path to the CSV file or the data that would have been saved to the CSV file as a dictionary, + where the keys are the column names and the values are the column values. + """ # Type check the input if not isinstance(group1, list) or not isinstance(group2, list): @@ -941,7 +960,7 @@ def compare_groups( raise ValueError(f'Expected plot to be a bool, got {type(plot)}') if not (isinstance(image_path, str) or image_path is None): raise ValueError(f'Expected image_path to be a string or None, got {type(image_path)}') - if not isinstance(output_directory, str) and not output_directory is None: + if not isinstance(output_directory, str) and output_directory is not None: raise ValueError(f'Expected output_directory to be a string or None, got {type(output_directory)}') elif isinstance(output_directory, str) and not os.path.isdir(output_directory): raise ValueError(f'Expected output_directory to be a valid directory, got {output_directory}') @@ -953,7 +972,7 @@ def compare_groups( b2, c2 = annotations_to_numpy(group2) a1, a2 = np.array([contour_area(c) for c in c1]), np.array([contour_area(c) for c in c2]) - len_1, len_2 = len(c1), len(c2) + len_1, len_2 = len(c1), len(c2) # noqa: F841 # Calculate the IoU matrix intersection = pairwise_contour_intersection(c1, c2, b1, b2, a1, a2) @@ -973,15 +992,15 @@ def compare_groups( contours2 = c2, group_labels = group_labels, image_path = image_path, - output_path = os.path.join(output_directory, f'{output_identifier}_matches.jpg') if not output_directory is None else None, + output_path = os.path.join(output_directory, f'{output_identifier}_matches.jpg') if output_directory is not None else None, scale = plot_scale, boxes = plot_boxes ) - if not any([l == 0 for l in iou.shape]): + if not any([dim == 0 for dim in iou.shape]): plot_heatmap( mat = iou, - axis_labels = group_labels[::-1], - output_path = os.path.join(output_directory, f'{output_identifier}_heatmap.jpg') if not output_directory is None else None, + axis_labels = group_labels[::-1] if group_labels is not None else None, + output_path = os.path.join(output_directory, f'{output_identifier}_heatmap.jpg') if output_directory is not None else None, scale = plot_scale ) @@ -1049,7 +1068,11 @@ def compare_groups( len_contours2]) if len(data_length) != 1: raise ValueError( - f"Lengths of the data are not all the same: {len_idx1, len_idx2, len_matched_iou, len_careas1, len_careas2, len_boxes1, len_boxes2, len_contours1, len_contours2}") + "Lengths of the data are not all the same: {}, {}, {}, {}, {}, {}, {}, {}, {}".format( # noqa: UP032 + len_idx1, len_idx2, len_matched_iou, len_careas1, + len_careas2, len_boxes1, len_boxes2, len_contours1, len_contours2 + ) + ) # Construct the output by combining the data output = { @@ -1066,7 +1089,7 @@ def compare_groups( "contour_2": contours2 } - if not output_directory is None: + if output_directory is not None: # Write the output to a CSV file output_path = f"{output_directory}{os.sep}{output_identifier}.csv" separator = ";" @@ -1084,16 +1107,16 @@ def compare_groups( return output def generate_block(min: int, max: int, size: int) -> np.ndarray: - """ - Generates a block of integers centered around a random start value within a given range. + """Generate a block of integers centered around a random start value within a given range. Args: - min (`int`): Minimum value for the block. - max (`int`): Maximum value for the block. - size (`int`): Size of the block to generate. + min: Minimum value for the block. + max: Maximum value for the block. + size: Size of the block to generate. Returns: - out (`np.ndarray`): Array of integers within the specified range. + Array of integers within the specified range. + """ if size <= 0 or min >= max: raise ValueError("Size must be positive and min must be less than max.") @@ -1106,17 +1129,17 @@ def generate_block(min: int, max: int, size: int) -> np.ndarray: return block[np.logical_and(block >= min, block < max)] -def generate_bootstraps(s: int, n: int, block: bool = False) -> List[np.ndarray]: - """ - Generates bootstrap samples with or without block sampling. +def generate_bootstraps(s: int, n: int, block: bool = False) -> list[np.ndarray]: + """Generate bootstrap samples with or without block sampling. Args: - s (`int`): The size of the dataset. - n (`int`): The number of bootstrap samples to generate. - block (`bool`, optional): If True, generates block-based bootstraps. Defaults to False. + s: The size of the dataset. + n: The number of bootstrap samples to generate. + block: If True, generates block-based bootstraps. Defaults to False. Returns: - out (`List[np.ndarray]`): List of bootstrap samples. + List of bootstrap samples. + """ if s <= 0 or n <= 0: raise ValueError("The size 's' and the number 'n' of bootstraps must be positive.") @@ -1128,15 +1151,15 @@ def generate_bootstraps(s: int, n: int, block: bool = False) -> List[np.ndarray] return [np.random.choice(s, s, replace=True) for _ in range(n)] def f1_score(GT : np.ndarray, MP : np.ndarray) -> float: - """ - Calculates the F1 score for a binary classification problem. + """Calculate the F1 score for a binary classification problem. Args: - GT (`np.ndarray`): Ground truth binary labels. - MP (`np.ndarray`): Predicted binary labels. + GT: Ground truth binary labels. + MP: Predicted binary labels. Returns: - out (`float`): The F1 score. + The F1 score. + """ if len(GT) != len(MP): raise ValueError("Lengths of GT and MP must match.") @@ -1156,17 +1179,17 @@ def optimal_threshold_f1( confidence: np.ndarray, num_thresholds: int = 100 ) -> float: - """ - Finds the optimal threshold for F1 score by iterating over possible thresholds. + """Find the optimal threshold for F1 score by iterating over possible thresholds. Args: - y (`np.ndarray`): Ground truth binary labels. - iou (`np.ndarray`): IoU values, - confidence (`np.ndarray`): Confidence scores for predictions. - num_thresholds (`int`, optional): Number of thresholds to test. Defaults to 100. + y: Ground truth binary labels. + iou: IoU values, + confidence: Confidence scores for predictions. + num_thresholds: Number of thresholds to test. Defaults to 100. Returns: - out (`float`): The threshold that maximizes the F1 score. + The threshold that maximizes the F1 score. + """ if len(y) != len(iou) or len(y) != len(confidence): raise ValueError("Lengths of y, iou, and confidence must match.") @@ -1188,22 +1211,22 @@ def optimal_threshold_f1( def best_confidence_threshold( - y: Union[List[int], np.ndarray], - iou: Union[List[float], np.ndarray], - confidence: Union[List[float], np.ndarray], + y: list[int] | np.ndarray, + iou: list[float] | np.ndarray, + confidence: list[float] | np.ndarray, n: int = 100 ) -> float: - """ - Finds the best confidence threshold using bootstrapping and F1 score optimization. + """Find the best confidence threshold using bootstrapping and F1 score optimization. Args: - y (`Union[List[int], np.ndarray]`): Ground truth binary labels. - iou (`Union[List[float], np.ndarray]`): IoU values, non-floats default to 0. - confidence (`Union[List[float], np.ndarray]`): Confidence scores for predictions, non-floats default to 0. - n (`int`, optional): Number of bootstrap samples. Defaults to 100. + y: Ground truth binary labels. + iou: IoU values, non-floats default to 0. + confidence: Confidence scores for predictions, non-floats default to 0. + n: Number of bootstrap samples. Defaults to 100. Returns: - out (`float`): The average of the optimal thresholds found for each bootstrap sample. + The average of the optimal thresholds found for each bootstrap sample. + """ if len(y) != len(iou) or len(y) != len(confidence): raise ValueError("Lengths of y, iou, and confidence must match.") diff --git a/src/flat_bug/geometric.py b/src/flat_bug/geometric.py index 3d8dcd0..6a8a02b 100644 --- a/src/flat_bug/geometric.py +++ b/src/flat_bug/geometric.py @@ -1,6 +1,8 @@ +"""Geometric helper functions for flatbug.""" import math +from collections.abc import Sequence from itertools import accumulate -from typing import List, Tuple, Union +from typing import Literal, TypeVar, overload import cv2 import numpy as np @@ -10,27 +12,30 @@ from flat_bug import logger -def equal_allocate_overlaps(total: int, segments: int, size: int) -> List[int]: - """ - Generates cumulative positions for placing segments of a given size within a total length, with controlled overlaps. +V = TypeVar("V", bound=torch.Tensor | np.ndarray) + + +def equal_allocate_overlaps(total: int, segments: int, size: int) -> list[int]: + """Generate cumulative positions for placing segments of a given size within a total length, with controlled overlaps. This function divides the specified `total` length into `segments` positions, ensuring each segment (of given `size`) fits evenly by introducing a small overlap between adjacent segments. The overlap is distributed uniformly, with the first few gaps adjusted slightly to ensure the segments collectively sum to `total`. Args: - total (`int`): The total length to be covered by the segments. This is the target cumulative length the segments should fit into. - segments (`int`): The number of segments to place within the total length. + total: The total length to be covered by the segments. This is the target cumulative length the segments should fit into. + segments: The number of segments to place within the total length. Must be greater than or equal to 2. - size (`int`): The desired size of each segment, used to determine the ideal spacing between segments. + size: The desired size of each segment, used to determine the ideal spacing between segments. Returns: - out (`List[int]`): A list of cumulative positions (starting from 0) where each segment should be placed. - These positions are spaced with controlled overlaps to ensure they collectively cover the `total` length. + A listt of cumulative positions (starting from 0) where each segment should be placed. + These positions are spaced with controlled overlaps to ensure they collectively cover the `total` length. Example: >>> equal_allocate_overlaps(1000, 5, 250) [0, 187, 374, 562, 750] + """ if segments < 2: return [0] * segments @@ -41,11 +46,11 @@ def equal_allocate_overlaps(total: int, segments: int, size: int) -> List[int]: return list(accumulate([distance - (1 if i < remainder else 0) for i in range(segments - 1)], initial=0)) -def calculate_tile_offsets( - image_size=(int, int), - tile_size=int, - minimum_overlap=int - ) -> List[Tuple[Tuple[int, int], Tuple[int, int]]]: +def calculate_tile_offsets( # noqa: D103 + image_size : tuple[int, int], + tile_size : int, + minimum_overlap : int + ) -> list[tuple[tuple[int, int], tuple[int, int]]]: w, h = image_size x_n_tiles = math.ceil((w - minimum_overlap) / (tile_size - minimum_overlap)) if w != tile_size else 1 y_n_tiles = math.ceil((h - minimum_overlap) / (tile_size - minimum_overlap)) if h != tile_size else 1 @@ -59,9 +64,9 @@ def create_contour_mask( mask: torch.Tensor, width: int=1 ) -> torch.Tensor: - """ - Converts a binary mask for a filled polygon to a binary mask for the non-filled polygon:: + """Convert a binary mask for a filled polygon to a binary mask for the non-filled polygon. + ``` # Before After # # --------- --------- @@ -71,17 +76,20 @@ def create_contour_mask( # --------- --------- # # (here dashes "-" represent 0s and hashes "#" represent 1s) + ``` We call the result ("After") the "contour mask". Optionally, the "linewidth" of the contour mask can be increased. Args: - mask (`torch.Tensor`): a NxM binary tensor with 1s inside the "polygon". - width (`int`, optional): Width of the contour in the result. Reasonable values are >= 1; Setting to 0 will result in all 0s in the output. Defaults to 1. + mask: a NxM binary tensor with 1s inside the "polygon". + width: Width of the contour in the result. + Reasonable values are >= 1; Setting to 0 will result in all 0s in the output. Defaults to 1. Returns: - out (`torch.Tensor`): a NxM binary tensor with 1s on the edge/border of the "polygon". + A NxM binary tensor with 1s on the edge/border of the "polygon". + """ device = mask.device # Kernel to check for 8-neighbors @@ -104,11 +112,17 @@ def create_contour_mask( else: raise ValueError(f"Invalid width: {width}") + +@overload +def find_contours(mask : torch.Tensor, largest_only : Literal[True], simplify : bool=True) -> torch.Tensor: ... +@overload +def find_contours(mask : torch.Tensor, largest_only : Literal[False], simplify : bool=True) -> list[torch.Tensor]: ... def find_contours( mask : torch.Tensor, largest_only : bool=True, simplify : bool=True - ) -> Union[torch.Tensor, List[torch.Tensor]]: + ) -> torch.Tensor | list[torch.Tensor]: + """Extract polygons from a boolean mask.""" contour = cv2.findContours(mask.to(torch.uint8).cpu().numpy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[0] if len(contour) == 0: logger.info("No contours found; mask shape:", mask.shape, "mask sum:", mask.sum()) @@ -117,7 +131,7 @@ def find_contours( # Calculate areas of each contour areas = np.array([cv2.contourArea(c) for c in contour]) # Select the largest contour and convert it to a tensor - contour = contour[np.argmax(areas)] + contour = contour[np.argmax(areas).item()] if simplify: contour = simplify_contour(contour, tolerance=1 if isinstance(simplify, bool) else simplify) # Convert to tensor @@ -126,48 +140,61 @@ def find_contours( else: return torch.tensor(contour, dtype=torch.long, device=mask.device).squeeze(1) +@overload +def simplify_contour(contour : V, tolerance : float=1.0) -> V: ... +@overload +def simplify_contour(contour : Sequence[V], tolerance : float=1.0) -> list[V]: ... def simplify_contour( - contour : Union[torch.Tensor, np.ndarray], + contour : V | Sequence[V], tolerance : float=1.0 - ) -> Union[torch.Tensor, np.ndarray]: - """ + ) -> V | list[V]: + """Simplify one or more polygons via cv2.approxPolyDP. + Wrapper for cv2.approxPolyDP that simplifies a contour by reducing the number of points while keeping the shape of the contour. Only works for simple closed contours without holes. Args: - contour (`Union[torch.Tensor, np.ndarray]`): The contour to simplify, represented as a Nx2 tensor or a Nx1x2 tensor. - tolerance (`float`, optional): The maximum distance between the original contour and the simplified contour. Defaults to 1.0. + contour: The contour to simplify, represented as a Nx2 tensor or a Nx1x2 tensor. + tolerance: The maximum distance between the original contour and the simplified contour. Defaults to 1.0. Returns: - out (`Union[torch.Tensor, np.ndarray]`): The simplified contour in the same format as the input. + The simplified contour in the same format as the input. + """ - if isinstance(contour, list): + if not isinstance(contour, (torch.Tensor, np.ndarray)) and hasattr(contour, "__iter__"): return [simplify_contour(c, tolerance) for c in contour] else: - isTensor = isinstance(contour, torch.Tensor) - if isTensor: + if isinstance(contour, torch.Tensor): device, dtype = contour.device, contour.dtype - contour = contour.cpu().numpy().astype(np.int32) - simplied_contour = cv2.approxPolyDP(contour, tolerance, True) - if isTensor: - simplied_contour = torch.tensor(simplied_contour, dtype=dtype, device=device).squeeze(1) - return simplied_contour + return torch.as_tensor( + cv2.approxPolyDP(contour.cpu().numpy().astype(np.int32), tolerance, True), + device=device, dtype=dtype + ) + elif isinstance(contour, np.ndarray): + return np.asarray(cv2.approxPolyDP(contour, tolerance, True)) + raise TypeError( + f'Unable to simplify contour of type {type(contour).__name__}, ' + 'expected a torch.Tensor or np.ndarray or an iterable of such.' + ) + def contours_to_masks( - contours : List[torch.Tensor], - height : Union[int, torch.Tensor], - width : Union[int, torch.Tensor] + contours : list[torch.Tensor], + height : int | torch.Tensor, + width : int | torch.Tensor ) -> torch.Tensor: - """ - Takes a list of contours represented as (i, j) index-coordinates in a Xx2 tensor and returns a NxHxW tensor of boolean masks with the contours filled in. + """Rasterize a list of countors to a NxHxW boolean tensor stack. + + Contours should be represented as (i, j) index-coordinates in a Xx2 tensor. Args: - contours (`List[torch.Tensor]`): List of contours represented as (i, j) index-coordinates in a Nx2 tensor (OBS: dtype=torch.long) - height (`Union[int, torch.Tensor]`): The height of the masks - width (`Union[int, torch.Tensor]`): The width of the masks + contours: List of contours represented as (i, j) index-coordinates in a Nx2 tensor (OBS: dtype=torch.long). + height: The height of the masks. + width: The width of the masks. Returns: - out (`torch.Tensor`): NxHxW tensor of boolean masks with the contours filled in + NxHxW tensor of boolean masks with the contours filled in. + """ device = contours[0].device N = len(contours) @@ -177,17 +204,20 @@ def contours_to_masks( assert all(len(c.shape) == 2 and c.shape[1] == 2 for c in contours), "All contours must be Xx2 tensors" if isinstance(height, torch.Tensor): assert height.numel() == 1, f"Height must be a scalar tensor not {height.shape}" - height = height.item() - assert isinstance(height, int), f"Height must be an integer not {height}" - height = int(height) + int_height = height.item() + else: + int_height = height + int_height = int(int_height) if isinstance(width, torch.Tensor): assert width.numel() == 1, f"Width must be a scalar tensor not {width.shape}" - width = width.item() - assert isinstance(width, int), f"Width must be an integer not {width}" - assert height > 0 and width > 0, f"Height and width must be positive not {height} and {width}" + int_width = width.item() + else: + int_width = width + int_width = int(int_width) + assert int_height > 0 and int_width > 0, f"Height and width must be positive not {int_height} and {int_width}" # Initialize the masks as UMATs - masks = np.zeros((N, height, width), dtype=np.uint8) + masks = np.zeros((N, int_height, int_width), dtype=np.uint8) # If there are no contours, return the empty masks if N == 0: # Convert to tensors @@ -202,44 +232,42 @@ def contours_to_masks( @torch.jit.script def poly_area(poly : torch.Tensor) -> float: - """ - Calculates the area of a 2D simple polygon represented by a positively oriented (counter clock wise) sequence of points. + """Calculate the area of a 2D simple polygon represented by a positively oriented (counter clock wise) sequence of points. See https://en.wikipedia.org/wiki/Shoelace_formula#Shoelace_formula for details. Args: - poly (`torch.Tensor`): A tensor of shape (n, 2), where n is the number of vertices and the 2 columns are the x and y coordinates of the vertices. + poly: A tensor of shape (n, 2), where n is the number of vertices and the 2 columns are the x and y coordinates of the vertices. Returns: - out (`float`): The area of the polygon + The area of the polygon + """ if len(poly) < 10e4: poly = poly.cpu() poly_r = poly.roll(1, 0) return (poly[:, 0] @ poly_r[:, 1] - poly[:, 1] @ poly_r[:, 0]).item() / 2 -def poly_normals(polygon : torch.Tensor) -> torch.Tensor: - """ - Calculates the normals of a polygon. +def poly_normals(polygon : torch.Tensor | np.ndarray) -> torch.Tensor: + """Calculate the normals of a polygon. Args: - poly (`torch.Tensor`): A tensor of shape (n, 2), where n is the number of vertices and the 2 columns are the x and y coordinates of the vertices. + polygon: A tensor of shape (n, 2), where n is the number of vertices and the 2 columns are the x and y coordinates of the vertices. Returns: - out (`torch.Tensor`): A tensor of shape (n, 2), where n is the number of vertices and the 2 columns are the x and y coordinates of the normals. + A tensor of shape (n, 2), where n is the number of vertices and the 2 columns are the x and y coordinates of the normals. + """ v = np.roll(polygon, -1, axis=0) - polygon n = np.column_stack([v[:, 1], -v[:, 0]]) n = (n + np.roll(n, 1, axis=0)) / 2 - return n + return torch.as_tensor(n) def linear_interpolate( poly : np.ndarray, scale : int ) -> np.ndarray: - """ - Linearly interpolates a N x 2 polygon to have N x scale vertices. - """ + """Linearly interpolates a N x 2 polygon to have N x scale vertices.""" if scale < 1: raise ValueError(f"Scale must be at least 1, not {scale}") if len(poly) == 0: @@ -253,9 +281,9 @@ def linear_interpolate( new_poly[-scale:] = np.linspace(poly[-1], poly[0], scale, endpoint=False) return new_poly[~(new_poly == np.roll(new_poly, -1, axis=0)).all(axis=1)] -def scale_contour( +def scale_contour( # noqa: D103 contour : np.ndarray, - scale : Union[List[Union[float, int]], np.ndarray, float, int], + scale : list[float | int] | np.ndarray | float | int, expand_by_one : bool=False ) -> np.ndarray: if len(contour.shape) != 2 or contour.shape[1] != 2: @@ -296,29 +324,30 @@ def scale_contour( drift = centroid - contour.mean(axis=0) return (contour + drift).round().astype(np.int32)[(n_interp // 2)::n_interp].copy() -def resize_mask( +def resize_masks( masks : torch.Tensor, - new_shape : Union[Tuple[int, int], List[int]] + new_shape : tuple[int, int] | list[int] | int ) -> torch.Tensor: - """ - Takes a mask (or a batch of masks) and resizes it by scaling the contour coordinates and snapping to the integer grid, - ensuring that snapping is always done towards the outside of the mask. + """Resize a mask (or a batch of masks) by scaling the contour coordinates and snapping to the integer grid. + + Ensures that snapping is always done towards the outside of the mask. Args: - mask (`torch.Tensor`): A mask of shape (H, W) or (N, H, W) where N is the batch size. - new_shape (`Tuple[int, int] | List[int]`): The new shape of the mask (H', W'). + masks: A mask of shape (H, W) or (N, H, W) where N is the batch size. + new_shape: The new shape of the mask (H', W'). Returns: - out (`torch.Tensor`): The resized mask of shape (H', W') or (N, H', W'). + The resized mask of shape (H', W') or (N, H', W'). + """ # If the mask is a not a batch of masks, unsqueeze and call the function again if len(masks.shape) == 2: - return resize_mask(masks.unsqueeze(0), new_shape).squeeze(0) + return resize_masks(masks.unsqueeze(0), new_shape).squeeze(0) # If the mask is already the target shape, return it if masks.shape[1:] == new_shape: return masks # If the target shape is smaller than 2x2, raise an error - if new_shape[0] <= 1 or new_shape[1] <= 1: + if not isinstance(new_shape, int) and (new_shape[0] <= 1 or new_shape[1] <= 1): raise ValueError(f"Target shape must be at least 2x2, not {new_shape}") # Resize the mask return F.interpolate(masks.float()[None], new_shape, mode='nearest-exact', antialias=False)[0] > 0.5 @@ -327,17 +356,17 @@ def resize_mask( def chw2hwc_uint8( crop : torch.Tensor, - mask : torch.Tensor + mask : torch.Tensor | None ) -> torch.Tensor: - """ - Converts a crop from CHW to HWC format, and adds the mask as an alpha channel if it exists. + """Convert a crop from CHW to HWC format, and adds the mask as an alpha channel if it exists. Args: - crop (`torch.Tensor`): The crop to convert from CHW to HWC format. - mask (`torch.Tensor`): The mask to add as an alpha channel. + crop: The crop to convert from CHW to HWC format. + mask: The mask to add as an alpha channel. Returns: - out (`torch.Tensor`): The crop in HWC format with the mask as an alpha channel, if supplied. + The crop in HWC format with the mask as an alpha channel, if supplied. + """ crop = _to_uint8(crop) if mask is not None: diff --git a/src/flat_bug/nms.py b/src/flat_bug/nms.py index 352787a..67e500f 100644 --- a/src/flat_bug/nms.py +++ b/src/flat_bug/nms.py @@ -1,28 +1,32 @@ +"""Implementations of non-maximum suppression for boxes, polygons and masks used in flatbug inference.""" +from collections.abc import Callable from functools import partial -from typing import Any, Callable, List, Optional, Tuple, Union +from typing import Any, Literal, cast, overload import numpy as np +import scipy.sparse import shapely import torch import torchvision -import scipy.sparse - def iou_boxes( rectangles : torch.Tensor, - other_rectangles : Optional[torch.Tensor]=None + other_rectangles : torch.Tensor | None=None ) -> torch.Tensor: - """ - Calculates the intersection over union (IoU) of a set of rectangles. + """Calculate the intersection over union (IoU) of a set of rectangles. Args: - rectangles (`torch.Tensor`): A tensor of shape (n, 4), where n is the number of rectangles and the 4 columns are the x_min, y_min, x_max and y_max coordinates of the rectangles. - other_rectangles (`Optional[torch.Tensor]`, optional): A tensor of shape (m, 4), where m is the number of rectangles and the 4 columns are the x_min, y_min, x_max and y_max coordinates of the rectangles. + rectangles: A tensor of shape (n, 4), where n is the number of rectangles + and the 4 columns are the x_min, y_min, x_max and y_max coordinates of the rectangles. + other_rectangles: A tensor of shape (m, 4), where m is the number of rectangles + and the 4 columns are the x_min, y_min, x_max and y_max coordinates of the rectangles. Defaults to None, in which case the symmetric IoU of the rectangles with themselves is calculated. Returns: - out (`torch.Tensor`): A tensor of shape (n, n), where n is the number of rectangles, containing the IoU of each rectangle with each other rectangle. + A tensor of shape (n, n), where n is the number of rectangles, + containing the IoU of each rectangle with each other rectangle. + """ if not isinstance(rectangles, torch.Tensor): raise ValueError(f"Rectangles must be a tensor, not {type(rectangles)}") @@ -46,18 +50,21 @@ def iou_boxes( def ios_boxes( rectangles : torch.Tensor, - other_rectangles : Optional[torch.Tensor]=None + other_rectangles : torch.Tensor | None=None ) -> torch.Tensor: - """ - Calculates the intersection over smaller (IoS) of a set of rectangles. + """Calculate the intersection over smaller (IoS) of a set of rectangles. Args: - rectangles (`torch.Tensor`): A tensor of shape (n, 4), where n is the number of rectangles and the 4 columns are the x_min, y_min, x_max and y_max coordinates of the rectangles. - other_rectangles (`Optional[torch.Tensor]`, optional): A tensor of shape (m, 4), where m is the number of rectangles and the 4 columns are the x_min, y_min, x_max and y_max coordinates of the rectangles. + rectangles: A tensor of shape (n, 4), where n is the number of rectangles + and the 4 columns are the x_min, y_min, x_max and y_max coordinates of the rectangles. + other_rectangles: A tensor of shape (m, 4), where m is the number of rectangles + and the 4 columns are the x_min, y_min, x_max and y_max coordinates of the rectangles. Defaults to None, in which case the symmetric IoS of the rectangles with themselves is calculated. Returns: - out (`torch.Tensor`): A tensor of shape (n, n), where n is the number of rectangles, containing the IoS of each rectangle with each other rectangle. + A tensor of shape (n, n), where n is the number of rectangles, + containing the IoS of each rectangle with each other rectangle. + """ if not isinstance(rectangles, torch.Tensor): raise ValueError(f"Rectangles must be a tensor, not {type(rectangles)}") @@ -84,12 +91,11 @@ def ios_boxes( def iou_masks( m1s : torch.Tensor, m2s : torch.Tensor, - a1s : Union[torch.Tensor, None]=None, - a2s : Union[torch.Tensor, None]=None, + a1s : torch.Tensor | None=None, + a2s : torch.Tensor | None=None, dtype : torch.dtype=torch.float32 ) -> torch.Tensor: - """ - Computes IoU between all pairs between two sets of masks. + """Compute IoU between all pairs between two sets of masks. The IoU is calculated using the formula: @@ -97,19 +103,21 @@ def iou_masks( `intersection[i, j] = (m1s[i] * m2s[j]).sum()` - The reason the intersection is calculated this way is that it can be vectorized and calculated in a single matrix multiplication for all pairs of masks. + The reason the intersection is calculated this way is that it can be vectorized + and calculated in a single matrix multiplication for all pairs of masks. OBS: Results will only be valid for boolean or masks containing only 0s and 1s. Args: - m1s (`torch.Tensor`): A tensor of shape (n, h, w), where n is the number of masks and h and w are the height and width of the masks. - m2s (`torch.Tensor`): A tensor of shape (m, h, w), where m is the number of masks and h and w are the height and width of the masks. - a1s (`Optional[torch.Tensor]`, optional): A tensor of shape (n, ) containing the areas of the masks in m1s. Defaults to None, in which case the areas are calculated. - a2s (`Optional[torch.Tensor]`, optional): A tensor of shape (m, ) containing the areas of the masks in m2s. Defaults to None, in which case the areas are calculated. - dtype (`torch.dtype`, optional): The data type of the output tensor. Defaults to torch.float32. + m1s: A tensor of shape (n, h, w), where n is the number of masks and h and w are the height and width of the masks. + m2s: A tensor of shape (m, h, w), where m is the number of masks and h and w are the height and width of the masks. + a1s: A tensor of shape (n, ) containing the areas of the masks in m1s. Defaults to None, in which case the areas are calculated. + a2s: A tensor of shape (m, ) containing the areas of the masks in m2s. Defaults to None, in which case the areas are calculated. + dtype: The data type of the output tensor. Defaults to torch.float32. Returns: - out (`torch.Tensor`): A tensor of shape (n, m) containing the IoU of each pair of masks. + A tensor of shape (n, m) containing the IoU of each pair of masks. + """ # 1. Standardize Inputs: Ensure batch dim and flatten spatial dims (N, H, W) -> (N, P) if m1s.dim() == 2: @@ -137,15 +145,14 @@ def iou_masks( return intersections / (unions + 1e-6) @torch.jit.script -def ios_masks( +def ios_masks( # noqa: D103 m1s : torch.Tensor, m2s : torch.Tensor, - a1s : Union[torch.Tensor, None]=None, - a2s : Union[torch.Tensor, None]=None, + a1s : torch.Tensor | None=None, + a2s : torch.Tensor | None=None, dtype : torch.dtype=torch.float32 ) -> torch.Tensor: - """ - Computes IoS (Intersection over Smaller area) between all pairs between two sets of masks. + """Compute IoS (Intersection over Smaller area) between all pairs between two sets of masks. The IoS is calculated using the formula: @@ -153,19 +160,21 @@ def ios_masks( `intersection[i, j] = (m1s[i] * m2s[j]).sum()` - The reason the intersection is calculated this way is that it can be vectorized and calculated in a single matrix multiplication for all pairs of masks. + The reason the intersection is calculated this way is that it can be vectorized + and calculated in a single matrix multiplication for all pairs of masks. OBS: Results will only be valid for boolean or masks containing only 0s and 1s. Args: - m1s (`torch.Tensor`): A tensor of shape (n, h, w), where n is the number of masks and h and w are the height and width of the masks. - m2s (`torch.Tensor`): A tensor of shape (m, h, w), where m is the number of masks and h and w are the height and width of the masks. - a1s (`Optional[torch.Tensor]`, optional): A tensor of shape (n, ) containing the areas of the masks in m1s. Defaults to None, in which case the areas are calculated. - a2s (`Optional[torch.Tensor]`, optional): A tensor of shape (m, ) containing the areas of the masks in m2s. Defaults to None, in which case the areas are calculated. - dtype (`torch.dtype`, optional): The data type of the output tensor. Defaults to torch.float32. + m1s: A tensor of shape (n, h, w), where n is the number of masks and h and w are the height and width of the masks. + m2s: A tensor of shape (m, h, w), where m is the number of masks and h and w are the height and width of the masks. + a1s: A tensor of shape (n, ) containing the areas of the masks in m1s. Defaults to None, in which case the areas are calculated. + a2s: A tensor of shape (m, ) containing the areas of the masks in m2s. Defaults to None, in which case the areas are calculated. + dtype: The data type of the output tensor. Defaults to torch.float32. Returns: - out (`torch.Tensor`): A tensor of shape (n, m) containing the IoS of each pair of masks. + A tensor of shape (n, m) containing the IoS of each pair of masks. + """ # 1. Standardize Inputs: Ensure batch dim and flatten spatial dims (N, H, W) -> (N, P) if m1s.dim() == 2: @@ -192,9 +201,10 @@ def ios_masks( return intersections / (amin + 1e-6) -def iou_polygons( - polygons1: Union[List[torch.Tensor], np.ndarray], - polygons2: Optional[Union[List[torch.Tensor], np.ndarray]] = None +def iou_polygons( # noqa: D103 + polygons1: list[torch.Tensor] | np.ndarray, + polygons2: list[torch.Tensor] | np.ndarray | None = None, + *args, **kwargs ) -> np.ndarray: if len(polygons1) == 0: @@ -226,9 +236,10 @@ def ensure_geoms(objs: Any) -> np.ndarray: return iou_mat -def ios_polygons( - polygons1: Union[List[torch.Tensor], np.ndarray], - polygons2: Optional[Union[List[torch.Tensor], np.ndarray]] = None +def ios_polygons( # noqa: D103 + polygons1: list[torch.Tensor] | np.ndarray, + polygons2: list[torch.Tensor] | np.ndarray | None = None, + *args, **kwargs ) -> np.ndarray: if len(polygons1) == 0: @@ -259,46 +270,76 @@ def ensure_geoms(objs: Any) -> np.ndarray: return ios_mat +@overload +def base_nms_( + objects : Any, + overlap_fn : Callable, + scores : torch.Tensor, + collate_fn : Callable | None=None, + overlap_threshold : float=0.5, + strict : bool=True, + return_indices : Literal[False]=False, + **kwargs + ) -> tuple[Any, torch.Tensor]: ... +@overload +def base_nms_( + objects : Any, + overlap_fn : Callable, + scores : torch.Tensor, + collate_fn : Callable | None=None, + overlap_threshold : float=0.5, + strict : bool=True, + return_indices : Literal[True]=True, + **kwargs + ) -> torch.Tensor: ... def base_nms_( objects : Any, overlap_fn : Callable, scores : torch.Tensor, - collate_fn : Callable=None, + collate_fn : Callable | None=None, overlap_threshold : float=0.5, strict : bool=True, return_indices : bool=False, **kwargs - ) -> Union[torch.Tensor, Tuple[Any, torch.Tensor]]: - """ - Implements the standard non-maximum suppression algorithm. + ) -> torch.Tensor | tuple[Any, torch.Tensor]: + """Perform the standard non-maximum suppression algorithm. Args: - objects (`Any`): An object which can be indexed by a tensor of indices. - overlap_fn (`Callable`): A function which takes an anchor object and a comparison set (not in the Python sense) of (different) objects and returns the IoU of the anchor object with each object in the comparison set as a tensor of shape (1, n). - The reason it is not just (n, ) is to allow for implementations of `overlap_fn` functions between two sets, where the IoU is calculated between each pair of objects from distinct sets. - scores (`torch.Tensor`): A tensor of shape (n, ) containing the "scores" of the objects, this can merely be though of as a priority score, where the higher the score, the higher the priority of the object - it does not have to be a probability/confidence. - collate_fn (`Callable`, optional): A function which takes a list of objects and returns a single combined object. Defaults to `torch.cat` if `objects` is a tensor and `lambda x : x` if `objects` is a list, otherwise it has to be specified. - overlap_threshold (`float`, optional): The overlap (e.g. IoU) threshold for non-maximum suppression. Defaults to 0.5. - strict (`bool`, optional): A flag to indicate whether to perform strict checks on the algorithm. Defaults to True. - return_indices (`bool`, optional): A flag to indicate whether to return the indices of the picked objects or the objects themselves. Defaults to False. If True, both the picked objects and scores are returned. + objects: An object which can be indexed by a tensor of indices. + overlap_fn: A function which takes an anchor object and a comparison set (not in the Python sense) of (different) objects + and returns the IoU of the anchor object with each object in the comparison set as a tensor of shape (1, n). + The reason it is not just (n, ) is to allow for implementations of `overlap_fn` functions between two sets, + where the IoU is calculated between each pair of objects from distinct sets. + scores: A tensor of shape (n, ) containing the "scores" of the objects, this can merely be though of as a priority score, + where the higher the score, the higher the priority of the object - it does not have to be a probability/confidence. + collate_fn: A function which takes a list of objects and returns a single combined object. + Defaults to `torch.cat` if `objects` is a tensor and `lambda x : x` if `objects` is a list, otherwise it has to be specified. + overlap_threshold: The overlap (e.g. IoU) threshold for non-maximum suppression. Defaults to 0.5. + strict: A flag to indicate whether to perform strict checks on the algorithm. Defaults to True. + return_indices: A flag to indicate whether to return the indices of the picked objects or the objects themselves. + Defaults to False. If True, both the picked objects and scores are returned. **kwargs: Additional keyword arguments to be passed to the overlap_fn function. Returns: - out (`Union[torch.Tensor, Tuple[Any, torch.Tensor]]`): - - `torch.Tensor`: A tensor of shape `(m,)` containing the indices of the picked objects. - - `Tuple[Any, torch.Tensor]`: A tuple where the first element contains the picked objects and the second element is a tensor of their scores. + Either a tensor of shape `(m,)` containing the indices of the picked objects, + or a tuple (`tuple[Any, torch.Tensor]`) where the first element contains + the picked objects and the second element is a tensor of their scores. + """ if collate_fn is None: if isinstance(objects, torch.Tensor): collate_fn = torch.cat elif isinstance(objects, list): - collate_fn = lambda x : x + collate_fn = lambda x : x # noqa: E731 else: raise ValueError(f"collate_fn must be specified for objects of type {type(objects)}") + + device = scores.device if len(scores.shape) != 1: raise ValueError(f"Scores must be of shape (n,), not {scores.shape}") - if len(objects) == 0 or len(objects) == 1: + N = len(objects) + if N == 0 or N == 1: if return_indices: return torch.arange(len(objects)) else: @@ -309,11 +350,9 @@ def base_nms_( # Initialize tensors for winners (selected boxes), possible boxes and counters winners = [] - possible = torch.ones((len(objects),), dtype=torch.bool, device=objects.device) + possible = torch.ones((len(objects),), dtype=torch.bool, device=device) left = len(objects) - i, n = 0, 0 - - while True: + for i in range(N): possible_idx = possible.nonzero().squeeze() n_possible = possible_idx.numel() if n_possible < 2: @@ -336,13 +375,12 @@ def base_nms_( # In/Decrement the counters increment = (~winner_mask).sum().item() + 1 left -= increment - n += 1 assert left == (possible_idx.numel() - 1), f"left ({left}) != possible_idx.numel() - 1 ({possible_idx.numel() - 1})" - assert n == len(winners), f"n ({n}) != winners.sum() ({len(winners)})" + assert (i + 1) == len(winners), f"n ({i + 1}) != winners.sum() ({len(winners)})" # Map the indices back to the original indices and sort them (returns boxes, scores & indices in the original order of the input) - winners = torch.tensor(winners, dtype=torch.long, device=objects.device) + winners = torch.tensor(winners, dtype=torch.long, device=device) winners = indices[winners].sort().values # Return the boxes and scores that were picked @@ -355,32 +393,39 @@ def fancy_nms( objects : Any, overlap_fn : Callable, scores : torch.Tensor, - overlap_threshold : Union[float, int]=0.5, + overlap_threshold : float | int=0.5, return_indices : bool=False - ) -> Union[torch.Tensor, Tuple[Any, torch.Tensor]]: - """ - This is a 'fancy' implementation of non-maximum suppression. It is not as fast as the non-maximum suppression algorithm, nor does it follow the exact same algorithm, but it is more readable and easier to debug. + ) -> torch.Tensor | tuple[Any, torch.Tensor]: + """Perform a 'fancy' implementation of non-maximum suppression (NMS). + + It is not as fast as the non-maximum suppression algorithm, + nor does it follow the exact same algorithm, but it is more readable and easier to debug. The algorithm works as follows: 1. Sort the objects by score (implicitly) 2. Calculate the overlap (e.g. IoU) matrix 3. Create a boolean matrix where overlap > overlap_threshold 4. Fold the boolean matrix sequentially (i.e. row_i = row_i + row_i-1 + ... + row_0) - (The values on the diagonal of the matrix now correspond to the number of higher-priority objects that suppress the current object, including itself) + (The values on the diagonal of the matrix now correspond to the number + of higher-priority objects that suppress the current object, including itself) 5. objects which are suppressed only by themselves are returned. Args: - objects (`Any`): Any object collection that can be indexed by a tensor, where the first dimension corresponds to the objects. - overlap_fn (`Callable`): A function that calculates the symmetric overlap (e.g. IoU) matrix of a set of objects returned as a `torch.Tensor` of shape (n, n), where n is the number of objects. The device should match the device of the scores. - scores (`torch.Tensor`): A tensor of shape (n, ) containing the scores of the objects. - overlap_threshold (`Union[float, int]`, optional): The overlap (e.g. IoU) threshold for non-maximum suppression. Defaults to 0.5. - return_indices (`bool`, optional): A flag to indicate whether to return the indices of the picked objects or the objects themselves. Defaults to False. If True, both the picked objects and scores are returned. + objects: Any object collection that can be indexed by a tensor, where the first dimension corresponds to the objects. + overlap_fn: A function that calculates the symmetric overlap (e.g. IoU) matrix + of a set of objects returned as a `torch.Tensor` of shape (n, n), + where n is the number of objects. The device should match the device of the scores. + scores: A tensor of shape (n, ) containing the scores of the objects. + overlap_threshold: The overlap (e.g. IoU) threshold for non-maximum suppression. Defaults to 0.5. + return_indices: A flag to indicate whether to return the indices of the picked objects or the objects themselves. + Defaults to False. If True, both the picked objects and scores are returned. Returns: - out (`Union[torch.Tensor, Tuple[Any, torch.Tensor]]`): - - `torch.Tensor`: A tensor of shape `(m,)` containing the indices of the picked objects. - - `Tuple[Any, torch.Tensor]`: A tuple where the first element contains the picked objects and the second element is a tensor of their scores. + Either a tensor containing the indices of the picked objects, + or a tuple (`tuple[Any, torch.Tensor`) where the first element contains + the picked objects and the second element is a tensor of their scores. + """ if not len(objects.shape) == 2: raise ValueError(f"Boxes must be of shape (n, x), not {objects.shape}") @@ -404,7 +449,8 @@ def fancy_nms( # Fold the overlap matrix sequentially (i.e. row_i = row_i + row_i-1 + ... + row_0) overlaps = (overlaps > overlap_threshold).cumsum(dim=1) <= 1 - # The boxes with an overlap greater than the threshold are the elements on the diagonal of the folded overlap matrix which are one (suppressed only by itself) + # The boxes with an overlap greater than the threshold are the elements on + # the diagonal of the folded overlap matrix which are one (suppressed only by itself) indices = indices[torch.where(overlaps.diagonal())[0]] if return_indices: @@ -417,18 +463,19 @@ def nms_masks_( masks : torch.Tensor, scores : torch.Tensor, overlap_threshold : float=0.5, - overlap_fn : Callable[[torch.Tensor, torch.Tensor], torch.Tensor]=iou_masks + overlap_fn : Callable[[torch.Tensor, torch.Tensor, torch.Tensor | None, torch.Tensor | None, torch.dtype], torch.Tensor]=iou_masks ) -> torch.Tensor: - """ - Performs non-maximum suppression on a set of masks. + """Perform non-maximum suppression (NMS) on a set of masks. Args: - masks (`torch.Tensor`): A tensor of shape (n, h, w), where n is the number of masks and h and w are the height and width of the masks. - scores (`torch.Tensor`): A tensor of shape (n, ) containing the scores of the masks. - overlap_threshold (`float`, optional): The overlap (e.g. IoU) threshold for non-maximum suppression. Defaults to 0.5. + masks: A tensor of shape (n, h, w), where n is the number of masks and h and w are the height and width of the masks. + scores: A tensor of shape (n, ) containing the scores of the masks. + overlap_threshold: The overlap (e.g. IoU) threshold for non-maximum suppression. Defaults to 0.5. + overlap_fn: A function to compute overlaps between masks. Returns: - out (`torch.Tensor`): A tensor containing the indices of the picked masks. + A tensor containing the indices of the picked masks. + """ N, device = len(scores), masks.device if N <= 1: @@ -461,9 +508,9 @@ def nms_masks_( overlaps = overlap_fn( masks[possible_idx[0:1]].unsqueeze(1), masks[possible_idx[1:]].unsqueeze(1), - a1s=areas[possible_idx[0:1]], - a2s=areas[possible_idx[1:]], - dtype=torch.float32 + areas[possible_idx[0:1]], + areas[possible_idx[1:]], + torch.float32 ).squeeze(0) winner_mask = overlaps <= overlap_threshold @@ -472,8 +519,8 @@ def nms_masks_( return indices[winners[:i]].sort().values -def nms_polygons_( - polys : List[torch.Tensor], +def nms_polygons_( # noqa: D103 + polys : list[torch.Tensor], scores : torch.Tensor, overlap_threshold : float=0.5, overlap_fn : Callable[[np.ndarray, np.ndarray], np.ndarray]=iou_polygons @@ -483,7 +530,7 @@ def nms_polygons_( return torch.arange(N, device=device) scores_np = scores.cpu().numpy() - geoms = np.array([shapely.polygons(p.cpu().numpy()).buffer(0) for p in polys]) + geoms = np.array([cast(shapely.Polygon, shapely.polygons(p.cpu().numpy())).buffer(0) for p in polys]) indices = np.argsort(scores_np)[::-1] # Ascending sort -> reverse for descending geoms = geoms[indices] @@ -525,9 +572,9 @@ def nms_polygons_( def cluster_overlap_boxes( boxes: torch.Tensor, overlap_threshold: float = 0.5, - overlap_fn: Callable[[torch.Tensor], torch.Tensor] = iou_boxes, + overlap_fn: Callable[[torch.Tensor, torch.Tensor], torch.Tensor] = iou_boxes, time: bool = False - ) -> Tuple[List[torch.Tensor], torch.Tensor]: + ) -> tuple[list[torch.Tensor], torch.Tensor]: """Cluster boxes via connected components. Note: Implementation relies on ``overlap_fn`` being symmetric (e.g. IoU/IoS). @@ -581,7 +628,7 @@ def cluster_overlap_boxes( return list(groups), cluster_vec -OVERLAP_FNS : dict[str, dict[str, Callable[[torch.Tensor], torch.Tensor]]] = { +OVERLAP_FNS : dict[str, dict[str, Callable]] = { "polygon" : { "iou" : iou_polygons, "ios" : ios_polygons @@ -596,15 +643,52 @@ def cluster_overlap_boxes( } } -def get_overlap_fn(geometry : str, metric : str): +def get_overlap_fn(geometry : str, metric : str): # noqa: D103 geometry, metric = geometry.lower().strip(), metric.lower().strip() if geometry not in OVERLAP_FNS: - raise NotImplementedError(f'No overlap metrics implemented for geometry type: "{geometry}", valid options are [{", ".join(OVERLAP_FNS.keys())}]') + raise NotImplementedError( + f'No overlap metrics implemented for geometry type: "{geometry}", ' + + 'valid options are [{}]'.format( + ", ".join(OVERLAP_FNS.keys()) + ) + ) options = OVERLAP_FNS[geometry] if metric not in options: - raise NotImplementedError(f'Overlap metric: "{metric}" not implemented for geometry type: "{geometry}", valid options are [{", ".join(options.keys())}]') + raise NotImplementedError( + f'Overlap metric: "{metric}" not implemented for geometry type: "{geometry}", ' + + 'valid options are [{}]'.format( + ", ".join(options.keys()) + ) + ) return options[metric] + +@overload +def nms_masks( + masks : torch.Tensor, + scores : torch.Tensor, + overlap_threshold : float=0.5, + return_indices : Literal[False]=False, + group_first : bool=True, + boxes : torch.Tensor | None=None, + overlap_fn : ( + Callable[[torch.Tensor, torch.Tensor, torch.Tensor | None, torch.Tensor | None, torch.dtype], torch.Tensor] | str + )=iou_masks, + overlap_fn_boxes : Callable[..., torch.Tensor] | str | None=None + ) -> tuple[torch.Tensor, torch.Tensor]: ... +@overload +def nms_masks( + masks : torch.Tensor, + scores : torch.Tensor, + overlap_threshold : float=0.5, + return_indices : Literal[True]=True, + group_first : bool=True, + boxes : torch.Tensor | None=None, + overlap_fn : ( + Callable[[torch.Tensor, torch.Tensor, torch.Tensor | None, torch.Tensor | None, torch.dtype], torch.Tensor] | str + )=iou_masks, + overlap_fn_boxes : Callable[..., torch.Tensor] | str | None=None + ) -> torch.Tensor: ... # @torch.jit.script def nms_masks( masks : torch.Tensor, @@ -612,27 +696,34 @@ def nms_masks( overlap_threshold : float=0.5, return_indices : bool=False, group_first : bool=True, - boxes : torch.Tensor=None, - overlap_fn : Callable[[torch.Tensor], torch.Tensor] | str=iou_masks, - overlap_fn_boxes : Optional[Union[Callable[[torch.Tensor], torch.Tensor], str]]=None - ) -> Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]: - """ - Efficiently perform non-maximum suppression on a set of boolean masks. + boxes : torch.Tensor | None=None, + overlap_fn : ( + Callable[[torch.Tensor, torch.Tensor, torch.Tensor | None, torch.Tensor | None, torch.dtype], torch.Tensor] | str + )=iou_masks, + overlap_fn_boxes : Callable[..., torch.Tensor] | str | None=None + ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]: + """Efficiently perform non-maximum suppression on a set of boolean masks. Defaults to a modified two-stage NMS algorithm, that aims to minimize the number of mask intersection calculations needed. Args: - masks (`torch.Tensor`): A tensor of shape (n, h, w), where n is the number of masks and h and w are the height and width of the masks. - scores (`torch.Tensor`): A tensor of shape (n, ) containing the "scores" of the masks, this can merely be though of as a priority score, where the higher the score, the higher the priority of the object - it does not have to be a probability/confidence. - overlap_threshold (`float`, optional): The overlap (e.g. IoU) threshold for non-maximum suppression. Defaults to 0.5. - return_indices (`bool`, optional): A flag to indicate whether to return the indices of the picked objects or the objects themselves. Defaults to False. If True, both the picked objects and scores are returned. - group_first (`bool`, optional): A flag to indicate whether two use the two-stage NMS method. Defaults to True. - boxes (`Optional[torch.Tensor]`, optional): Bounding boxes for the masks. A tensor of shape (n, 4), where n is the number of masks and the 4 columns are the x_min, y_min, x_max and y_max coordinates of the bounding boxes. + masks: A tensor of shape (n, h, w), where n is the number of masks and h and w are the height and width of the masks. + scores: A tensor of shape (n, ) containing the "scores" of the masks, this can merely be though of as a priority score, + where the higher the score, the higher the priority of the object - it does not have to be a probability/confidence. + overlap_threshold: The overlap (e.g. IoU) threshold for non-maximum suppression. Defaults to 0.5. + return_indices: A flag to indicate whether to return the indices of the picked objects or the objects themselves. Defaults to False. + If True, both the picked objects and scores are returned. + group_first: A flag to indicate whether two use the two-stage NMS method. Defaults to True. + boxes: Bounding boxes for the masks. A tensor of shape (n, 4), where n is the number of masks and + the 4 columns are the x_min, y_min, x_max and y_max coordinates of the bounding boxes. + overlap_fn: A function to compute overlaps between masks. + overlap_fn_boxes: A function to compute overlaps between boxes. Returns: - out (`Union[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]`): - - `torch.Tensor`: A tensor of shape `(m,)` containing the indices of the picked objects. - - `Tuple[torch.Tensor, torch.Tensor]`: A tuple where the first element contains the picked masks and the second element is a tensor of their scores. + Either a tensor of shape `(m,)` containing the indices of the picked objects, + or a tuple (`tuple[torch.Tensor, torch.Tensor]`) where the first element contains + the picked masks and the second element is a tensor of their scores. + """ if isinstance(overlap_fn_boxes, str): overlap_fn_boxes = get_overlap_fn("box", overlap_fn_boxes) @@ -647,16 +738,27 @@ def nms_masks( raise ValueError("'boxes' must be specified for nms_masks when 'group_first' is True") if overlap_fn_boxes is None: raise RuntimeError("If an overlap function is manually provided for masks, one must also be provided for boxes.") - # We decrease the overlap_threshold for the clustering, since there is no straight-forward relationship between the IoU of the boxes and the IoU of the masks - groups, _ = cluster_overlap_boxes(boxes=boxes, overlap_threshold=min(1, overlap_threshold / 4), overlap_fn=overlap_fn_boxes, time=False) - _nms_ind = [torch.empty(0) for i in range(len(groups))] + # We decrease the overlap_threshold for the clustering, + # since there is no straight-forward relationship between the IoU of the boxes and the IoU of the masks + groups, _ = cluster_overlap_boxes( + boxes=boxes, + overlap_threshold=min(1, overlap_threshold / 4), + overlap_fn=overlap_fn_boxes, + time=False + ) + _nms_ind = [torch.empty((0,)) for i in range(len(groups))] for i, group in enumerate(groups): if len(group) == 1: _nms_ind[i] = group else: group_boxes = boxes[group].round().long() xmin, ymin, xmax, ymax = group_boxes[:, 0].min(), group_boxes[:, 1].min(), group_boxes[:, 2].max(), group_boxes[:, 3].max() - _nms_ind[i] = group[nms_masks_(masks=masks[group, ymin:(ymax+1), xmin:(xmax+1)], scores=scores[group], overlap_threshold=overlap_threshold, overlap_fn=overlap_fn)] + _nms_ind[i] = group[nms_masks_( + masks=masks[group, ymin:(ymax+1), xmin:(xmax+1)], + scores=scores[group], + overlap_threshold=overlap_threshold, + overlap_fn=overlap_fn + )] if len(_nms_ind) > 0: nms_ind = torch.cat(_nms_ind) else: @@ -666,33 +768,67 @@ def nms_masks( else: return masks[nms_ind], scores[nms_ind] + +@overload def nms_polygons( - polygons : List[torch.Tensor], + polygons : list[torch.Tensor], scores : torch.Tensor, - overlap_threshold : Union[float, int]=0.5, + overlap_threshold : float | int=0.5, + return_indices : Literal[False]=False, + group_first : bool=True, + boxes : torch.Tensor | None=None, + overlap_fn : Callable | str="IoU", + overlap_fn_boxes : Callable[..., torch.Tensor] | str | None=None, + ) -> tuple[list[torch.Tensor], torch.Tensor]: ... +@overload +def nms_polygons( + polygons : list[torch.Tensor], + scores : torch.Tensor, + overlap_threshold : float | int=0.5, + return_indices : Literal[True]=True, + group_first : bool=True, + boxes : torch.Tensor | None=None, + overlap_fn : Callable | str="IoU", + overlap_fn_boxes : Callable[..., torch.Tensor] | str | None=None, + ) -> torch.Tensor: ... +def nms_polygons( + polygons : list[torch.Tensor], + scores : torch.Tensor, + overlap_threshold : float | int=0.5, return_indices : bool=False, group_first : bool=True, - boxes : Optional[torch.Tensor]=None, - overlap_fn : Union[Callable[[List[torch.Tensor], List[torch.Tensor]], torch.Tensor], str]="IoU", - overlap_fn_boxes : Optional[Union[Callable[[torch.Tensor], torch.Tensor], str]]=None, - ) -> Union[torch.Tensor, Tuple[List[torch.Tensor], torch.Tensor]]: - """ - Efficiently perform non-maximum suppression on a set of polygons. + boxes : torch.Tensor | None=None, + overlap_fn : Callable | str="IoU", + overlap_fn_boxes : Callable[..., torch.Tensor] | str | None=None, + ) -> torch.Tensor | tuple[list[torch.Tensor], torch.Tensor]: + """Efficiently perform non-maximum suppression on a set of polygons. - Defaults to a modified two-stage NMS algorithm, that aims to minimize the number of polygon intersection calculations needed (very expensive). + Defaults to a modified two-stage NMS algorithm, + that aims to minimize the number of polygon intersection calculations needed (very expensive). Args: - polygons (`List[torch.Tensor]`): A list of tensors of shape (n, 2), where n is the number of vertices in the polygon and the 2 columns are the x and y coordinates of the vertices. - scores (`torch.Tensor`): A tensor of shape (n, ) containing the "scores" of the polygons, this can merely be though of as a priority score, where the higher the score, the higher the priority of the object - it does not have to be a probability/confidence. - overlap_threshold (`float`, optional): The overlap (e.g. IoU) threshold for non-maximum suppression. Defaults to 0.5. - return_indices (`bool`, optional): A flag to indicate whether to return the indices of the picked objects or the objects themselves. Defaults to False. If True, both the picked objects and scores are returned. - group_first (`bool`, optional): A flag to indicate whether two use the two-stage NMS method. Defaults to True (recommended). - boxes (`Optional[torch.Tensor]`, optional): Bounding boxes for the polygons. A tensor of shape (n, 4), where n is the number of polygons and the 4 columns are the x_min, y_min, x_max and y_max coordinates of the bounding boxes. + polygons: A list of tensors of shape (n, 2), + where n is the number of vertices in the polygon and the 2 columns are the x and y coordinates of the vertices. + scores: A tensor of shape (n, ) containing the "scores" of the polygons, this can merely be though of as a priority score, + where the higher the score, the higher the priority of the object - it does not have to be a probability/confidence. + overlap_threshold: The overlap (e.g. IoU) threshold for non-maximum suppression. + Defaults to 0.5. + return_indices: A flag to indicate whether to return the indices of the picked objects or the objects themselves. + Defaults to False. If True, both the picked objects and scores are returned. + group_first: A flag to indicate whether two use the two-stage NMS method. Defaults to True (recommended). + boxes: Bounding boxes for the polygons. + A tensor of shape (n, 4), where n is the number of polygons and + the 4 columns are the x_min, y_min, x_max and y_max coordinates of the bounding boxes. + overlap_fn: A callable to compute overlap between polygons. Must accept either one or two lists of tensors and return a tensor. + Can also be a string (e.g., "IoU"). + overlap_fn_boxes: A callable to compute overlap between a set of bounding boxes. + Can also be a string (e.g., "IoU"). Returns: - out (`Union[torch.Tensor, Tuple[List[torch.Tensor], torch.Tensor]]`): - - `torch.Tensor`: A tensor of shape `(m,)` containing the indices of the picked polygons. - - `Tuple[List[torch.Tensor], torch.Tensor]`: A tuple where the first element contains the picked polygons and the second element is a tensor of their scores. + Either a tensor of shape `(m,)` containing the indices of the picked polygons, + or a tuple (`tuple[list[torch.Tensor], torch.Tensor]`) where the first element + contains the picked polygons and the second element is a tensor of their scores. + """ if isinstance(overlap_fn_boxes, str): overlap_fn_boxes = get_overlap_fn("box", overlap_fn_boxes) @@ -705,18 +841,35 @@ def nms_polygons( raise RuntimeError("If an overlap function is manually provided for polygons, one must also be provided for boxes.") device = polygons[0].device if not group_first or len(polygons) < 10: - nms_ind = nms_polygons_(polys=polygons, scores=scores, overlap_threshold=overlap_threshold, overlap_fn=overlap_fn) + nms_ind = nms_polygons_( + polys=polygons, + scores=scores, + overlap_threshold=overlap_threshold, + overlap_fn=overlap_fn + ) else: if boxes is None: raise ValueError("'boxes' must be specified for nms_masks when 'group_first' is True") - # We decrease the overlap_threshold for the clustering, since there is no straight-forward relationship between the overlap of the boxes and the overlap of the polygons - groups, _ = cluster_overlap_boxes(boxes=boxes, overlap_threshold=min(1, overlap_threshold / 4), overlap_fn=overlap_fn_boxes, time=False) - nms_ind = [None for _ in range(len(groups))] + # We decrease the overlap_threshold for the clustering, + # since there is no straight-forward relationship between the overlap of the boxes and the overlap of the polygons + groups, _ = cluster_overlap_boxes( + boxes=boxes, + overlap_threshold=min(1, overlap_threshold / 4), + overlap_fn=overlap_fn_boxes, + time=False + ) + nms_ind : list[torch.Tensor] | torch.Tensor = [] for i, group in enumerate(groups): if len(group) == 1: - nms_ind[i] = group + nms_ind.append(group) else: - nms_ind[i] = group[nms_polygons_(polys=[polygons[gi] for gi in group], scores=scores[group], overlap_threshold=overlap_threshold, overlap_fn=overlap_fn)] + nms_ind.append( + group[nms_polygons_( + polys=[polygons[gi] for gi in group], + scores=scores[group], + overlap_threshold=overlap_threshold, overlap_fn=overlap_fn + )] + ) if len(nms_ind) > 0: nms_ind = torch.cat(nms_ind) else: @@ -729,11 +882,12 @@ def nms_polygons( def nms_boxes( boxes : torch.Tensor, scores : torch.Tensor, - overlap_threshold : Union[float, int]=0.5, - overlap_fn : Optional[Union[Callable[[torch.Tensor], torch.Tensor], str]]=None, + overlap_threshold : float | int=0.5, + overlap_fn : Callable[[torch.Tensor], torch.Tensor] | str | None=None, ) -> torch.Tensor: - """ - Wrapper for `torchvision.ops.nms`; the standard non-maximum suppression algorithm. + """Perform NMS on boxes and return the NMS indexes. + + Wraps `torchvision.ops.nms`; the standard non-maximum suppression algorithm. """ if overlap_fn is None or isinstance(overlap_fn, str) and (overlap_fn := overlap_fn.strip().lower()) == "iou": if boxes.dtype != torch.float32: @@ -743,4 +897,4 @@ def nms_boxes( return torchvision.ops.nms(boxes, scores, overlap_threshold).sort().values if isinstance(overlap_fn, str): overlap_fn = get_overlap_fn("box", overlap_fn) - return base_nms_(boxes, overlap_fn=overlap_fn, scores=scores, overlap_threshold=overlap_fn, return_indices=True) \ No newline at end of file + return base_nms_(boxes, overlap_fn=overlap_fn, scores=scores, overlap_threshold=overlap_threshold, return_indices=True) \ No newline at end of file diff --git a/src/flat_bug/predictor.py b/src/flat_bug/predictor.py index 98126fc..876b980 100644 --- a/src/flat_bug/predictor.py +++ b/src/flat_bug/predictor.py @@ -1,3 +1,4 @@ +"""Implementation of the public flatbug `Predictor` and `TensorPredictions`.""" import atexit import base64 import json @@ -5,8 +6,10 @@ import pathlib import queue import threading -from concurrent.futures import Future, wait, as_completed -from typing import Any, List, Optional, Self, Tuple, Union +import uuid +from concurrent.futures import Future, as_completed, wait +from pathlib import Path +from typing import Any, cast import cv2 import numpy as np @@ -40,13 +43,13 @@ merge_tile_results, offset_box, postprocess, - resize_mask, + resize_masks, stack_masks, ) -class AsyncExecutor: - def __init__(self, max_workers=None, backlog=10000): +class AsyncExecutor: # noqa: D101 + def __init__(self, max_workers=None, backlog=10000): # noqa: D107 self.limit = max_workers or max(1, min(16, (os.cpu_count() or 2) // 2)) self._queue = queue.Queue(maxsize=backlog) self._threads, self._active, self._lock = [], set(), threading.Lock() @@ -106,58 +109,81 @@ def flush(self, progress=False): _executor = AsyncExecutor() -## Class for containing the results from a single _detect_instances call -# This should probably not be its own class, but just a TensorPredictions object with a single element instead, -# but this would require altering the TensorPredictions._combine_predictions function to handle a single element differently or pass a flag or something class Prepared_Results: - def __init__(self, predictions: "ResultsWithTiles", scale: Tuple[float, float], device, dtype): + """Class for containing the results from a single `Predictor._detect_instances` call. + + This should probably not be its own class, but just a TensorPredictions object with a single element instead, + but this would require altering the `TensorPredictions._combine_predictions` function + to handle a single element differently or pass a flag or something. + """ + + def __init__(self, predictions: ResultsWithTiles, scale: tuple[float, float], device, dtype): # noqa: D107 self.wh_scale = torch.tensor(scale, device=device, dtype=dtype).unsqueeze(0) self._predictions = predictions + assert self._predictions.boxes is not None and isinstance(self._predictions.boxes, torch.Tensor) self._predictions.boxes.data[:, :4] /= self.wh_scale.repeat(1, 2) - self._predictions.polygons = [(poly + torch.roll(poly, 1, dims=0)) / (2 * self.wh_scale) for poly in self._predictions.polygons] + self._predictions.polygons = self._predictions.polygons._apply( + lambda poly : (poly + torch.roll(poly, 1, dims=0)) / (2 * self.wh_scale) + ) self.scale = sum(scale) / 2 self.device = device self.dtype = dtype - def __len__(self) -> int: + def __len__(self): return len(self._predictions) - def __getitem__(self, i) -> "Prepared_Results": - return Prepared_Results(self._predictions[i], self.scale, self.device, self.dtype) + def __getitem__(self, i): + elems = self._predictions[i] + assert isinstance(elems, ResultsWithTiles) + return Prepared_Results(elems, (self.scale, self.scale), self.device, self.dtype) # Properties for accessing the data @property - def contours(self) -> List["torch.Tensor"]: - return [c if c is not None else torch.tensor([], dtype=torch.long, device=self.device) for c in self._predictions.masks.xy] + def contours(self): # noqa: D102 + assert self._predictions.masks is not None + return [ + torch.as_tensor(c) if c is not None else torch.tensor([], dtype=torch.long, device=self.device) + for c in self._predictions.masks.xy + ] @property - def masks(self) -> Union["torch.Tensor", "np.ndarray"]: + def masks(self) -> torch.Tensor | np.ndarray: # noqa: D102 + assert self._predictions.masks is not None return self._predictions.masks.data @property - def boxes(self) -> Union["torch.Tensor", "np.ndarray"]: - return self._predictions.boxes.xyxy + def boxes(self) -> torch.Tensor | np.ndarray: # noqa: D102 + assert self._predictions.boxes is not None + return torch.as_tensor(self._predictions.boxes.xyxy) @property - def confs(self) -> Union["torch.Tensor", "np.ndarray"]: - return self._predictions.boxes.conf + def confs(self) -> torch.Tensor | np.ndarray: # noqa: D102 + assert self._predictions.boxes is not None + return torch.as_tensor(self._predictions.boxes.conf) @property - def classes(self) -> "torch.Tensor": - ### OBS: This is not really implemented, but exists just so that the the rest of the code already handles the multiclass case, but this function will need to be changed for it to work properly ### + def classes(self) -> torch.Tensor: + """Not implemented properly.""" + ### OBS: This is not really implemented, but exists just so that the the rest of the code already handles the multiclass case, + # but this function will need to be changed for it to work properly ### # Currently this function is pretty redundant, since the localizer only has a single class. - # If there were more classes, the function should do some kind of argmax on self._predictions.boxes.cls (I assume these are class probabilities). - return torch.ones_like(self._predictions.boxes.cls) + # If there were more classes, the function should do some kind of argmax on self._predictions.boxes.cls + # (I assume these are class probabilities). + assert self._predictions.boxes is not None + return torch.ones_like(torch.as_tensor(self._predictions.boxes.cls)) # Class for containing the results from multiple _detect_instances calls class TensorPredictions: - """ - Result handling class for combining the results from multiple YOLOv8 detections at different scales into a single object. + """Result handling class for combining the results from multiple YOLOv8 detections at different scales into a single object. - `TensorPredictions` handles a rather complex merging procedure, resizing to remove image padding and scaling effects on the masks and boxes, and non-maximum suppression using mask-IoU or mask-IoS. + `TensorPredictions` handles a rather complex merging procedure, + resizing to remove image padding and scaling effects on the masks and boxes, + and non-maximum suppression using mask-IoU or mask-IoS. - `TensorPredictions` also allows for easy conversion from mask to contours and back, plotting of the results, and (de-)serialization to save and load the results to/from disk. + `TensorPredictions` also allows for easy conversion from mask to contours and back, plotting of the results, + and (de-)serialization to save and load the results to/from disk. """ + DUPLICATE_THRESHOLD = 1 PREFER_POLYGONS = True # If True, will use shapely Polygons instead of masks for NMS and drawing # These are simply initialized here to decrease clutter in the __init__ function and arguments @@ -165,23 +191,40 @@ class TensorPredictions: mask_height = None device = None dtype = None - CONSTANTS = ["image", "image_path", "device", "dtype", "time", "mask_height", "mask_width", "CONSTANTS", + CONSTANTS = ("image", "image_path", "device", "dtype", "time", "mask_height", "mask_width", "CONSTANTS", "BOX_IS_EQUAL_MARGIN", - "PREFER_POLYGONS"] # Attributes that should not be changed after initialization - should 'contours' be here? + "PREFER_POLYGONS") # Attributes that should not be changed after initialization - should 'contours' be here? def __init__( self, - predictions : Optional[list[Prepared_Results]]=None, - image : Optional["torch.Tensor"]=None, - image_path : Optional[str] = None, + predictions : list[Prepared_Results] | None=None, + image : torch.Tensor | None=None, + image_path : str | None = None, time : bool=False, **kwargs ): + """Create a `TensorPredictions` instance from scratch. + + You probably don't want to use this method manually. If you want to load saved results use: + + ``` + prediction = TensorPredictions.load(...) + ``` + + Args: + predictions: Predictions from multiple `Predictor._detect_instances` calls. + image: The image where the predictions originate. + image_path: Path to the source file for `image`, can be used as a substitute. + time: Whether operations (such as initialization, NMS, etc.) should be timed. + kwargs: Additional configuration arguments. + + """ # Set option flags self.time = time + start = end = None # Timing could probably be hidden in a decorator... - if self.time and len(predictions) > 0: + if self.time and predictions is not None and len(predictions) > 0: # Initialize timing calculations start = torch.cuda.Event(enable_timing=True) end = torch.cuda.Event(enable_timing=True) @@ -194,40 +237,54 @@ def __init__( else: logger.warning(f"WARNING: Unknown keyword argument {k}={v} for TensorPredictions is ignored!") - # Device and dtype are None by default, but they may be set by the user or passwed by **kwargs, so we check if they are None and if so set them to the default values + # Device and dtype are None by default, but they may be set by the user or + # passed by **kwargs, so we check if they are None and if so set them to the default values # Then we check that they are the same for all predictions and the image (if they are not None) - no_predictions = predictions is None or len(predictions) == 0 - if not no_predictions: + if predictions is not None and len(predictions) >= 0: # Check that all predictions have the same device and dtype + elem = predictions[0] if self.device is None: - self.device = predictions[0].device + self.device = elem.device if self.dtype is None: - self.dtype = predictions[0].dtype + self.dtype = elem.dtype for pi, p in enumerate(predictions): assert p.device == self.device, RuntimeError(f"predictions[{pi}].device {p.device} != device {self.device}") assert p.dtype == self.dtype, RuntimeError(f"predictions[{pi}].dtype {p.dtype} != dtype {self.dtype}") if image is not None: assert image.device == self.device, RuntimeError(f"image.device {image.device} != device {self.device}") + else: + self.device, self.dtype = torch.device("cpu"), torch.float32 # Set attributes - self.image = image self.image_path = image_path + if image is None: + if self.image_path is None: + raise ValueError('Either `image` or `image_path` must be specified.') + self.image = decode_image( + input=self.image_path, + mode=ImageReadMode.RGB, + apply_exif_orientation=True + ).to(self.device) + else: + self.image = image.to(self.device) # Combine the predictions - if not no_predictions: + if predictions is not None and len(predictions) >= 0: self._combine_predictions(predictions) else: - # If there are no predictions, set other attributes to empty tensors or lists - ensures correct type and device for the attributes when there are no predictions + # If there are no predictions, set other attributes to empty tensors or lists. + # Ensures correct type and device for the attributes when there are no predictions self.masks, self.polygons, self.boxes, self.confs, self.classes, self.scales = ( torch.empty((0, 0), device=self.device, dtype=self.dtype), [], torch.empty((0, 4), device=self.device, dtype=self.dtype), - torch.empty(0, device=self.device, dtype=self.dtype), - torch.empty(0, device=self.device, dtype=self.dtype), + torch.empty((0,), device=self.device, dtype=self.dtype), + torch.empty((0,), device=self.device, dtype=self.dtype), [] ) - if self.time and len(predictions) > 0: + if self.time and predictions is not None and len(predictions) > 0: + assert end is not None and start is not None end.record() torch.cuda.synchronize() logger.info(f'Initializing TensorPredictions took {start.elapsed_time(end) / 1000:.3f} s') @@ -236,13 +293,16 @@ def _combine_predictions( self, predictions: list[Prepared_Results] ): - """ - Combines a list of Prepared_Results from multiple _detect_instances calls into a single TensorPredictions object. + """Combine a list of Prepared_Results from multiple `Predictor._detect_instances` calls. + + This function is used in-place during initialization of a `TensorPrediction` instance. Args: - predictions (list[Prepared_Results]): A list of Prepared_Results objects. - offset (torch.Tensor): A vector of length 2 containing the x and y offset of the image. + predictions: A list of Prepared_Results objects. + offset: A vector of length 2 containing the x and y offset of the image. + """ + start = end = end_duplication_removal = end_mask_combination = None if self.time: # Initialize timing calculations start = torch.cuda.Event(enable_timing=True) @@ -250,8 +310,8 @@ def _combine_predictions( end_duplication_removal = torch.cuda.Event(enable_timing=True) end_mask_combination = torch.cuda.Event(enable_timing=True) start.record() - self.boxes = torch.cat([p.boxes for p in predictions]) # Nx4 - self.confs = torch.cat([p.confs for p in predictions]) # N + self.boxes = torch.cat([torch.as_tensor(p.boxes) for p in predictions]) # Nx4 + self.confs = torch.cat([torch.as_tensor(p.confs) for p in predictions]) # N self.scales = [p.scale for p in predictions for _ in range(len(p))] # N ## Duplicate removal ## @@ -266,22 +326,37 @@ def _combine_predictions( # Divide the valid indices into each prediction object n_detections = [len(p) for p in predictions] # max_indices = cumsum(n_detections) - max_indices = np.cumsum(n_detections).tolist() - valid_chunked = [valid_indices[(valid_indices < max_indices[i]) & (valid_indices >= (max_indices[i - 1] if i > 0 else 0))] - (max_indices[i] - n_detections[i]) for i in range(len(predictions))] + max_indices = cast(list[int], np.cumsum(n_detections).tolist()) + valid_chunked = [ + valid_indices[ + (valid_indices < max_indices[i]) & (valid_indices >= (max_indices[i - 1] if i > 0 else 0)) + ] - (max_indices[i] - n_detections[i]) + for i in range(len(predictions)) + ] if self.time: + assert end_duplication_removal is not None end_duplication_removal.record() # For the remaining attributes we remove the duplicates before combining them - self.masks = stack_masks([p.masks[nd] for p, nd in zip(predictions, valid_chunked)]) # NxMHxMW - MH and MW are proportional to the original image size - self.mask_height, self.mask_width = self.masks.shape[1:] + # NxMHxMW - MH and MW are proportional to the original image size + self.masks = stack_masks([p.masks[nd] for p, nd in zip(predictions, valid_chunked)]) + mhw = self.masks.shape[1:] + assert len(mhw) == 2 + self.mask_height, self.mask_width = map(round, mhw) if self.time: + assert end_mask_combination is not None end_mask_combination.record() - self.masks.orig_shape = self.image.shape[1:] # Set the target shape of the masks to the shape of the image passed to the TensorPredictions object + # Set the target shape of the masks to the shape of the image passed to the TensorPredictions object + self.masks.orig_shape = self.image.shape[1:] - self.polygons = [p._predictions.polygons[nd_i] for p, nd in zip(predictions, valid_chunked) for nd_i in nd] + poly_lists = [p._predictions.polygons.to_list() for p in predictions] + self.polygons : list[torch.Tensor] = [ + p[int(nd_i.item()) if isinstance(nd, torch.Tensor) else int(nd)] + for p, nd in zip(poly_lists, valid_chunked) for nd_i in nd + ] self.classes = torch.cat([p.classes[nd] for p, nd in zip(predictions, valid_chunked)]) # N self.scales = [predictions[i].scale for i, p in enumerate(valid_chunked) for _ in range(len(p))] # N @@ -300,6 +375,7 @@ def _combine_predictions( assert len(self) == len(self.classes), RuntimeError(f"len(self) {len(self)} != len(self.classes) {len(self.classes)}") assert len(self) == len(self.scales), RuntimeError(f"len(self) {len(self)} != len(self.scales) {len(self.scales)}") if self.time: + assert start is not None and end is not None and end_duplication_removal is not None and end_mask_combination is not None end.record() torch.cuda.synchronize() total = start.elapsed_time(end) / 1000 @@ -315,19 +391,21 @@ def offset_scale_pad( offset: torch.Tensor, scale: float, pad: int = 0 - ) -> Self: - """ - Since the image may be padded, the masks and boxes should be offset by the padding-width and scaled by - the `scale_before` factor to match the original image size. Also pads the boxes by pad pixels to be safe. + ): + """Scale and offset the detections to real image coordinates in-place. + + Since the image may be padded, the masks and boxes should be offset by the padding-width and scaled + by the `scale_before` factor to match the original image size. Also pads the boxes by pad pixels to be safe. Args: - offset (`torch.Tensor`): A vector of length 2 containing the x and y offset of the image. Useful for removing image-padding effects. - scale (`float`): The scale factor of the image. - pad (`int`, optional): The number of pixels to pad the boxes by. Defaults to 0. (Not to be confused with image-padding, + offset: A vector of length 2 containing the x and y offset of the image. Useful for removing image-padding effects. + scale: The scale factor of the image. + pad: The number of pixels to pad the boxes by. Defaults to 0. (Not to be confused with image-padding, this is about expanding the boxes a bit to ensure they cover the entire mask) Returns: - out (`Self`): The `TensorPredictions` object with the masks, polygons and boxes offset, scaled and padded. + The `TensorPredictions` object with the masks, polygons and boxes offset, scaled and padded. + """ if self.time: # Initialize timing calculations @@ -368,7 +446,7 @@ def offset_scale_pad( offset_mask_coords = offset_norm * orig_mask_shape # Round the coordinates to the nearest integer and convert to long (needed for indexing) offset_mask_coords = torch.round(offset_mask_coords).long() - self.masks.data = self.masks.data[ + self.masks.data = torch.as_tensor(self.masks.data)[ :, offset_mask_coords[0]:(-(offset_mask_coords[0] + 1) if offset_mask_coords[0] != 0 else None), offset_mask_coords[1]:(-(offset_mask_coords[1] + 1) if offset_mask_coords[1] != 0 else None) @@ -381,9 +459,8 @@ def offset_scale_pad( return self - def fix_boxes(self) -> Self: - """ - This function simply sets the boxes to match the masks. + def fix_boxes(self): + """Set the boxes to match the masks in-place. It is not strictly needed, but can be used as a sanity check to see if the boxes match the masks. The discrepancy between the boxes and the masks comes about by all the scaling and smoothing of the masks. @@ -392,8 +469,9 @@ def fix_boxes(self) -> Self: """ if self.PREFER_POLYGONS: raise NotImplementedError("`fix_boxes` is not implemented for polygons") - nonzero_indices = self.masks.data.nonzero() - mask_size = torch.tensor([self.masks.data.shape[1], self.masks.data.shape[2]], device=self.device, dtype=self.dtype) + mask_data = torch.as_tensor(self.masks.data) + nonzero_indices = mask_data.nonzero() + mask_size = torch.tensor([mask_data.shape[1], mask_data.shape[2]], device=self.device, dtype=self.dtype) image_size = torch.tensor([self.image.shape[1], self.image.shape[2]], device=self.device, dtype=self.dtype) mask_to_image_scale = image_size / mask_size for i in range(len(self)): @@ -401,9 +479,13 @@ def fix_boxes(self) -> Self: if len(this_mask_nz) == 0: self.boxes[i] = torch.tensor([0, 0, 0, 0], device=self.device, dtype=self.dtype) else: - self.boxes[i] = torch.tensor( - [this_mask_nz[:, 1].min(), this_mask_nz[:, 0].min(), this_mask_nz[:, 1].max(), - this_mask_nz[:, 0].max()], device=self.device, dtype=self.dtype) * mask_to_image_scale.repeat(2) + self.boxes[i] = torch.tensor([ + this_mask_nz[:, 1].min(), + this_mask_nz[:, 0].min(), + this_mask_nz[:, 1].max(), + this_mask_nz[:, 0].max() + ], device=self.device, dtype=self.dtype + ) * mask_to_image_scale.repeat(2) self.boxes[:, :2] = self.boxes[:, :2].floor() self.boxes[:, 2:] = self.boxes[:, 2:].ceil() self.boxes[:, 0:4:2] = self.boxes[:, 0:4:2].clamp(0, self.image.shape[2]) @@ -415,9 +497,10 @@ def non_max_suppression( overlap_threshold : float, metric : str, **kwargs - ) -> Self: - """ - Simply wraps the `nms_masks` function from yolo_helpers.py, and removes the duplicates from the `TensorPredictions` object. + ): + """Perform non-max suppression (NMS) in-place. + + Either uses polygons (most likely) or masks. """ if self.time: # Initialize timing calculations @@ -434,7 +517,7 @@ def non_max_suppression( if self.PREFER_POLYGONS: nms_ind = nms_polygons( polygons=self.polygons, - scores=self.confs,# * torch.tensor(self.scales, dtype=self.dtype, device=self.device), + scores=self.confs, overlap_threshold=overlap_threshold, overlap_fn=metric, return_indices=True, @@ -447,8 +530,8 @@ def non_max_suppression( device=self.device, dtype=self.dtype ) nms_ind : torch.Tensor = nms_masks( - masks=self.masks.data, - scores=self.confs,# * torch.tensor(self.scales, dtype=self.dtype, device=self.device), + masks=torch.as_tensor(self.masks.data), + scores=self.confs, overlap_threshold=overlap_threshold, overlap_fn=metric, return_indices=True, @@ -458,7 +541,7 @@ def non_max_suppression( # Remove the instances that were not selected self = self[nms_ind.sort().values] else: - nms_ind = [] + nms_ind = torch.empty((0,)) if self.time: end.record() @@ -470,10 +553,8 @@ def non_max_suppression( return self @property - def contours(self) -> List[torch.Tensor]: - """ - This function wraps the openCV.findContours function, and uses openCV.contourArea to select the largest contour for each mask. - """ + def contours(self) -> list[torch.Tensor]: + """Wraps the openCV.findContours function, and uses openCV.contourArea to select the largest contour for each mask.""" if self.PREFER_POLYGONS: return self.polygons else: @@ -485,8 +566,9 @@ def contours(self) -> List[torch.Tensor]: @contours.setter def contours( self, - value : List[torch.Tensor] + value : list[torch.Tensor | np.ndarray] ): + assert self.mask_height is not None and self.mask_width is not None if self.PREFER_POLYGONS: if not isinstance(value, list): raise RuntimeError(f"Unknown type `{type(value)}` for `contours` - should be a list of polygons") @@ -502,18 +584,22 @@ def contours( raise RuntimeError(f"Unknown shape `{value[i].shape}` for `contours[{i}]` - should be (N, 2)") value[i] = torch.from_numpy( scale_contour( - contour=value[i], + contour=np.asarray(value[i]), scale=contour_scaling, expand_by_one=True ) ).long().to(self.device) - self.polygons = value - self.masks = [torch.empty((0, 0), device=self.device, dtype=self.dtype) for _ in range(len(value))] # Initialize empty masks + self.polygons = cast(list[torch.Tensor], value) + self.masks = torch.stack([ + torch.empty((0, 0), device=self.device, dtype=self.dtype) + for _ in range(len(value)) + ]) # Initialize empty masks else: - self.masks = contours_to_masks(value, self.mask_height, self.mask_width).to(self.device) + self.masks = contours_to_masks(list(map(torch.as_tensor, value)), self.mask_height, self.mask_width).to(self.device) @property def areas(self): + """Detection areas (either from polygons or masks).""" if self.PREFER_POLYGONS: return [poly_area(poly) for poly in self.polygons] else: @@ -524,16 +610,17 @@ def contour_to_image_coordinates( contour: torch.Tensor, scale: float = 1 ) -> torch.Tensor: - """ - Converts a contour from mask coordinates to image coordinates. + """Convert a contour from mask coordinates to image coordinates. Args: - contour (`torch.Tensor`): The contour to convert. - scale (`float`, optional): The scale factor to apply to the contour. Defaults to 1. + contour: The contour to convert. + scale: The scale factor to apply to the contour. Defaults to 1. Returns: - out (`torch.Tensor`): The contour in image coordinates. + The contour in image coordinates. + """ + assert self.mask_height is not None and self.mask_width is not None image_h, image_w = self.image.shape[1:] mask_to_image_scale = [(image_h - 1) / (self.mask_height - 1), (image_w - 1) / (self.mask_width - 1)] mask_to_image_scale = torch.tensor(mask_to_image_scale, device=self.device, dtype=torch.float32) * scale @@ -546,16 +633,16 @@ def contour_to_image_coordinates( def flip( self, direction : str="vertical" - ) -> Self: - """ - Flips the masks, polygons and boxes along the specified axis. + ): + """Flips the masks, polygons and boxes along the specified axis in-place. Args: - direction (`str`, optional): The axis to flip the masks, polygons and boxes along. + direction: The axis to flip the masks, polygons and boxes along. Defaults to "vertical". Should be one of "vertical", "y", "horizontal" or "x". Returns: - out (`Self`): The `TensorPredictions` object with the masks, polygons and boxes flipped. + The `TensorPredictions` instance with the masks, polygons and boxes flipped. + """ if self.time: # Initialize timing calculations @@ -563,16 +650,17 @@ def flip( end = torch.cuda.Event(enable_timing=True) start.record() + mask_data = torch.as_tensor(self.masks.data) if direction == "vertical" or direction == "y": - if self.masks.data.dim() == 3: - self.masks.data = torch.flip(self.masks.data, [1]) + if mask_data.dim() == 3: + self.masks.data = torch.flip(mask_data, [1]) self.boxes[:, 1] = self.image.shape[1] - self.boxes[:, 1] self.boxes[:, 3] = self.image.shape[1] - self.boxes[:, 3] for i in range(len(self)): self.polygons[i][:, 1] = self.image.shape[1] - self.polygons[i][:, 1] elif direction == "horizontal" or direction == "x": - if self.masks.data.dim() == 3: - self.masks.data = torch.flip(self.masks.data, [2]) + if mask_data.dim() == 3: + self.masks.data = torch.flip(mask_data, [2]) self.boxes[:, 0] = self.image.shape[2] - self.boxes[:, 0] self.boxes[:, 2] = self.image.shape[2] - self.boxes[:, 2] for i in range(len(self)): @@ -590,12 +678,13 @@ def flip( def __len__(self) -> int: return len(self.polygons) - def new(self): + def new(self): # noqa: D102 return TensorPredictions([], **{k: self.__dict__[k] for k in self.CONSTANTS if k in self.__dict__}) def __getitem__(self, i): - """ - Flexible indexing for TensorPredictions. Can be used to get a single element, a slice, or an iterable of indices (e.g. a list, tuple, tensor). + """Flexible indexing for TensorPredictions. + + Can be used to get a single element, a slice, or an iterable of indices (e.g. a list, tuple, tensor). """ new_tp = self.new() for k, v in self.__dict__.items(): @@ -606,10 +695,11 @@ def __getitem__(self, i): # Check if 'i' is an iterable elif hasattr(i, "__iter__"): if isinstance(i, torch.Tensor): - i = i.float().round().long().tolist() # Just to be super safe we cast to float, then round, then cast to long, then to list - assert all([isinstance(j, int) for j in i]) or all([isinstance(j, float) and (j % 1) == 0 for j in i]), \ - RuntimeError(f"Unknown type or non-integer float for {i}: {type(i)}") - i = [int(j) for j in i] + # Just to be super safe we cast to float, then round, then cast to long, then to list + i = i.float().round().long().tolist() + if not all([isinstance(j, int) for j in i]) or all([isinstance(j, float) and (j % 1) == 0 for j in i]): # type: ignore + raise RuntimeError(f"Unknown type or non-integer float for {i}: {type(i).__name__}") + i = [int(j) for j in i] # type: ignore # If v is a tensor, we can just index it with the list if isinstance(v, torch.Tensor): new_value = v[i] @@ -647,29 +737,43 @@ def plot( masks : bool=True, boxes : bool=True, confidence : bool=True, - outpath : Optional[str]=None, + outpath : str | None=None, scale : float=1, - contour_color : Tuple[int, int, int]=(255, 0, 0), - box_color : Tuple[int, int, int]=(0, 0, 0), + contour_color : tuple[int, int, int]=(255, 0, 0), + box_color : tuple[int, int, int]=(0, 0, 0), alpha : float=0.3, wait : bool=False ): - """ - Visualizes `flatbug` predictions from a `TensorPredictions` object. + """Visualizes `flatbug` predictions from a `TensorPredictions` object. Args: - linewidth (`int`, optional): Linewidth of the segmentation countours and bounding boxes. Default to 2. - masks (`bool`, optional): Flag to indicate whether segmentation contours should be included. Default to True. - boxes (`bool`, optional): Flag to indicate whether bounding boxes should be included, if False confidences are also omitted. Defaults to True. - confidences (`bool`, optional): Flag to indicate whether detection confidences should be included, if boxes is False, this argument is ignored. Defaults to True. - outpath (`Optional[str]`, optional): Where should the visualization be saved? If outpath is None, then the rasterized visualization is returned as a `cv2.UMat`/`np.ndarray` (shape: HWC, colors: BGR). Defaults to None. - scale (`float`, optional): Render the visualization at a scale relative to the image size (from which the predictions originate). **OBS**: Large images and/or scales can be very slow to render. Defaults to 1. - contour_color (`Tuple[int, int, int]`, optional): RGB color ([0, 255]) to use for contour border and fill. Defaults to `(255, 0, 0)` (red). - box_color (`Tuple[int, int, int]`, optional): RGB color ([0, 255]) to use for bounding box and confidence text color. Defaults to `(0, 0, 0)` (black). - alpha (`float`, optional): Transparency of the contour fill ([0, 1]). Defaults to 0.3. + linewidth: Linewidth of the segmentation countours and bounding boxes. + Default to 2. + masks: Flag to indicate whether segmentation contours should be included. + Default to True. + boxes: Flag to indicate whether bounding boxes should be included, if False confidences are also omitted. + Defaults to True. + confidence: Flag to indicate whether detection confidences should be included, if boxes is False, this argument is ignored. + Defaults to True. + outpath: Where should the visualization be saved? + If outpath is None, then the rasterized visualization is returned as a `cv2.UMat`/`np.ndarray` (shape: HWC, colors: BGR). + Defaults to None. + scale: Render the visualization at a scale relative to the image size (from which the predictions originate). + **OBS**: Large images and/or scales can be very slow to render. + Defaults to 1. + contour_color: RGB color ([0, 255]) to use for contour border and fill. + Defaults to `(255, 0, 0)` (red). + box_color: RGB color ([0, 255]) to use for bounding box and confidence text color. + Defaults to `(0, 0, 0)` (black). + alpha: Transparency of the contour fill ([0, 1]). + Defaults to 0.3. + wait: If `False` (default) returns a future immediately, otherwise block and return the actual result. Returns: - out (`Union[cv2.UMat, str]`): If outpath is supplied, it is returned, otherwise the rasterized visualization is returned as as a `cv2.UMat`/`np.ndarray` (shape: HWC, colors: BGR). + If outpath is supplied, it is returned. + Otherwise the rasterized visualization is returned as as a `cv2.UMat`/`np.ndarray` (shape: HWC, colors: BGR). + **OBS**: If `wait=True` then a future is returned instead. + """ params = locals() params.pop("self", None) @@ -680,7 +784,7 @@ def plot( "contours" : [poly.detach().cpu().clone() for poly in self.polygons], "confs" : self.confs.detach().cpu().clone(), } - if outpath not in [None, ""] and outpath.lower().endswith(".svg"): + if outpath and outpath.lower().endswith(".svg"): retval = _executor.submit(TensorPredictions._plot_svg, **data, **params) else: retval = _executor.submit(TensorPredictions._plot_image, **data, **params) @@ -692,11 +796,11 @@ def plot( def _box_to_svg_element( box : torch.Tensor, scale : float=1.0, - color : Tuple[int, int, int]=(0, 0, 0), - linewidth : Union[float, int]=2, - label : Optional[str]=None, - label_fontsize : Union[float, int]=12, - background_image : Optional[Any]=None # expected to be a NumPy array in BGR + color : tuple[int, int, int]=(0, 0, 0), + linewidth : float | int=2, + label : str | None=None, + label_fontsize : float | int=12, + background_image : Any | None=None # expected to be a NumPy array in BGR ) -> str: # Convert box color (RGB tuple) to hex. hex_color = f'#{"".join(hs if len(hs)==2 else hs+"0" for v in color if len(hs:=hex(v)[2:]))}' @@ -751,11 +855,11 @@ def _box_to_svg_element( @staticmethod def _contour_to_svg_element( - contour : Union["torch.Tensor", Any], + contour : torch.Tensor | Any, scale : float=1.0, - color : Tuple[int, int, int]=(255, 0, 0), - linewidth : Union[int, float]=2, - alpha : Union[int, float]=0.33 + color : tuple[int, int, int]=(255, 0, 0), + linewidth : int | float=2, + alpha : int | float=0.33 ): d_list = [] hex_color = f'#{"".join(hs if len(hs) == 2 else hs + "0" for v in color if len(hs := hex(v)[2:]))}' @@ -785,49 +889,63 @@ def _plot_svg( masks : bool=True, boxes : bool=True, confidence : bool=True, - outpath : Optional[str]=None, + outpath : str | None=None, scale : float=1, - contour_color : Tuple[int, int, int] = (255, 0, 0), - box_color : Tuple[int, int, int] = (0, 0, 0), + contour_color : tuple[int, int, int] = (255, 0, 0), + box_color : tuple[int, int, int] = (0, 0, 0), alpha : float = 0.3 ): embed_jpeg = True if isinstance(image, str): - image = decode_image( + tensor_image = decode_image( input=image, mode=ImageReadMode.RGB, apply_exif_orientation=True ) - image = torchvision.transforms.ConvertImageDtype(torch.uint8)(image).permute(1, 2, 0).cpu().numpy() + else: + tensor_image = image + np_image = torchvision.transforms.ConvertImageDtype(torch.uint8)(tensor_image).permute(1, 2, 0).cpu().numpy() if scale != 1: - image = cv2.resize(image, (0, 0), fx=scale, fy=scale) - image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) + np_image = cv2.resize(np_image, (0, 0), fx=scale, fy=scale) + np_image = cv2.cvtColor(np_image, cv2.COLOR_RGB2BGR) try: - height, width = image.shape[0:2] + height, width = np_image.shape[0:2] ((_, text_height), _) = cv2.getTextSize("0", cv2.FONT_HERSHEY_SIMPLEX, 1 * scale, 2) - encoded_string = base64.b64encode(cv2.imencode('.jpg', image)[1]) + encoded_string = base64.b64encode(cv2.imencode('.jpg', np_image)[1]) desc = '' content = [] - content.append('') + content.append( + f'' + ) + # Embed the background image. if embed_jpeg: - content.append('' % \ - (desc, width, height, str(encoded_string, 'utf-8'))) + content.append( + f'' + ) if masks: for cont in contours: - content.append(TensorPredictions._contour_to_svg_element(cont, scale=scale, color=contour_color, linewidth=linewidth, alpha=alpha)) + content.append(TensorPredictions._contour_to_svg_element( + cont, + scale=scale, color=contour_color, + linewidth=linewidth, alpha=alpha + )) if boxes: for box, conf in zip(bboxes, confs): lbl = f'{conf.item():.1%}' if confidence else None # Pass the background image so the function can sample the area behind the label. - content.append(TensorPredictions._box_to_svg_element(box, scale=scale, color=box_color, linewidth=linewidth, label=lbl, background_image=image, label_fontsize=text_height)) + content.append(TensorPredictions._box_to_svg_element( + box, + scale=scale, color=box_color, + linewidth=linewidth, label=lbl, + background_image=image, label_fontsize=text_height + )) content.append('') if outpath: @@ -850,50 +968,52 @@ def _plot_image( masks : bool=True, boxes : bool=True, confidence : bool=True, - outpath : Optional[str]=None, + outpath : str | None=None, scale : float=1, - contour_color : Tuple[int, int, int] = (255, 0, 0), - box_color : Tuple[int, int, int] = (0, 0, 0), + contour_color : tuple[int, int, int] = (255, 0, 0), + box_color : tuple[int, int, int] = (0, 0, 0), alpha : float = 0.3 - ) -> Optional[cv2.UMat]: + ) -> cv2.UMat | None: if isinstance(image, str): - image = decode_image( + tensor_image = decode_image( input=image, mode=ImageReadMode.RGB, apply_exif_orientation=True ) - image = torchvision.transforms.ConvertImageDtype(torch.uint8)(image).permute(1, 2, 0).cpu().numpy() + else: + tensor_image = image + np_image = cast(np.ndarray, torchvision.transforms.ConvertImageDtype(torch.uint8)(tensor_image).permute(1, 2, 0).cpu().numpy()) if scale != 1: - image = cv2.resize(image, (0, 0), fx=scale, fy=scale) - image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) + np_image = cv2.resize(np_image, (0, 0), fx=scale, fy=scale) + np_image = cv2.cvtColor(np_image, cv2.COLOR_RGB2BGR) # Convert colors from RGB to BGR - contour_color = contour_color[::-1] - box_color = box_color[::-1] + contour_color = (contour_color[-1], contour_color[-2], contour_color[-3]) + box_color = (box_color[-1], box_color[-2], box_color[-3]) if len(contours) > 0: # Draw masks if masks: - contours = [simplify_contour((c * scale).round().to(torch.int32).cpu().numpy(), scale / 2) for c in contours] - ih, iw = image.shape[:2] + smpl_contours = [simplify_contour((c * scale).round().to(torch.int32).cpu().numpy(), scale / 2) for c in contours] + ih, iw = np_image.shape[:2] _alpha = int(255 * alpha) poly_alpha = np.zeros((ih, iw, 1), dtype=np.int32) - for i, c in enumerate(contours): + for i, c in enumerate(smpl_contours): this_poly_alpha = np.zeros((ih, iw, 1), dtype=np.uint8) cv2.drawContours(this_poly_alpha, [c], -1, 1, -1) poly_alpha += this_poly_alpha * _alpha poly_alpha = poly_alpha.clip(0, 255) / 255 # Create a red fill for the polygons - poly_fill = np.zeros_like(image) + poly_fill = np.zeros_like(np_image) for i, channel_color in enumerate(contour_color): poly_fill[:, :, i] = channel_color # Add the polygons to the image by blending the fill and the image using the alpha mask - image = (image.astype(np.float32) * (1 - poly_alpha) + poly_fill * poly_alpha).round().astype(np.uint8) + np_image = (np_image.astype(np.float32) * (1 - poly_alpha) + poly_fill * poly_alpha).round().astype(np.uint8) # Draw the contours - for i, c in enumerate(contours): - cv2.drawContours(image, [c], -1, contour_color, linewidth) + for i, c in enumerate(smpl_contours): + cv2.drawContours(np_image, [c], -1, contour_color, linewidth) # Draw boxes and confidences if boxes: @@ -904,7 +1024,7 @@ def _plot_image( box = box.long() start_point = (int(box[0]), int(box[1])) end_point = (int(box[2]), int(box[3])) - cv2.rectangle(image, start_point, end_point, box_color, linewidth) # black box + cv2.rectangle(np_image, start_point, end_point, box_color, linewidth) # black box if confidence: # Get the width and height of the text (text_width, text_height), _ = cv2.getTextSize( @@ -918,10 +1038,10 @@ def _plot_image( if yp < text_height: yp = end_point[1] + text_height + linewidth * 2 # Get the average color intensity behind the text - avg_color = np.mean(image[yp:yp + text_height, xp:xp + text_width]) + avg_color = np.mean(np_image[yp:yp + text_height, xp:xp + text_width]) # Draw the text cv2.putText( - img=image, + img=np_image, text=f"{conf * 100:.3g}%", org=(xp, yp), fontFace=cv2.FONT_HERSHEY_SIMPLEX, @@ -932,17 +1052,19 @@ def _plot_image( # Save or show the image if outpath: - cv2.imwrite(outpath, image) + cv2.imwrite(outpath, np_image) return None else: - return cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + return cv2.cvtColor(np_image, cv2.COLOR_BGR2RGB) # type: ignore @property - def crops(self) -> List[torch.Tensor]: + def crops(self) -> list[torch.Tensor]: + """Detection crops.""" return [self.image[:, y1:y2, x1:x2] for x1, y1, x2, y2 in self.boxes.long().tolist()] @property - def crop_masks(self) -> List[torch.Tensor]: + def crop_masks(self) -> list[torch.Tensor]: + """Masks for detection crops.""" if self.PREFER_POLYGONS: return [ contours_to_masks([contour.round().long() - box[:2]], box[3] - box[1], box[2] - box[0]) @@ -950,14 +1072,14 @@ def crop_masks(self) -> List[torch.Tensor]: ] else: return [ - resize_mask(mask, self.image.shape[1:])[box[1]:box[3], box[0]:box[2]] - for mask, box in zip(self.masks, self.boxes.long()) + resize_masks(mask, self.image.shape[1:])[box[1]:box[3], box[0]:box[2]] # type: ignore - TODO: fixme + for mask, box in zip(self.masks, self.boxes.long()) # type: ignore - TODO: fixme ] @staticmethod def _save_1_crop( crop : torch.Tensor, - mask : Union[torch.Tensor, None], + mask : torch.Tensor | None, path : str, ) -> str: Image.fromarray( @@ -967,35 +1089,41 @@ def _save_1_crop( return path def save_crops( - self : Self, + self, outdir : str, - basename : Optional[str]=None, + basename : str | None=None, mask : bool=False, - identifier : str=None, + identifier : str | None=None, wait : bool=False - ) -> List[Union[str, torch.Tensor]]: + ) -> list[str]: + """Save prediction crops.""" if outdir is None or not os.path.exists(outdir) or not os.path.isdir(outdir): raise RuntimeError(f"Invalid outdir {outdir}, does not exist or is not a directory") - if basename is None: - assert self.image_path is not None, RuntimeError("Cannot save crops without image_path") - basename, _ = os.path.splitext(os.path.basename(self.image_path)) - _, image_ext = os.path.splitext(os.path.basename(self.image_path)) + if self.image_path is not None: + if basename is None: + assert self.image_path is not None, RuntimeError("Cannot save crops without image_path") + basename, _ = os.path.splitext(os.path.basename(self.image_path)) + _, image_ext = os.path.splitext(os.path.basename(self.image_path)) + else: + basename, image_ext = str(uuid.uuid4()), ".jpg" if mask: image_ext = ".png" if identifier is None: - identifier = "NONE" + identifier_field = "" + else: + identifier_field = f'UUID_{identifier}' crops = self.crops if mask: crop_masks = self.crop_masks else: crop_masks = [None] * len(crops) - crop_paths = [os.path.join(outdir, f"crop_{basename}_CROPNUMBER_{i}_UUID_{identifier}{image_ext}") for i in range(len(crops))] + crop_paths = [os.path.join(outdir, f"crop_{basename}_CROPNUMBER_{i}_{identifier_field}{image_ext}") for i in range(len(crops))] - for crop, mask, path in zip(crops, crop_masks, crop_paths): - if isinstance(mask, torch.Tensor): - mask = mask.detach().cpu().clone() - _executor.submit(self._save_1_crop, crop.detach().cpu().clone(), mask, path) + for crop, _mask, path in zip(crops, crop_masks, crop_paths): + if isinstance(_mask, torch.Tensor): + _mask = _mask.detach().cpu().clone() + _executor.submit(self._save_1_crop, crop.detach().cpu().clone(), _mask, path) if wait: _executor.flush() @@ -1003,6 +1131,7 @@ def save_crops( @property def json_data(self): + """JSON-compatible dictionary with instance state data.""" ## Clean up the data # 1. Convert the boxes to list boxes = self.boxes.cpu().tolist() @@ -1016,6 +1145,8 @@ def json_data(self): scales = self.scales # 6. Get the areas (already floats in a list) areas = self.areas + # 7. Get mask data + mdata = self.masks.data return { "boxes": boxes, "contours": contours, @@ -1026,8 +1157,8 @@ def json_data(self): "image_path": self.image_path, "image_width": self.image.shape[2], "image_height": self.image.shape[1], - "mask_width": self.image.shape[2] if self.PREFER_POLYGONS else self.masks.data.shape[2], - "mask_height": self.image.shape[1] if self.PREFER_POLYGONS else self.masks.data.shape[1], + "mask_width": self.image.shape[2] if self.PREFER_POLYGONS else mdata.shape[2], + "mask_height": self.image.shape[1] if self.PREFER_POLYGONS else mdata.shape[1], "identifier": None } @@ -1036,18 +1167,20 @@ def serialize( outpath: str, save_json: bool = True, save_pt: bool = False, - identifier: str = None + identifier: str | None=None ) -> None: - """ - This function serializes the `TensorPredictions` object to a .pt file and/or a .json file. - The .pt file contains an exact copy of the `TensorPredictions` object, while the .json file contains the data in a more human-readable format, - which can be deserialized into a `TensorPredictions` object using the 'load' function. + """Serialize the `TensorPredictions` object to a .pt file and/or a .json file. + + The .pt file contains an exact copy of the `TensorPredictions` object, while the .json file + contains the data in a more human-readable format, which can be + deserialized into a `TensorPredictions` object using the 'load' function. Args: - outpath (str, optional): The path to save the serialized data to. Defaults to None. - save_json (bool, optional): Whether to save the .json file. Defaults to True. Recommended. - save_pt (bool, optional): Whether to save the .pt file. Defaults to False. Rather disk space wasteful. - identifier (str, optional): An identifier for the serialized data. Defaults to None. + outpath: The path to save the serialized data to. Defaults to None. + save_json: Whether to save the .json file. Defaults to True. Recommended. + save_pt: Whether to save the .pt file. Defaults to False. Rather disk space wasteful. + identifier: An identifier for the serialized data. Defaults to None. + """ assert len(outpath) > 0, RuntimeError("Cannot serialize with empty outpath") assert os.path.exists(os.path.dirname(outpath)), RuntimeError(f"Invalide outpath {outpath}, directory does not exist") @@ -1079,20 +1212,20 @@ def serialize( def load( self, - data: Union[str, dict], - device : Optional[DeviceLikeType]=None, - dtype : Optional[torch.types._dtype]=None - ) -> Self: - """ - Deserializes a TensorPredictions object from a .pt or .json file, or a dictionary. OBS: Mutates and returns the current object. + data: str | dict, + device : DeviceLikeType | None=None, + dtype : torch.types._dtype | None=None + ): + """Deserializes a TensorPredictions object from a .pt or .json file, or a dictionary. OBS: Mutates and returns the current object. Args: - data (Union[str, dict]): The path to the file to load or a dictionary with the deserialized json data. - device (Optional[DeviceLikeType], optional): The device to load the data to. Defaults to None. If None, the device is set to "cpu". - dtype (Optional[torch.types._dtype], optional): The data type to load the data as. Defaults to None. If None, the data type is set to torch.float32. + data: The path to the file to load or a dictionary with the deserialized json data. + device: The device to load the data to. Defaults to None. If None, the device is set to "cpu". + dtype: The data type to load the data as. Defaults to None. If None, the data type is set to torch.float32. Returns: - Self: This object with the deserialized data. + This object with the deserialized data. + """ if isinstance(data, str): path = data @@ -1104,17 +1237,18 @@ def load( self = torch.load(path) return self elif ext == ".json": - with open(path, 'r') as f: + with open(path) as f: data = json.load(f) else: raise RuntimeError(f"Unknown file-extension: {ext} for path: {path}") + assert not isinstance(data, str) if device is None: device = torch.device("cpu") if dtype is None: dtype = torch.float32 - empty_image = torch.zeros((3, data["image_height"], data["image_width"]), device=device, dtype=dtype) + 255 + empty_image = torch.zeros((3, data["image_height"], data["image_width"]), device=device, dtype=dtype) + 255 # type: ignore self.__init__(image=empty_image, device=device, dtype=dtype) setattr(self, "PREFER_POLYGONS", True) # Since we only store contours in the .json file, we prefer polygons on loading @@ -1153,40 +1287,42 @@ def load( def save( self, output_directory: str, - overview: Union[bool, str]=True, - crops: Union[bool, str]=True, - metadata: Union[bool, str]=True, + overview: bool | str=True, + crops: bool | str=True, + metadata: bool | str=True, fast: bool=False, mask_crops: bool=False, - identifier: Optional[str]=None, - basename: Optional[str]=None, + identifier: str | None=None, + basename: str | None=None, wait: bool=False - ) -> Optional[str]: - """ - Saves the serialized prediction results, crops, and overview to the given output directory. + ) -> str | None: + """Save the serialized prediction results, crops, and overview to the given output directory. - TODO: Add the identifier to the names of the files, so that we can save multiple predictions for the same image or images with the same name. + TODO: Add the identifier to the names of the files, + so that we can save multiple predictions for the same image or images with the same name. Args: - output_directory (`str`): The directory to save the prediction results to. - overview (`bool | str`, optional): Whether to save the overview image. Defaults to True. + output_directory: The directory to save the prediction results to. + overview: Whether to save the overview image. Defaults to True. If a string is given, it is interpreted as a path to a directory to save the overview image to. - crops (`bool | str`, optional): Whether to save the crops. Defaults to True. + crops: Whether to save the crops. Defaults to True. If a string is given, it is interpreted as a path to a directory to save the crops to. - metadata (`bool | str`, optional): Whether to save the metadata. Defaults to True. + metadata: Whether to save the metadata. Defaults to True. If a string is given, it is interpreted as a path to a directory to save the metadata to. - fast (`bool`, optional): Whether to use the fast version of the overview image. Defaults to False. + fast: Whether to use the fast version of the overview image. Defaults to False. Saves the overview image at half the resolution. - mask_crops (`bool`, optional): Whether to mask the crops. Defaults to False. - identifier (`str | None`, optional): An identifier for the serialized data. Defaults to None. - basename (`str | None`, optional): The base name of the image. Defaults to None. + mask_crops: Whether to mask the crops. Defaults to False. + identifier: An identifier for the serialized data. Defaults to None. + basename: The base name of the image. Defaults to None. If None, the base name is extracted from the image path, which must be set in this case. - wait (`bool`, optional): If true `save` blocks execution until results are finished saving, + wait: If true `save` blocks execution until results are finished saving, otherwise results will be saved asynchronously. Returns: - `str`: The path to the directory containing the serialized data - the crops and overview image(s) are also saved here by default. \\ - If the standard location is not used at all, the directory is not created and None is returned instead. + The path to the directory containing the serialized data. + The crops and overview image(s) are also saved here by default. + If the standard location is not used at all, the directory is not created and None is returned instead. + """ if basename is None: if self.image_path is None: @@ -1195,7 +1331,8 @@ def save( basename = os.path.splitext(os.path.basename(self.image_path))[0] # Construct the prediction directory path prediction_directory = os.path.join(output_directory, basename) - # Create the prediction directory if it does not exist and it is needed (i.e. if we are saving crops, overview, or metadata to a standard location) + # Create the prediction directory if it does not exist and it is needed + # (i.e. if we are saving crops, overview, or metadata to a standard location) prediction_directory_is_used = (overview is True) or (crops is True) or (metadata is True) if prediction_directory_is_used: os.makedirs(prediction_directory, exist_ok=True) @@ -1206,7 +1343,8 @@ def save( overview_directory = overview if isinstance(overview, str) else prediction_directory os.makedirs(overview_directory, exist_ok=True) assert os.path.isdir(overview_directory), RuntimeError(f"Invalid path for overview: {overview_directory}") - # The overview path is then constructed as a .jpg file in the overview directory with the name overview_{base_name}.jpg + # The overview path is then constructed as a .jpg file + # in the overview directory with the name overview_{base_name}.jpg overview_path = os.path.join(overview_directory, f"overview_{basename}_UUID_{identifier}.jpg") # Save the overview image to the overview path scale, linewidth = 1, 2 @@ -1230,9 +1368,11 @@ def save( metadata_directory = metadata if isinstance(metadata, str) else prediction_directory os.makedirs(metadata_directory, exist_ok=True) assert os.path.isdir(metadata_directory), RuntimeError(f"Invalid path for metadata: {metadata_directory}") - # The metadata path is then constructed as a .json file in the metadata directory with the name metadata_{base_name}_id_{identifier}. + # The metadata path is then constructed as a .json file + # in the metadata directory with the name metadata_{base_name}_id_{identifier}. metadata_path = os.path.join(metadata_directory, f'metadata_{basename}_UUID_{identifier}') - # Serialize the data to the metadata path (we don't do this as a future since it is fast, and then we don't need to copy data) + # Serialize the data to the metadata path + # (we don't do this as a future since it is fast, and then we don't need to copy data) self.serialize(outpath=metadata_path, identifier=identifier) if wait: @@ -1242,16 +1382,18 @@ def save( def _process_batch( image : torch.Tensor, - offsets : List[Tuple[Tuple[int, int], Tuple[int, int]]], + offsets : list[tuple[tuple[int, int], tuple[int, int]]], tile_size : int, batch_start_idx : int, batch_size : int, - device : Optional[DeviceLikeType] = None, - model : torch.nn.Module = None, + device : DeviceLikeType | None = None, + model : torch.nn.Module = None, # type: ignore # TODO: fixthis! time : bool = False, callback : str = "__call__", **kwargs : Any # Swallow any extra arguments - ) -> Tuple[int, torch.Tensor, Tuple]: + ) -> tuple[torch.Tensor, Any, tuple[int, int, int] | None]: + start_batch_event = end_fetch_event = end_forward_event = \ + end_batch_event = start_batch_event = current_device_stream = None if time: # Initialize batch timing calculations start_batch_event = torch.cuda.Event(enable_timing=True) @@ -1269,18 +1411,22 @@ def _process_batch( ], dim=0) if time: # Record end of fetch + assert current_device_stream is not None and end_fetch_event is not None end_fetch_event.record(current_device_stream) # Forward pass the model on the batch tiles with torch.no_grad(): batch_outputs = getattr(model, callback)(batch) if time: # Record end of forward + assert current_device_stream is not None and end_forward_event is not None end_forward_event.record(current_device_stream) # Record batch end + assert current_device_stream is not None and end_batch_event is not None end_batch_event.record(current_device_stream) # Calculate timing torch.cuda.synchronize(device=device) + assert current_device_stream is not None and start_batch_event is not None and end_fetch_event is not None batch_time = start_batch_event.elapsed_time(end_batch_event) / 1000 # Convert to seconds fetch_time = start_batch_event.elapsed_time(end_fetch_event) / 1000 # Convert to seconds forward_time = end_fetch_event.elapsed_time(end_forward_event) / 1000 # Convert to seconds @@ -1291,77 +1437,93 @@ def _process_batch( # ) # Return the postprocessed batch outputs and optionally the timing if time: - return batch, batch_outputs, (batch_time, fetch_time, forward_time) + return batch, batch_outputs, (batch_time, fetch_time, forward_time) # type: ignore else: return batch, batch_outputs, None -class Predictor(object): - HYPERPARAMETERS : List[str] = CFG_PARAMS +class Predictor: + """A flatbug predictor. + + The flatbug is built to be used primarily via calling the instance itself + (which is an alias for `Predictor.pyramid_predictions`): + + ``` + model = Predictor(...) + prediction = model(image) + ``` + + The flatbug predictor returns an object of type `TensorPredictions` which + is designed for use in a vertically integrated computer vision pipeline + (i.e. intermediary results are not written to disk, but kept in GPU or CPU + RAM to avoid I/O), but also has export and visualization functionality. + """ + + HYPERPARAMETERS : list[str] = CFG_PARAMS """ The available hyperparameters for the predictor. \\ These can be set using the `set_hyperparameters` class method. """ # Hyperparameters, set to None so they are visible in the class - MIN_MAX_OBJ_SIZE : Tuple[int, int] = None + MIN_MAX_OBJ_SIZE : tuple[int, int] = None # type: ignore """ Defines the minimum and maximum object size as seen in a single tile. \\ Size is defined as the square root of the pixel area of the bounding box. """ - MAX_MASK_SIZE : int = None + MAX_MASK_SIZE : int = None # type: ignore """ Defines the maximum size of the segmentation masks. \\ Only applies if PREFER_POLYGONS is False. """ - SCORE_THRESHOLD : float = None + SCORE_THRESHOLD : float = None # type: ignore """ The score threshold for the predictions. \\ TODO: This should be called CONFIDENCE_THRESHOLD. """ - OVERLAP_THRESHOLD: float = None + OVERLAP_THRESHOLD: float = None # type: ignore """ The overlap (e.g. IOU) threshold used to determine if two instances are duplicates. \\ """ - MINIMUM_TILE_OVERLAP : int = None + MINIMUM_TILE_OVERLAP : int = None # type: ignore """ The minimum - but not necessarily the maximum - overlap between tiles \\ in a single layer of the pyramid. Increasing this value will increase \\ the computation time, but may improve the detection of large instances. """ - EDGE_CASE_MARGIN : int = None + EDGE_CASE_MARGIN : int = None # type: ignore """ The margin to add to the edge of the image to catch instances that are \\ split between tiles. The margin is added to the edge of the image, such \\ that instances on the true edge of the images are not removed. """ - PREFER_POLYGONS : bool = None + PREFER_POLYGONS : bool = None # type: ignore """ Whether to prefer representing the instance segmentation using polygons \\ instead of masks. This is a much more compact representation, but cannot \\ represent complex shapes (like holes in the mask), only concave polygons. """ - EXPERIMENTAL_NMS_OPTIMIZATION : bool = None + EXPERIMENTAL_NMS_OPTIMIZATION : bool = None # type: ignore """ Enables an experimental optimization for the NMS step. \\ This optimization improves the performance of the NMS step when there are \\ many instances in a large image and CUDA is available. """ - OVERLAP_METRIC : str = None + OVERLAP_METRIC : str = None # type: ignore """ Metric to use for NMS. One of "IOU" or "IOS", more might be added in the future. """ - TIME : bool = None + TIME : bool = None # type: ignore """ Whether to time the different parts of the prediction process. \\ Enabling this will print a verbose output of the timing of the different \\ parts of the prediction process. """ - TILE_SIZE : int = None + TILE_SIZE : int = None # type: ignore """ The size of the tiles to split the image into. \\ This is defined by the model and should probably not be changed. """ - BATCH_SIZE : int = None + BATCH_SIZE : int = None # type: ignore """ The batch size to use for the prediction. \\ This determines how many tiles are processed in parallel. \\ @@ -1373,16 +1535,25 @@ class Predictor(object): def __init__( self, - model : Union[str, pathlib.Path]="flat_bug_M.pt", - cfg : Optional[Union[dict, str, os.PathLike]]=None, - device : Union[str, torch.device, int, List[Union[str, torch.device, int]]]=torch.device("cpu"), - dtype : Union[torch.types._dtype, str]=torch.float32 + model : str | pathlib.Path="flat_bug_M.pt", + cfg : dict | str | Path | None=None, + device : str | torch.device | int | list[str | torch.device | int]=torch.device("cpu"), + dtype : torch.types._dtype | str=torch.float32 ): - cfg = read_cfg(cfg, strict=True) if isinstance(cfg, (str, os.PathLike)) else (cfg or {}) + """Instantiate a flatbug predictor. + + Args: + model: Path to a local weight file, or the name of a weight file in the flatbug model zoo. + cfg: A dictionary or a path to a YAML containing the flatbug config for this model instance. + device: Which device to run the model on. + dtype: Which dtype to run the model on. + + """ + cfg = read_cfg(cfg, strict=True) if isinstance(cfg, (str, Path)) else (cfg or {}) self.set_hyperparameters(**{**DEFAULT_CFG, **cfg}) self._multi_gpu = isinstance(device, (list, tuple)) - self._devices = [torch.device(device)] if not self._multi_gpu else [torch.device(d) for d in device] + self._devices = [torch.device(device)] if not self._multi_gpu else [torch.device(d) for d in device] # type: ignore if len(self._devices) > 1: # TODO: Implement single-producer multi-consumer model for _detect_instances in the multi-gpu case raise NotImplementedError("Multi-GPU is not implemented yet") @@ -1411,8 +1582,10 @@ def __init__(self, d): "model": yolo.model, "fp16" : self._dtype == torch.float16, "dnn" : False, - "data" : None # If we want to support multiclass inference, this needs to point to "Path to the additional data.yaml file containing class names. Optional." - # see: https://github.com/ultralytics/ultralytics/blob/bc9fd45cdf10ebe8009037aaf8def2353761c9ed/ultralytics/nn/autobackend.py#L53 + # If we want to support multiclass inference, + # this needs to point to "Path to the additional data.yaml file containing class names. Optional." + # see: https://github.com/ultralytics/ultralytics/blob/bc9fd45cdf10ebe8009037aaf8def2353761c9ed/ultralytics/nn/autobackend.py#L53 + "data" : None }) pred.args = args pred.setup_model(self=pred, model=yolo.model, verbose=True) @@ -1428,15 +1601,15 @@ def __init__(self, d): self._yolo_predictor = None - def set_hyperparameters(self, **kwargs) -> Self: - """ - Mutably set the hyperparameters for the predictor. + def set_hyperparameters(self, **kwargs): + """Set the hyperparameters in-place for the predictor. Args: **kwargs: The hyperparameters to set. Returns: - Self: This object (mutated with the new hyperparameters). + This instance with the new hyperparameters. + """ for k, v in kwargs.items(): if k in self.HYPERPARAMETERS: @@ -1454,9 +1627,10 @@ def _detect_instances( TILE_SIZE = self.TILE_SIZE this_MIN_MAX_OBJ_SIZE = list(self.MIN_MAX_OBJ_SIZE) this_EDGE_CASE_MARGIN = self.EDGE_CASE_MARGIN - # If we are at the top level, we don't want to remove large instances - since there are no layers above to detect them as small instances + # If we are at the top level, we don't want to remove large instances + # - since there are no layers above to detect them as small instances if max_scale: - this_MIN_MAX_OBJ_SIZE[1] = 1e9 + this_MIN_MAX_OBJ_SIZE[1] = int(1e9) this_EDGE_CASE_MARGIN = 0 if self.TIME: @@ -1518,7 +1692,7 @@ def _detect_instances( batch_times, fetch_times, forward_times, postprocess_times = [], [], [], [] start_event.record(main_stream) - postprocessed_results = [None for _ in range(len(offsets))] + postprocessed_results : list[Results] = [None for _ in range(len(offsets))] # type: ignore batches = 0 with torch.no_grad(): for batch_start_idx in range(0, len(offsets), self.BATCH_SIZE): @@ -1546,10 +1720,11 @@ def _detect_instances( edge_margin = self.EDGE_CASE_MARGIN, ) for batch_index in range(len(this_postprocessed_results)): - tr = Results(**this_postprocessed_results[batch_index]) + tr = Results(**this_postprocessed_results[batch_index]) # type: ignore tr.orig_img = None # Comment this line if we want debug output. postprocessed_results[batch_start_idx + batch_index] = tr if self.TIME: + assert timing is not None batch_times.append(timing[0]) fetch_times.append(timing[1]) forward_times.append(timing[2]) @@ -1565,7 +1740,13 @@ def _detect_instances( fetch_time, forward_time, postprocess_time = sum(fetch_times), sum(forward_times), sum(postprocess_times) total_batch_time = sum(batch_times) + postprocess_time overhead_prop = (total_elapsed - total_batch_time) / total_elapsed - fetch_prop, forward_prop, postprocess_prop = fetch_time / total_batch_time, forward_time / total_batch_time, postprocess_time / total_batch_time + fetch_prop, forward_prop, postprocess_prop = ( + fetch_time / total_batch_time, + forward_time / total_batch_time, + postprocess_time / total_batch_time + ) + + # ruff: disable[E501] # DEBUG ##### # if self.DEBUG: # logger.info(f'Number of tiles processed before merging and plotting: {len(postprocessed_results)}') @@ -1580,16 +1761,25 @@ def _detect_instances( # for i in range(len(postprocessed_results)): # postprocessed_results[i].orig_img = torch.tensor(postprocessed_results[i].orig_img).squeeze(0).to(dtype=self._dtype, device=self._device) / 255.0 # Backtransform ############### + # ruff: enable[E501] ## Combine the results from the tiles MASK_SIZE = 256 # Defined by the YOLOv8 model segmentation architecture - MASK_TO_IMG_RATIO = MASK_SIZE / torch.tensor([TILE_SIZE, TILE_SIZE], dtype=torch.float32, device=self._device).unsqueeze(0) + MASK_TO_IMG_RATIO = MASK_SIZE / torch.tensor( + [TILE_SIZE, TILE_SIZE], dtype=torch.float32, device=self._device + ).unsqueeze(0) # For the boxes, we can simply add the offsets (and possibly subtract the padding) - box_offsetters = torch.tensor([[o[1][0] - pad_lrtb[2], o[1][1] - pad_lrtb[0]] for o in offsets], dtype=torch.float32, device=self._device) - # However for the masks, we need to create a new mask which can contain every tile, and then add the masks from each tile to the correct area - this will of course use some memory, but it's probably not too bad + box_offsetters = torch.tensor( + [[o[1][0] - pad_lrtb[2], o[1][1] - pad_lrtb[0]] for o in offsets], + dtype=torch.float32, device=self._device + ) + # However for the masks, we need to create a new mask which can contain every tile, + # and then add the masks from each tile to the correct area + # - this will of course use some memory, but it's probably not too bad # Since the masks do not have the same size as the tiles, we need to scale the offsets mask_offsetters = box_offsetters * MASK_TO_IMG_RATIO - # We also need to round the offsets, since they may not line up with the pixel-grid - RE: Now they do since I made sure the offsets are multiples of 4 + # We also need to round the offsets, since they may not line up with the pixel-grid + # RE: Now they do since I made sure the offsets are multiples of 4 mask_offsetters = torch.round(mask_offsetters).long() # The padding must also be scaled and subtracted from the new mask size new_mask_size = ( @@ -1599,7 +1789,7 @@ def _detect_instances( # Finally, we can merge the results - this function basically just does what I described above orig_img = image[:, pad_lrtb[2]:(-pad_lrtb[3] if pad_lrtb[3] != 0 else None), pad_lrtb[0]:(-pad_lrtb[1] if pad_lrtb[1] != 0 else None)] if padded else image - postprocessed_results = merge_tile_results( + merged_results = merge_tile_results( results = postprocessed_results, orig_img = orig_img.permute(1, 2, 0), box_offsetters = box_offsetters.to(self._dtype), @@ -1610,6 +1800,7 @@ def _detect_instances( exclude_masks = self.PREFER_POLYGONS ) + # ruff: disable[E501] #### DEBUG ##### # if self.DEBUG: # logger.info(f'Number of tiles processed after merging and filtering: {len(ps)}') @@ -1622,6 +1813,7 @@ def _detect_instances( # ps.orig_img = torch.tensor(ps.orig_img).squeeze(0).to(dtype=self._dtype, device=self._device) / 255.0 # Backtransform # # ps.boxes.data[:, :4] *= scale ################# + # ruff: enable[E501] if self.TIME: end_detect.record(main_stream) @@ -1641,7 +1833,7 @@ def _detect_instances( if hasattr(self, "total_forward_time"): self.total_forward_time += forward_time return Prepared_Results( - predictions = postprocessed_results, + predictions = merged_results, scale = real_scale, device = self._device, dtype = self._dtype @@ -1649,47 +1841,50 @@ def _detect_instances( def pyramid_predictions( self, - image : Union[torch.Tensor, str], - path : Optional[str]=None, + image : torch.Tensor | str, + path : str | None=None, scale_increment : float=2/3, - scale_before : Union[float, int]=1, + scale_before : float | int=1, single_scale : bool=False ) -> TensorPredictions: - """ - Performs inference on an image at multiple scales and returns the predictions. + """Perform inference on an image at multiple scales and return the predictions. Args: - image (Union[torch.Tensor, str]): The image to run inference on. If a string is given, the image is read from the path. - If it is a `torch.Tensor`, the path must be provided. \\ - We assume that floating point images are in the range [0, 1] and integer images are in the range [0, integer_type_max]. \\ - (see https://github.com/pytorch/vision/blob/6d7851bd5e2bedc294e40e90532f0e375fcfee04/torchvision/transforms/_functional_tensor.py#L66) - path (Optional[str], optional): The path to the image. Defaults to None. Must be provided if `image` is a `torch.Tensor`. - scale_increment (float, optional): The scale increment to use when resizing the image. Defaults to 2/3. - scale_before (Union[float, int], optional): The scale to apply before running inference. Defaults to 1. - single_scale (bool, optional): Whether to run inference on a single scale. Defaults to False. + image: The image to run inference on. If a string is given, the image is read from the path. + If it is a `torch.Tensor`, the path must be provided. + We assume that floating point images are in the range [0, 1] and integer images are in the range [0, integer_type_max]. + *(see https://github.com/pytorch/vision/blob/6d7851bd5e2bedc294e40e90532f0e375fcfee04/torchvision/transforms/_functional_tensor.py#L66)* + path: The path to the image. Defaults to None. Must be provided if `image` is a `torch.Tensor`. + scale_increment: The scale increment to use when resizing the image. Defaults to 2/3. + scale_before: The scale to apply before running inference. Defaults to 1. + single_scale: Whether to run inference on a single scale. Defaults to False. Returns: - TensorPredictions: The predictions for the image. + The predictions for the image. + """ if self.TIME: # Initialize timing calculations start_pyramid, end_pyramid = torch.cuda.Event(enable_timing=True), torch.cuda.Event(enable_timing=True) start_pyramid.record() + real_path = image if isinstance(image, str) else path if isinstance(image, str): - path : str = image - image : torch.Tensor = decode_image( + tensor_image : torch.Tensor = decode_image( input=image, mode=ImageReadMode.RGB, apply_exif_orientation=True ) elif isinstance(image, torch.Tensor): - image = image.cpu() - logger.debug("Input image source file not specified for prediction, saving the prediction will require specifying the source file basename.") + tensor_image = image.cpu() + logger.debug( + "Input image source file not specified for prediction, " + "saving the prediction will require specifying the source file basename." + ) else: raise TypeError(f"Unknown type for image: {type(image)}, expected str or torch.Tensor") - c, h, w = image.shape + c, h, w = tensor_image.shape transform_list = [] if scale_before != 1: @@ -1698,34 +1893,45 @@ def pyramid_predictions( transform_list.append(resize) # Check if the image has an integer data type - if image.dtype in [torch.uint8, torch.int8, torch.int16, torch.int32, torch.int64]: + if tensor_image.dtype in [torch.uint8, torch.int8, torch.int16, torch.int32, torch.int64]: transform_list.append(transforms.ConvertImageDtype(self._dtype)) - # A border is always added now, to avoid edge-cases on the actual edge of the image. I.e. only detections on internal edges of tiles should be removed, not detections on the edge of the image. + # A border is always added now, to avoid edge-cases on the actual edge of the image. + # I.e. only detections on internal edges of tiles should be removed, not detections on the edge of the image. edge_case_margin_padding_multiplier = 2 - padding_offset = torch.tensor((self.EDGE_CASE_MARGIN, self.EDGE_CASE_MARGIN), dtype=self._dtype) * edge_case_margin_padding_multiplier + padding_offset = torch.tensor( + (self.EDGE_CASE_MARGIN, self.EDGE_CASE_MARGIN), dtype=self._dtype + ) * edge_case_margin_padding_multiplier + if padding_offset.sum() > 0: padding_for_edge_cases = transforms.Pad( padding=self.EDGE_CASE_MARGIN * edge_case_margin_padding_multiplier, fill=0, padding_mode='constant' ) - # padding_for_edge_cases = InpaintPad(padding=self.EDGE_CASE_MARGIN * edge_case_margin_padding_multiplier) + # padding_for_edge_cases = InpaintPad( + # padding=self.EDGE_CASE_MARGIN * edge_case_margin_padding_multiplier + # ) transform_list.append(padding_for_edge_cases) else: padding_offset[:] = 0 transformed_image = ( - transforms.Compose(transform_list)(image) if transform_list else image + transforms.Compose(transform_list)(tensor_image) if transform_list else tensor_image ).to(device=self._device, dtype=self._dtype) # Check correct dimensions - assert len(transformed_image.shape) == 3, RuntimeError(f"transformed_image.shape {transformed_image.shape} != 3") + assert len(transformed_image.shape) == 3, RuntimeError( + f"transformed_image.shape {transformed_image.shape} != 3" + ) # Check correct number of channels - assert transformed_image.shape[0] == 3, RuntimeError(f"transformed_image.shape[0] {transformed_image.shape[0]} != 3. The image is probably supplied in WxHxC instead of CxWxH, try image.permute(2, 1, 0) before passing it.") + assert transformed_image.shape[0] == 3, RuntimeError( + f"transformed_image.shape[0] {transformed_image.shape[0]} != 3. " + "The image is probably supplied in WxHxC instead of CxWxH, try image.permute(2, 1, 0) before passing it." + ) max_dim = max(transformed_image.shape[1:]) - min_dim = min(transformed_image.shape[1:]) + # min_dim = min(transformed_image.shape[1:]) scales = [] @@ -1761,8 +1967,8 @@ def pyramid_predictions( all_preds = TensorPredictions( predictions = all_preds, - image = image.to(self._device), - image_path = path, + image = tensor_image.to(self._device), + image_path = real_path, dtype = self._dtype, device = self._device, time = self.TIME, @@ -1792,27 +1998,27 @@ def pyramid_predictions( def __call__( self, - image : Union[torch.Tensor, str], - path : Optional[str]=None, + image : torch.Tensor | str, + path : str | None=None, scale_increment : float=2/3, - scale_before : Union[float, int]=1, + scale_before : float | int=1, single_scale : bool=False ) -> TensorPredictions: - """ - Performs inference on an image at multiple scales and returns the predictions. + """Perform inference on an image at multiple scales and return the predictions. Args: - image (`Union[torch.Tensor, str]`): The image to run inference on. If a string is given, the image is read from the path. - If it is a `torch.Tensor`, the path must be provided. \\ - We assume that floating point images are in the range [0, 1] and integer images are in the range [0, integer_type_max]. \\ - (see https://github.com/pytorch/vision/blob/6d7851bd5e2bedc294e40e90532f0e375fcfee04/torchvision/transforms/_functional_tensor.py#L66) - path (`Optional[str]`, optional): The path to the image. Defaults to None. Must be provided if `image` is a `torch.Tensor`. - scale_increment (`float`, optional): The scale increment to use when resizing the image. Defaults to 2/3. - scale_before (`Union[float, int]`, optional): The scale to apply before running inference. Defaults to 1. - single_scale (`bool`, optional): Whether to run inference on a single scale. Defaults to False. + image: The image to run inference on. If a string is given, the image is read from the path. + If it is a `torch.Tensor`, the path must be provided. + We assume that floating point images are in the range [0, 1] and integer images are in the range [0, integer_type_max]. + *(see https://github.com/pytorch/vision/blob/6d7851bd5e2bedc294e40e90532f0e375fcfee04/torchvision/transforms/_functional_tensor.py#L66)* + path: The path to the image. Defaults to None. Must be provided if `image` is a `torch.Tensor`. + scale_increment: The scale increment to use when resizing the image. Defaults to 2/3. + scale_before: The scale to apply before running inference. Defaults to 1. + single_scale: Whether to run inference on a single scale. Defaults to False. Returns: - out (`TensorPredictions`): The predictions for the image. + The predictions for the image. + """ params = locals() params.pop("self", None) diff --git a/src/flat_bug/tests/__init__.py b/src/flat_bug/tests/__init__.py index e69de29..e5b970e 100644 --- a/src/flat_bug/tests/__init__.py +++ b/src/flat_bug/tests/__init__.py @@ -0,0 +1 @@ +# noqa: D104 \ No newline at end of file diff --git a/src/flat_bug/tests/generate_model_outputs.py b/src/flat_bug/tests/generate_model_outputs.py index b717acf..8436f89 100644 --- a/src/flat_bug/tests/generate_model_outputs.py +++ b/src/flat_bug/tests/generate_model_outputs.py @@ -1,12 +1,17 @@ -import os, argparse +# noqa: D100 +import argparse +import os import torch from torchvision.io import read_image -from flat_bug.tests.test_predictor import DummyModel -from flat_bug.tests.test_predictor import ASSET_DIR, ASSET_NAME, TEST_MODEL_NAME +from flat_bug.tests.test_predictor import ASSET_DIR, ASSET_NAME, TEST_MODEL_NAME, DummyModel -# Command I used: python3 src/flat_bug/tests/generate_model_outputs.py --model model_snapshots/fb_2024-03-18_large_best.pt --image src/flat_bug/tests/assets/ALUS_Non-miteArachnids_Unknown_2020_11_03_4545.jpg --type both +# ruff: disable[E501] +# Command I used: +# python3 src/flat_bug/tests/generate_model_outputs.py --model model_snapshots/fb_2024-03-18_large_best.pt --image src/flat_bug/tests/assets/ALUS_Non-miteArachnids_Unknown_2020_11_03_4545.jpg --type both +# +# ruff: enable[E501] if __name__ == "__main__": parser = argparse.ArgumentParser() diff --git a/src/flat_bug/tests/remote_lfs_fallback.py b/src/flat_bug/tests/remote_lfs_fallback.py index c94fc88..68abd12 100644 --- a/src/flat_bug/tests/remote_lfs_fallback.py +++ b/src/flat_bug/tests/remote_lfs_fallback.py @@ -1,20 +1,24 @@ +# noqa: D100 import os import re from urllib.request import urlretrieve -def file_is_lfs_or_erda_pointer(file): - with open(file, "r") as f: +def file_is_lfs_or_erda_pointer(file): # noqa: D103 + with open(file) as f: try: return bool(re.search(r"git-lfs\.github\.com|ERDA Pointer", f.read())) except UnicodeDecodeError: return False -def check_file_with_remote_fallback(file, file_storage : str="https://anon.erda.au.dk/share_redirect/ecgKtuRWe5"): +def check_file_with_remote_fallback(file, file_storage : str="https://anon.erda.au.dk/share_redirect/ecgKtuRWe5"): # noqa: D103 if not os.path.exists(file) or file_is_lfs_or_erda_pointer(file): remote_uri = f"{file_storage}/{os.path.basename(file)}" try: urlretrieve(remote_uri, file) except Exception as e: - raise type(e)(f"Failed to download test file {file} from remote file storage ({remote_uri}), perhaps the file is not available." + str(e)) + raise RuntimeError( + f"Failed to download test file {file} from remote file storage ({remote_uri})." + "\n\tPerhaps the file is not available." + ) from e return file \ No newline at end of file diff --git a/src/flat_bug/tests/restore_assets.py b/src/flat_bug/tests/restore_assets.py index 8f0b039..55b4c62 100644 --- a/src/flat_bug/tests/restore_assets.py +++ b/src/flat_bug/tests/restore_assets.py @@ -1,8 +1,13 @@ +# noqa: D100 import os if __name__ == "__main__": ASSET_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "assets")) - prediction_assets = [os.path.join(ASSET_DIR, f) for f in os.listdir(ASSET_DIR) if f.startswith("pyramid_") or f.startswith("single_scale_")] + prediction_assets = [ + os.path.join(ASSET_DIR, f) + for f in os.listdir(ASSET_DIR) + if f.startswith("pyramid_") or f.startswith("single_scale_") + ] for asset in prediction_assets: print(asset) diff --git a/src/flat_bug/tests/test_augmentations.py b/src/flat_bug/tests/test_augmentations.py index ca2f8af..de19def 100644 --- a/src/flat_bug/tests/test_augmentations.py +++ b/src/flat_bug/tests/test_augmentations.py @@ -1,24 +1,21 @@ +"""Tests for flatbug augmentations (including their integration into the dataloader).""" +import math +import os import unittest - -import os, math - from copy import deepcopy - from typing import Any import cv2 import numpy as np import torch - +from ultralytics.data.augment import Compose from ultralytics.data.base import Path from ultralytics.data.utils import verify_image_label -from ultralytics.data.augment import Compose from ultralytics.utils import IterableSimpleNamespace from ultralytics.utils.instance import Instances from ultralytics.utils.ops import resample_segments from flat_bug.datasets import train_augmentation_pipeline, validation_augmentation_pipeline - from flat_bug.tests.remote_lfs_fallback import check_file_with_remote_fallback TEST_HYP = { @@ -44,7 +41,7 @@ check_file_with_remote_fallback(TEST_IMG) check_file_with_remote_fallback(TEST_LABEL) -def generate_train_augmentation_pipeline(hyp): +def generate_train_augmentation_pipeline(hyp): # noqa: D103 hyp = IterableSimpleNamespace(**hyp) return train_augmentation_pipeline( hyperparameters=hyp, @@ -55,7 +52,7 @@ def generate_train_augmentation_pipeline(hyp): use_keypoints=hyp.use_keypoints ) -def generate_validation_augmentation_pipeline(hyp): +def generate_validation_augmentation_pipeline(hyp): # noqa: D103 hyp = IterableSimpleNamespace(**hyp) return validation_augmentation_pipeline( image_size=hyp.imgsz, @@ -64,11 +61,11 @@ def generate_validation_augmentation_pipeline(hyp): use_keypoints=hyp.use_keypoints ) -def mock_verify_image_label(image_path, label_path): +def mock_verify_image_label(image_path, label_path): # noqa: D103 try: args = (image_path, label_path, "unit_test", False, 1, 0, 0) im_file, lb, shape, segments, keypoint, nm_f, nf_f, ne_f, nc_f, msg = verify_image_label(args) - except ValueError as e: + except ValueError: args = (image_path, label_path, "unit_test", False, 1, 0, 0, True) im_file, lb, shape, segments, keypoint, nm_f, nf_f, ne_f, nc_f, msg = verify_image_label(args) label = { @@ -81,23 +78,29 @@ def mock_verify_image_label(image_path, label_path): "normalized": True, "bbox_format": "xywh", } - label["instances"] = Instances(np.array(label["bboxes"]), np.array(resample_segments(label["segments"])), label["keypoints"], bbox_format=label["bbox_format"], normalized=label["normalized"]) + label["instances"] = Instances( + np.array(label["bboxes"]), + np.array(resample_segments(label["segments"])), + label["keypoints"], + bbox_format=label["bbox_format"], + normalized=label["normalized"] + ) return label -def mock_yolo_base_dataset_load_image(image_path, imgsz, rect_mode=False): - """ - Loads an image from the given path and resizes it if necessary. +def mock_yolo_base_dataset_load_image(image_path, imgsz, rect_mode=False): # noqa: D103 + """Load an image from the given path and resize it if necessary. Args: - image_path (str or Path): Path to the image file. - imgsz (int): Desired image size for resizing. - rect_mode (bool): Whether to maintain the aspect ratio when resizing. + image_path: Path to the image file. + imgsz: Desired image size for resizing. + rect_mode: Whether to maintain the aspect ratio when resizing. Returns: tuple: (im, (h0, w0), resized_shape) - im: The loaded and possibly resized image. - (h0, w0): Original height and width of the image. - resized_shape: Shape of the resized image. + """ f = Path(image_path) @@ -120,7 +123,7 @@ def mock_yolo_base_dataset_load_image(image_path, imgsz, rect_mode=False): return im, (h0, w0), im.shape[:2] -def make_empty(obj : Any) -> Any: +def make_empty(obj : Any) -> Any: # noqa: D103 if isinstance(obj, np.ndarray): obj = np.empty((0, *obj.shape[1:]), dtype=obj.dtype) elif isinstance(obj, torch.Tensor): @@ -129,13 +132,16 @@ def make_empty(obj : Any) -> Any: obj = [] return obj -class TestMockYOLOHelpers(unittest.TestCase): - def test_mock_yolo_base_dataset_load_image(self): +class TestMockYOLOHelpers(unittest.TestCase): # noqa: D101 + def test_mock_yolo_base_dataset_load_image(self): # noqa: D102 loaded_img, _, _ = mock_yolo_base_dataset_load_image(TEST_IMG, TEST_HYP["imgsz"]) self.assertIsInstance(loaded_img, np.ndarray, msg=f"Expected {np.ndarray} object, got {type(loaded_img)}") - self.assertEqual(loaded_img.shape, (TEST_HYP["imgsz"], TEST_HYP["imgsz"], 3), msg=f"Expected image shape ({TEST_HYP['imgsz']}, {TEST_HYP['imgsz']}, 3), got {loaded_img.shape}") + self.assertEqual( + loaded_img.shape, (TEST_HYP["imgsz"], TEST_HYP["imgsz"], 3), + msg=f"Expected image shape ({TEST_HYP['imgsz']}, {TEST_HYP['imgsz']}, 3), got {loaded_img.shape}" + ) - def test_mock_verify_image_label(self): + def test_mock_verify_image_label(self): # noqa: D102 result = mock_verify_image_label(TEST_IMG, TEST_LABEL) self.assertIsInstance(result, dict, msg=f"Expected {dict} object, got {type(result)}") correct = { @@ -155,16 +161,16 @@ def test_mock_verify_image_label(self): continue self.assertIsInstance(result[k], v, msg=f"Invalid type for key '{k}'. Expected {v}, got {type(result[k])}") -class TestAugmentations(unittest.TestCase): - def test_generate_train_augmentation_pipeline(self): +class TestAugmentations(unittest.TestCase): # noqa: D101 + def test_generate_train_augmentation_pipeline(self): # noqa: D102 pipeline = generate_train_augmentation_pipeline(TEST_HYP) self.assertTrue(isinstance(pipeline, Compose), msg=f"Expected {Compose} object, got {type(pipeline)}") - def test_generate_validation_augmentation_pipeline(self): + def test_generate_validation_augmentation_pipeline(self): # noqa: D102 pipeline = generate_validation_augmentation_pipeline(TEST_HYP) self.assertTrue(isinstance(pipeline, Compose), msg=f"Expected {Compose} object, got {type(pipeline)}") - def test_train_augmentation_pipeline(self): + def test_train_augmentation_pipeline(self): # noqa: D102 pipeline = generate_train_augmentation_pipeline(TEST_HYP) loaded_img, _, _ = mock_yolo_base_dataset_load_image(TEST_IMG, int(TEST_HYP["imgsz"] * 2)) pipeline_input = mock_verify_image_label(TEST_IMG, TEST_LABEL) @@ -188,7 +194,13 @@ def test_train_augmentation_pipeline(self): if k == "img": continue if isinstance(v, Instances): - empty_pipeline_input[k] = Instances(make_empty(v.bboxes), make_empty(v.segments), make_empty(v.keypoints), bbox_format=empty_pipeline_input["bbox_format"], normalized=v.normalized) + empty_pipeline_input[k] = Instances( + make_empty(v.bboxes), + make_empty(v.segments), + make_empty(v.keypoints), + bbox_format=empty_pipeline_input["bbox_format"], + normalized=v.normalized + ) else: empty_pipeline_input[k] = make_empty(v) try: @@ -197,7 +209,7 @@ def test_train_augmentation_pipeline(self): raise type(e)("Failed to execute training augmentation pipeline on image without labels due to:\n\t" + str(e)) self.assertIsInstance(out, dict, msg="Invalid output of training augmentation pipeline on image without labels") - def test_validation_augmentation_pipeline(self): + def test_validation_augmentation_pipeline(self): # noqa: D102 pipeline = generate_validation_augmentation_pipeline(TEST_HYP) loaded_img, _, _ = mock_yolo_base_dataset_load_image(TEST_IMG, int(TEST_HYP["imgsz"] * 2)) pipeline_input = mock_verify_image_label(TEST_IMG, TEST_LABEL) @@ -214,7 +226,13 @@ def test_validation_augmentation_pipeline(self): if k == "img": continue if isinstance(v, Instances): - empty_pipeline_input[k] = Instances(make_empty(v.bboxes), make_empty(v.segments), make_empty(v.keypoints), bbox_format=empty_pipeline_input["bbox_format"], normalized=v.normalized) + empty_pipeline_input[k] = Instances( + make_empty(v.bboxes), + make_empty(v.segments), + make_empty(v.keypoints), + bbox_format=empty_pipeline_input["bbox_format"], + normalized=v.normalized + ) else: empty_pipeline_input[k] = make_empty(v) try: diff --git a/src/flat_bug/tests/test_config.py b/src/flat_bug/tests/test_config.py index 4b89024..5a08f0b 100644 --- a/src/flat_bug/tests/test_config.py +++ b/src/flat_bug/tests/test_config.py @@ -1,8 +1,10 @@ +"""Tests for flatbug config submodule.""" +import copy +import os +import tempfile import unittest -import os, tempfile, copy - -from flat_bug.config import get_type_def, check_types, check_cfg_types, read_cfg, write_cfg, DEFAULT_CFG +from flat_bug.config import DEFAULT_CFG, check_cfg_types, check_types, get_type_def, read_cfg, write_cfg TEST_OBJECTS = { "float": 1.23, @@ -40,7 +42,7 @@ "list of mixed list and mixed tuple": [(tuple, list), [[(tuple, list), [int, str]], [(tuple, list), [str, int]]]] } -def check_equals_recursive(obj1, obj2): +def check_equals_recursive(obj1, obj2): # noqa: D103 if isinstance(obj1, (tuple, list)): if len(obj1) != len(obj2): return False @@ -50,19 +52,19 @@ def check_equals_recursive(obj1, obj2): return True return obj1 == obj2 -class TestConfig(unittest.TestCase): - def test_check_types(self): +class TestConfig(unittest.TestCase): # noqa: D101 + def test_check_types(self): # noqa: D102 for i, (key, obj) in enumerate(TEST_OBJECTS.items()): expected_type = get_type_def(obj) check_types(obj, expected_type, f"Object '{key}' ({i})") check_types(obj, TEST_OBJECTS_TYPES_LIST_TUPLE_NOT_INTERCHANGEABLE[key], f"Object '{key}' ({i})") check_types(obj, TEST_OBJECTS_TYPES_LIST_TUPLE_INTERCHANGEABLE[key], f"Object '{key}' ({i})") - def test_check_cfg_types(self): + def test_check_cfg_types(self): # noqa: D102 try: check_cfg_types(DEFAULT_CFG, strict=True) except Exception as e: - type(e)(f"Error raised when checking the types of the default config:\n" + str(e)) + type(e)("Error raised when checking the types of the default config:\n" + str(e)) altered_cfg = copy.deepcopy(DEFAULT_CFG) altered_cfg["UNKNOWN_KEY"] = "value" with self.assertRaises(KeyError, msg="Failed to raise an error when checking config with unknown key and strict=True"): @@ -70,24 +72,33 @@ def test_check_cfg_types(self): try: check_cfg_types(altered_cfg, strict=False) except Exception as e: - raise type(e)(f"Error raised when checking config with unknown key and strict=False:\n" + str(e)) + raise type(e)("Error raised when checking config with unknown key and strict=False:\n" + str(e)) - def test_get_type_def(self): + def test_get_type_def(self): # noqa: D102 error_msg = \ """ Failed to generate the correct type definitions for the test objects with tuple_list_interchangeable={}. + TEST_OBJECTS_TYPES should be a dictionary with: - keys: same as TEST_OBJECTS, - values: the type definitions of the corresponding values in TEST_OBJECTS, that pass the check_types function. - Either TEST_OBJECTS_TYPES is incorrect, get_type_def is not generating the correct type definitions or test_check_types did not pass. + + Either TEST_OBJECTS_TYPES is incorrect, + get_type_def is not generating the correct type definitions or test_check_types did not pass. """ try: for key, expected_type in TEST_OBJECTS_TYPES_LIST_TUPLE_NOT_INTERCHANGEABLE.items(): obj = TEST_OBJECTS[key] type_def = get_type_def(obj, tuple_list_interchangeable=False) # Check that the generated type definition is the same as the expected type definition - self.assertTrue(check_equals_recursive(type_def, expected_type), f"\nFailed on object:\n'{key}' => {obj}\nwith generated type definition:\n{type_def}\nand expected type definition:\n{expected_type}") - # Check that the object passes the type definition - no need to assertTrue, since check_types will raise an error if it fails + self.assertTrue( + check_equals_recursive(type_def, expected_type), + f"\nFailed on object:\n'{key}' => {obj}\n" + f"with generated type definition:\n{type_def}\n" + f"and expected type definition:\n{expected_type}" + ) + # Check that the object passes the type definition + # (no need to assertTrue, since check_types will raise an error if it fails) check_types(obj, type_def, f"Object {key}") except Exception as e: raise type(e)(str(e) + error_msg.format(False)) @@ -96,19 +107,25 @@ def test_get_type_def(self): obj = TEST_OBJECTS[key] type_def = get_type_def(obj, tuple_list_interchangeable=True) # Check that the generated type definition is the same as the expected type definition - self.assertTrue(check_equals_recursive(type_def, expected_type), f"\nFailed on object:\n'{key}' => {obj}\nwith generated type definition:\n{type_def}\nand expected type definition:\n{expected_type}") - # Check that the object passes the type definition - no need to assertTrue, since check_types will raise an error if it fails + self.assertTrue( + check_equals_recursive(type_def, expected_type), + f"\nFailed on object:\n'{key}' => {obj}\n" + f"with generated type definition:\n{type_def}" + f"\nand expected type definition:\n{expected_type}" + ) + # Check that the object passes the type definition + # (no need to assertTrue, since check_types will raise an error if it fails) check_types(obj, type_def, f"Object {key}") except Exception as e: raise type(e)(str(e) + error_msg.format(True)) - def test_default_cfg(self): + def test_default_cfg(self): # noqa: D102 try: check_cfg_types(DEFAULT_CFG, strict=True) except Exception as e: - raise type(e)(f"Error raised when checking the types and keys of the default config:\n" + str(e)) + raise type(e)("Error raised when checking the types and keys of the default config:\n" + str(e)) - def test_write_read_cfg(self): + def test_write_read_cfg(self): # noqa: D102 orig_cfg = copy.deepcopy(DEFAULT_CFG) with tempfile.TemporaryDirectory() as tmpdir: invalid_cfg_file = os.path.join(tmpdir, "test.cfg") @@ -126,9 +143,15 @@ def test_write_read_cfg(self): try: read_cfg(alter_file) except Exception as e: - type(e)(f"Error raised when reading a config file with unknown key and strict=False:\n" + str(e)) - self.assertTrue(check_types(new_cfg, get_type_def(orig_cfg), "Reconstructed Config", strict=False), f"Failed to reconstruct the original config with comparable types after writing and reading.") - self.assertTrue(check_equals_recursive(orig_cfg, new_cfg), "Failed to reconstruct the values of the original config after writing and reading. Although the types are comparable, the values are not equal.") + type(e)("Error raised when reading a config file with unknown key and strict=False:\n" + str(e)) + self.assertTrue( + check_types(new_cfg, get_type_def(orig_cfg), "Reconstructed Config", strict=False), + "Failed to reconstruct the original config with comparable types after writing and reading.") + self.assertTrue( + check_equals_recursive(orig_cfg, new_cfg), + "Failed to reconstruct the values of the original config after writing and reading. " + "Although the types are comparable, the values are not equal." + ) if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/src/flat_bug/tests/test_dataset.py b/src/flat_bug/tests/test_dataset.py index 5e8633b..f9f03af 100644 --- a/src/flat_bug/tests/test_dataset.py +++ b/src/flat_bug/tests/test_dataset.py @@ -1,12 +1,11 @@ +"""Tests for the custom flatbug datasets used for training.""" +import glob import os import tempfile -import glob import unittest - from copy import deepcopy import numpy as np - from ultralytics.data import build_dataloader from ultralytics.data.utils import verify_image_label from ultralytics.utils import DEFAULT_CFG, IterableSimpleNamespace @@ -15,7 +14,6 @@ from ultralytics.utils.plotting import plot_images from flat_bug.datasets import FlatBugYOLODataset, FlatBugYOLOValidationDataset - from flat_bug.tests.remote_lfs_fallback import check_file_with_remote_fallback TEST_DIR = os.path.dirname(__file__) @@ -42,11 +40,11 @@ TEST_CFG = deepcopy(DEFAULT_CFG) setattr(TEST_CFG, "task", "segment") -def mock_verify_image_label(image_path : str, label_path : str) -> dict: +def mock_verify_image_label(image_path : str, label_path : str) -> dict: # noqa: D103 try: args = (image_path, label_path, "unit_test", False, 1, 0, 0) im_file, lb, shape, segments, keypoint, nm_f, nf_f, ne_f, nc_f, msg = verify_image_label(args) - except ValueError as e: + except ValueError: args = (image_path, label_path, "unit_test", False, 1, 0, 0, True) im_file, lb, shape, segments, keypoint, nm_f, nf_f, ne_f, nc_f, msg = verify_image_label(args) label = { @@ -59,10 +57,16 @@ def mock_verify_image_label(image_path : str, label_path : str) -> dict: "normalized": True, "bbox_format": "xywh", } - label["instances"] = Instances(np.array(label["bboxes"]), np.array(resample_segments(label["segments"])), label["keypoints"], bbox_format=label["bbox_format"], normalized=label["normalized"]) + label["instances"] = Instances( + np.array(label["bboxes"]), + np.array(resample_segments(label["segments"])), + label["keypoints"], + bbox_format=label["bbox_format"], + normalized=label["normalized"] + ) return label -def create_train_dataset(args : IterableSimpleNamespace) -> FlatBugYOLODataset: +def create_train_dataset(args : IterableSimpleNamespace) -> FlatBugYOLODataset: # noqa: D103 return FlatBugYOLODataset( data=ASSET_DATA, img_path=ASSET_DIR, @@ -79,7 +83,7 @@ def create_train_dataset(args : IterableSimpleNamespace) -> FlatBugYOLODataset: subset_args={"n" : 1, "pattern" : ASSET_NAME} ) -def create_validation_dataset(args : IterableSimpleNamespace) -> FlatBugYOLOValidationDataset: +def create_validation_dataset(args : IterableSimpleNamespace) -> FlatBugYOLOValidationDataset: # noqa: D103 return FlatBugYOLOValidationDataset( data=ASSET_DATA, img_path=ASSET_DIR, @@ -96,7 +100,7 @@ def create_validation_dataset(args : IterableSimpleNamespace) -> FlatBugYOLOVali subset_args={"n" : 1, "pattern" : ASSET_NAME} ) -def test_plot_batch(batch, ni): +def test_plot_batch(batch, ni): # noqa: D103 with tempfile.NamedTemporaryFile(delete=False, suffix=".jpg") as f: plot_images( batch["img"], @@ -109,8 +113,8 @@ def test_plot_batch(batch, ni): on_plot=os.remove, ) -class TestDataset(unittest.TestCase): - def test_train_dataset(self): +class TestDataset(unittest.TestCase): # noqa: D101 + def test_train_dataset(self): # noqa: D102 args = IterableSimpleNamespace(**TEST_CFG) if not isinstance(TEST_CFG, IterableSimpleNamespace) else TEST_CFG dataset = create_train_dataset(args) dataloader = build_dataloader(dataset, BATCH_SIZE, N_WORKERS, True, RANK) @@ -124,7 +128,7 @@ def test_train_dataset(self): except Exception as e: self.fail(f"Failed to plot training batch: {e}") - def test_validation_dataset(self): + def test_validation_dataset(self): # noqa: D102 args = IterableSimpleNamespace(**TEST_CFG) if not isinstance(TEST_CFG, IterableSimpleNamespace) else TEST_CFG dataset = create_validation_dataset(args) dataloader = build_dataloader(dataset, BATCH_SIZE, N_WORKERS, True, RANK) @@ -138,7 +142,7 @@ def test_validation_dataset(self): except Exception as e: self.fail(f"Failed to plot validation batch: {e}") - def test_verify_image_label(self): + def test_verify_image_label(self): # noqa: D102 label = mock_verify_image_label(IMAGE_ASSET, LABEL_ASSET) self.assertIsInstance(label, dict) self.assertIn("im_file", label) @@ -152,7 +156,7 @@ def test_verify_image_label(self): self.assertIn("instances", label) @classmethod - def tearDownClass(cls): + def tearDownClass(cls): # noqa: D102 # Clean caches i.e. files ending with .cache or .cache.lock in the directory of this script cache_files = glob.glob(os.path.join(TEST_DIR, "*.cache*")) for cache_file in cache_files: diff --git a/src/flat_bug/tests/test_predictor.py b/src/flat_bug/tests/test_predictor.py index 717cefb..334eafd 100644 --- a/src/flat_bug/tests/test_predictor.py +++ b/src/flat_bug/tests/test_predictor.py @@ -1,10 +1,11 @@ +"""Tests for the public flatbug Predictor class.""" import os import re import shutil import tempfile import unittest -from glob import glob from collections import defaultdict +from glob import glob import numpy as np import torch @@ -43,13 +44,16 @@ "BATCH_SIZE": 1 } -class TestTensorPredictions(unittest.TestCase): - def test_load(self): +class TestTensorPredictions(unittest.TestCase): # noqa: D101 + def test_load(self): # noqa: D102 tp = TensorPredictions() tp.load(check_file_with_remote_fallback(SERIALISED_TENSOR_PREDS)) - self.assertEqual(len(tp), N_PREDICTIONS, msg=f"Number of predictions ({len(tp)}) does not match the expected number of predictions ({N_PREDICTIONS})") + self.assertEqual( + len(tp), N_PREDICTIONS, + msg=f"Number of predictions ({len(tp)}) does not match the expected number of predictions ({N_PREDICTIONS})" + ) - def test_save(self): + def test_save(self): # noqa: D102 tp = TensorPredictions() tp = tp.load(check_file_with_remote_fallback(SERIALISED_TENSOR_PREDS)) image_path = os.path.join(os.path.dirname(__file__), "assets", f"{ASSET_NAME}.jpg") @@ -58,6 +62,7 @@ def test_save(self): tp.image_path = image_path with tempfile.TemporaryDirectory() as tmp_directory: save_dir = tp.save(tmp_directory, mask_crops=True, wait=True) + assert save_dir is not None self.assertTrue(os.path.exists(os.path.join(save_dir, "crops"))) crops = glob(os.path.join(save_dir, "crops", "*")) n_crops = len(crops) @@ -66,47 +71,56 @@ def test_save(self): # overview = glob(os.path.join(save_dir, "overview*"))[0] # shutil.move(overview, os.path.join(os.path.dirname(__file__), "assets", os.path.basename(overview))) # ################### - self.assertEqual(n_crops, N_PREDICTIONS, msg=f"Number of crops ({n_crops}) saved does not match the expected number of predictions ({N_PREDICTIONS})") + self.assertEqual( + n_crops, N_PREDICTIONS, + msg=f"Number of crops ({n_crops}) saved does not match the expected number of predictions ({N_PREDICTIONS})" + ) centroid_initial = [i.float().mean(dim=0).numpy() for i in tp.contours] - centroid_reloaded = [i.float().mean(dim=0).numpy() for i in TensorPredictions().load(glob(os.path.join(save_dir, "metadata*.json"))[0]).contours] + centroid_reloaded = [ + i.float().mean(dim=0).numpy() + for i in TensorPredictions().load(glob(os.path.join(save_dir, "metadata*.json"))[0]).contours + ] centroid_initial = np.stack(centroid_initial) centroid_reloaded = np.stack(centroid_reloaded) abs_diff = np.abs(centroid_initial - centroid_reloaded).max() - self.assertTrue(abs_diff < 0.01, msg=f"Centroid difference between initial and reloaded contours ({abs_diff}) is too large") + self.assertTrue( + abs_diff < 0.01, + msg=f"Centroid difference between initial and reloaded contours ({abs_diff}) is too large" + ) -def cast_nested(obj, new_dtype): +def cast_nested(obj, new_dtype): # noqa: D103 if not isinstance(obj, torch.Tensor): if hasattr(obj, "__iter__"): return [cast_nested(o, new_dtype) for o in obj] return obj return obj.to(new_dtype) -class DummyModel(torch.nn.Module): - def __init__(self, type : str, asset_dir : str): +class DummyModel(torch.nn.Module): # noqa: D101 + def __init__(self, type : str, asset_dir : str): # noqa: D107 if type not in ["single_scale", "pyramid"]: raise ValueError(f"Invalid type {type}") - self.type = type + self.type = type # type: ignore self.asset_dir = asset_dir self.index = 1 self.save_counter = defaultdict(lambda : 0) - def to(self, *args, **kwargs): + def to(self, *args, **kwargs): # noqa: D102 return self - def cpu(self): + def cpu(self): # noqa: D102 return self - def cuda(self, *args, **kwargs): + def cuda(self, *args, **kwargs): # noqa: D102 return self - def eval(self): + def eval(self): # noqa: D102 return self - def train(self, mode=True): + def train(self, mode=True): # noqa: D102 return self - def __call__(self, image): + def __call__(self, image): # noqa: D102 try: this_asset = os.path.join(self.asset_dir, f'{self.type}_tps_{self.index}.pt') print(f'Processing asset {this_asset}') @@ -125,7 +139,7 @@ def __call__(self, image): self.index += 1 return out - def hook_save_raw_output(self, model, label : str): + def hook_save_raw_output(self, model, label : str): # noqa: D102 ocall = model.__call__ def call_wrapped(*args, **kwargs): @@ -136,60 +150,88 @@ def call_wrapped(*args, **kwargs): model.__call__ = call_wrapped - def generate_single_scale_files(self, weights, image): + def generate_single_scale_files(self, weights, image): # noqa: D102 dtype, device = image.dtype, image.device model = Predictor(model=weights, device=device, dtype=dtype, cfg=TEST_CFG) self.hook_save_raw_output(model._model, "single_scale") model.TIME = True model.total_detection_time = 0 model.total_forward_time = 0 - output = model._detect_instances(image, scale=(model.TILE_SIZE / torch.tensor(image.shape[1:])).min().item(), max_scale=False) + output = model._detect_instances( + image, + scale=(model.TILE_SIZE / torch.tensor(image.shape[1:])).min().item(), + max_scale=False + ) # Rename the files with the pattern "assets/tps_.pt" to "assets/single_scale_tps_.pt" - [shutil.move(f, os.path.join(self.asset_dir, re.sub(r'tps_', "single_scale_tps_", f))) for f in glob(os.path.join(self.asset_dir, "tps_*.pt"))] - # Create a file with the length of the output object as a reference - this is the number of detections in the final object + [ + shutil.move(f, os.path.join(self.asset_dir, re.sub(r'tps_', "single_scale_tps_", f))) + for f in glob(os.path.join(self.asset_dir, "tps_*.pt")) + ] + # Create a file with the length of the output object as a reference + # (number of detections in the final object) with open(os.path.join(self.asset_dir, "single_scale_output_length.txt"), "w") as f: f.write(str(len(output))) - def generate_pyramid_files(self, weights, image, image_path): + def generate_pyramid_files(self, weights, image, image_path): # noqa: D102 dtype, device = image.dtype, image.device model = Predictor(model=weights, device=device, dtype=dtype, cfg=TEST_CFG) self.hook_save_raw_output(model._model, "pyramid") model.TIME = True - output = model.pyramid_predictions(image, image_path, scale_increment=1/2, scale_before=PYRAMID_SCALE_BEFORE, single_scale=False) + output = model.pyramid_predictions( + image, image_path, + scale_increment=1/2, scale_before=PYRAMID_SCALE_BEFORE, single_scale=False + ) # Rename the files with the pattern "assets/tps_.pt" to "assets/pyramid_tps_.pt" - [shutil.move(f, os.path.join(self.asset_dir, re.sub(r'tps_', "pyramid_tps_", f))) for f in glob(os.path.join(self.asset_dir, "tps_*.pt"))] - # Create a file with the length of the output object as a reference - this is the number of detections in the final object + [ + shutil.move(f, os.path.join(self.asset_dir, re.sub(r'tps_', "pyramid_tps_", f))) + for f in glob(os.path.join(self.asset_dir, "tps_*.pt")) + ] + # Create a file with the length of the output object as a reference + # (number of detections in the final object) with open(os.path.join(self.asset_dir, "pyramid_output_length.txt"), "w") as f: f.write(str(len(output))) -class TestPredictor(unittest.TestCase): +class TestPredictor(unittest.TestCase): # noqa: D101 TOLERANCE = 0.1 - def test_single_scale(self): + def test_single_scale(self): # noqa: D102 dtype = torch.float16 - predictor = Predictor(model=DummyModel("single_scale", ASSET_DIR), dtype=dtype, cfg=TEST_CFG) + predictor = Predictor(model=DummyModel("single_scale", ASSET_DIR), dtype=dtype, cfg=TEST_CFG) # type: ignore image_path = os.path.join(ASSET_DIR, ASSET_NAME + ".jpg") check_file_with_remote_fallback(image_path) image = read_image(image_path).to(torch.device("cpu"), dtype=dtype) / 255. - output = predictor._detect_instances(image, scale=(predictor.TILE_SIZE / torch.tensor(image.shape[1:])).min().item(), max_scale=False) + output = predictor._detect_instances( + image, + scale=(predictor.TILE_SIZE / torch.tensor(image.shape[1:])).min().item(), + max_scale=False + ) output_length = len(output) with open(check_file_with_remote_fallback(os.path.join(ASSET_DIR, "single_scale_output_length.txt"))) as f: reference_length = int(f.read()) # Check that the output length is within tolerance of the reference length - self.assertTrue(abs(1 - output_length/reference_length) < self.TOLERANCE, msg=f"Output length ({output_length}) does not match the reference length ({reference_length})") + self.assertTrue( + abs(1 - output_length/reference_length) < self.TOLERANCE, + msg=f"Output length ({output_length}) does not match the reference length ({reference_length})" + ) - def test_pyramid(self): + def test_pyramid(self): # noqa: D102 dtype = torch.float16 - predictor = Predictor(model=DummyModel("pyramid", ASSET_DIR), dtype=dtype, cfg=TEST_CFG) + predictor = Predictor(model=DummyModel("pyramid", ASSET_DIR), dtype=dtype, cfg=TEST_CFG) # type: ignore image_path = os.path.join(ASSET_DIR, ASSET_NAME + ".jpg") check_file_with_remote_fallback(image_path) image = read_image(image_path).to(torch.device("cpu"), dtype=dtype) / 255. - output = predictor.pyramid_predictions(image, image_path, scale_increment=1/2, scale_before=PYRAMID_SCALE_BEFORE, single_scale=False) + output = predictor.pyramid_predictions( + image, image_path, + scale_increment=1/2, scale_before=PYRAMID_SCALE_BEFORE, single_scale=False + ) output_length = len(output) with open(check_file_with_remote_fallback(os.path.join(ASSET_DIR, "pyramid_output_length.txt"))) as f: reference_length = int(f.read()) # Check that the output length is within tolerance of the reference length - self.assertTrue(abs(1 - output_length/reference_length) < self.TOLERANCE, msg=f"Output length ({output_length}) does not match the reference length ({reference_length})") + self.assertTrue( + abs(1 - output_length/reference_length) < self.TOLERANCE, + msg=f"Output length ({output_length}) does not match the reference length ({reference_length})" + ) if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/src/flat_bug/trainers.py b/src/flat_bug/trainers.py index 1598a3a..b6883f1 100644 --- a/src/flat_bug/trainers.py +++ b/src/flat_bug/trainers.py @@ -1,10 +1,13 @@ +"""Custom modified YOLO segmentation training class and associated utilities.""" import glob import json import os import random +from collections.abc import Sequence from copy import copy +from pathlib import Path from tempfile import NamedTemporaryFile -from typing import Any, Dict, List, Optional, Self, Tuple, Union +from typing import Any, overload import numpy as np import torch @@ -13,17 +16,23 @@ from ultralytics.models import yolo from ultralytics.models.yolo.segment import SegmentationTrainer from ultralytics.nn.tasks import attempt_load_one_weight -from ultralytics.utils import (DEFAULT_CFG, LOGGER, RANK, - IterableSimpleNamespace, __version__, yaml_load) +from ultralytics.utils import DEFAULT_CFG, LOGGER, RANK, IterableSimpleNamespace, yaml_load from ultralytics.utils.files import increment_path -from ultralytics.utils.torch_utils import (smart_inference_mode, - torch_distributed_zero_first) +from ultralytics.utils.torch_utils import smart_inference_mode, torch_distributed_zero_first from flat_bug import logger from flat_bug.datasets import FlatBugYOLODataset, FlatBugYOLOValidationDataset -def remove_custom_fb_args(args : Union[Dict, IterableSimpleNamespace, Any]) -> Union[Dict, IterableSimpleNamespace, Any]: +def remove_custom_fb_args(args : dict | IterableSimpleNamespace | Any) -> dict | IterableSimpleNamespace | Any: + """Remove all custom flatbug key-value pairs from a dict or namespace. + + All custom flatbug arguments must start with "_fb". + + Returns: + The dict or namespace without any custom flatbug key-value pairs. + + """ if isinstance(args, dict): for k in list(args.keys()): if k.startswith("fb_"): @@ -35,7 +44,15 @@ def remove_custom_fb_args(args : Union[Dict, IterableSimpleNamespace, Any]) -> U return args -def extract_custom_fb_args(args : Dict) -> Dict: +def extract_custom_fb_args(args : dict) -> dict: + """Extract all custom flatbug arguments from a dictionary. + + All custom flatbug arguments must start with "_fb". + + Returns: + The dictionary all, and only, custom flatbug key-value pairs. + + """ custom_fb_args = {} for k, v in args.items(): if k.startswith("fb_"): @@ -43,8 +60,13 @@ def extract_custom_fb_args(args : Dict) -> Dict: return custom_fb_args -def data2labels(data : Union[str, List[str]]) -> Union[str, List[str]]: - if hasattr(data, "__iter__") and not isinstance(data, str): +@overload +def data2labels(data : str) -> str: ... +@overload +def data2labels(data : Sequence[str]) -> list[str]: ... +def data2labels(data : str | Sequence[str]) -> str | list[str]: + """Infer label file(s) from image director[y/ies].""" + if not isinstance(data, str): return [data2labels(d) for d in data] data = data.replace("images", "labels") # Remove possible trailing directory separator @@ -52,7 +74,8 @@ def data2labels(data : Union[str, List[str]]) -> Union[str, List[str]]: data = data[:-1] return data + f'{os.sep}instances_default.json' -def get_latest_weight(weight_dir : str) -> Union[str, None]: +def get_latest_weight(weight_dir : str | Path) -> str | None: + """Get the most recently updated weights (files ending in ".pt") in a directory.""" weights = glob.glob(f"{weight_dir}{os.sep}*.pt") if not weights: logger.warning(f"No weights found in {weight_dir}") @@ -65,32 +88,49 @@ def _custom_end_to_end_validation(self : "FlatBugSegmentationTrainer"): self._do_custom_eval = False # Get image and label paths train_data, val_data = self.data["train"], self.data["val"] - train_labels, val_labels = data2labels(train_data), data2labels(val_data) - train_paths, val_paths = self.training_image_paths, self.val_image_paths + train_labels, val_labels = data2labels(train_data), data2labels(val_data) # noqa: F841 + train_paths, val_paths = self.training_image_paths, self.val_image_paths # noqa: F841 if self._custom_num_images > -1 and self._custom_num_images < len(val_paths): # Sample n images # train_paths = random.sample(train_paths, self._custom_num_images) val_paths = random.sample(val_paths, min(len(val_paths), self._custom_num_images)) - escape_dots = lambda f: os.path.basename(f).replace(".", r"\.") - val_pattern = f'({"|".join([escape_dots(f) for f in val_paths])})' + val_pattern = '({})'.format( + "|".join([os.path.basename(f).replace(".", r"\.") for f in val_paths]) + ) # Get latest model path weight_dir = self.wdir latest_weights = get_latest_weight(weight_dir) # Construct end-to-end evaluation command custom_eval_path = os.path.join(os.path.dirname(__file__), "..", "..", "scripts", "eval", "end_to_end_eval.sh") - command = f'bash "{custom_eval_path}" -w "{latest_weights}" -d "{val_data}" -l "{val_labels}" -o "{self.save_dir}{os.sep}e2e_val{os.sep}{self.epoch}" -g "{self.args.device}" -p "{val_pattern}"' + # command = ( + # f'bash "{custom_eval_path}" -w "{latest_weights}" -d "{val_data}" -l "{val_labels}" ' + # f'-o "{self.save_dir}{os.sep}e2e_val{os.sep}{self.epoch}" -g "{self.args.device}" -p "{val_pattern}"' + # ) + command = 'bash "{}" -w "{}" -d "{}" -l "{}" -o "{}" -g "{}" -p "{}"'.format( + custom_eval_path, latest_weights, val_data, val_labels, + f"{self.save_dir}{os.sep}e2e_val{os.sep}{self.epochs}", + self.args.device, val_pattern + ) logger.debug(f"Running custom end-to-end validation command: `{command}`") # Run command os.system(command) def findattr(o, name : str, filters : list=[lambda _ : True], exclude_prefix="_", label : str="object"): + """Recursively extract certain attributes of an object or object's nested within the object's state. + + TODO: This is very brittle and could easily result in a recursion loop and other errors. + + Returns: + A dictionary with the attribute access "paths" as keys and the values as values. + + """ isd = isinstance(o, dict) if isd: attrs = list(o.keys()) else: try: attrs = list(o.__dict__.keys()) - except: + except Exception: return {} values = {} for attr in attrs: @@ -104,14 +144,23 @@ def findattr(o, name : str, filters : list=[lambda _ : True], exclude_prefix="_" values.update(findattr(val, name, filters=filters, exclude_prefix=exclude_prefix, label=new_label)) return values -def replaceattr(o, name : str, value, filters : list=[lambda _ : True], exclude_prefix="_", label : str="object"): +def replaceattr(o : object, name : str, value, filters : list=[lambda _ : True], exclude_prefix="_", label : str="object"): + """Recursively replace certain attributes of an object or object's nested within the object's state. + + TODO: This is very brittle and could easily result in a recursion loop and other errors. + + Returns: + None if successful, otherwise False. + *(TODO: It is probably unexpected for most developers to use None as a success state.)* + + """ isd = isinstance(o, dict) if isd: attrs = list(o.keys()) else: try: attrs = list(o.__dict__.keys()) - except: + except Exception: return False for attr in attrs: if (isinstance(attr, str) and attr.startswith(exclude_prefix)): @@ -125,15 +174,22 @@ def replaceattr(o, name : str, value, filters : list=[lambda _ : True], exclude_ else: setattr(o, attr, value) else: - replaceattr(o.get(attr) if isd else getattr(o, attr), name, value, filters=filters, exclude_prefix=exclude_prefix, label=new_label) + replaceattr( + o.get(attr) if isd else getattr(o, attr), + name, value, + filters=filters, exclude_prefix=exclude_prefix, label=new_label + ) -def apply_overrides_to_checkpoint(overrides): +def apply_overrides_to_checkpoint(overrides): # noqa: D103 if not overrides.get("resume", False): return resume_model = overrides["resume"] _, ckpt_ext = os.path.splitext(resume_model) if not isinstance(resume_model, str): - raise NotImplementedError(f"`flat-bug` currently onyl supports resuming training from a file. Please specify resume=.pt instead of resume={resume_model}") + raise NotImplementedError( + "`flat-bug` currently onyl supports resuming training from a file. " + f"Please specify resume=.pt instead of resume={resume_model}" + ) if not os.path.exists(resume_model): raise FileNotFoundError(f"Resume checkpoint {resume_model} not found.") # Load original checkpoint @@ -177,36 +233,41 @@ def apply_overrides_to_checkpoint(overrides): return overrides class FlatBugSegmentationTrainer(SegmentationTrainer): + """Modified YOLO Segmentation trainer used for training flatbug.""" + def __init__( self, cfg : IterableSimpleNamespace=DEFAULT_CFG, - overrides : Dict=None, + overrides : dict | None=None, _callbacks : Any=None, *args, **kwargs ): """Initialize a SegmentationTrainer object with given arguments.""" cfg = DEFAULT_CFG # In DDP mode, a CFG is created for each rank, but we always want the default one - custom_fb_args = extract_custom_fb_args(overrides) + custom_fb_args = extract_custom_fb_args(overrides or {}) self._max_instances = custom_fb_args["fb_max_instances"] self._max_images = custom_fb_args["fb_max_images"] self._exclude_datasets = custom_fb_args["fb_exclude_datasets"] self.custom_eval = custom_fb_args["fb_custom_eval"] self._do_custom_eval = False # This is a dynamic signalling flag, not a hyperparameter self._custom_num_images = custom_fb_args["fb_custom_eval_num_images"] - assert self._custom_num_images != 0, 'fb_custom_eval_num_images/custom_eval_num_images cannot be 0. If you mean to disable custom eval set fb_custom_eval/custom_eval=False.' + assert self._custom_num_images != 0, ( + 'fb_custom_eval_num_images/custom_eval_num_images cannot be 0. ' + 'If you mean to disable custom eval set fb_custom_eval/custom_eval=False.' + ) assert self._max_instances != 0, 'fb_max_instances/max_instances cannot be 0.' assert self._max_images != 0, "fb_max_images/max_images cannot be 0." - overrides = remove_custom_fb_args(overrides) # The custom arguments must be removed before calling super.__init___ + updated_overrides = remove_custom_fb_args(overrides or {}) # The custom arguments must be removed before calling super.__init___ # To use overrides we must apply these to the checkpoint file itself (only applies if we resume a training run) # otherwise the overrides are overwritten by the old training arguments stored within the checkpoint file - apply_overrides_to_checkpoint(overrides) + apply_overrides_to_checkpoint(updated_overrides) super().__init__(cfg, overrides, _callbacks, *args, **kwargs) - if overrides.get("resume", False): - self.args.__dict__.update(overrides) + if updated_overrides.get("resume", False): + self.args.__dict__.update(updated_overrides) self.args.__dict__.update(custom_fb_args) # But we need to add them back, otherwise they will be missing in DDP mode - if overrides.get("resume", False): + if updated_overrides.get("resume", False): self.args.resume = True self.add_callback("on_train_epoch_start", FlatBugSegmentationTrainer.log_lr) # self.use_ewa_sampler() @@ -222,11 +283,10 @@ def __init__( # Reproducibility self._reproducibility_setup() - @staticmethod - def log_lr(self): - LOGGER.info(f"LR: {self.scheduler.get_last_lr()}") + def log_lr(self): # noqa: D102 + LOGGER.info(f"LR: {self.scheduler.get_last_lr() if self.scheduler is not None else 'NaN'}") - def setup_model(self : Self) -> Optional[Dict]: + def setup_model(self) -> dict | None: # noqa: D102 if isinstance(self.model, torch.nn.Module): # if model is loaded beforehand. No setup needed return @@ -246,16 +306,19 @@ def setup_model(self : Self) -> Optional[Dict]: return ckpt @property - def exclude_pattern(self : Self) -> str: + def exclude_pattern(self) -> str: # noqa: D102 return f'^(?!({"|".join(self._exclude_datasets)}))' if self._exclude_datasets else "" - def build_dataset( - self : Self, + def build_dataset( # noqa: D102 + self, img_path : str, mode : str='train', - batch : Optional[int]=None - ) -> Union[FlatBugYOLODataset, FlatBugYOLOValidationDataset]: - LOGGER.info(f"Building dataset with max instances ({self._max_instances}), max images ({self._max_images}) and exclude pattern ({self.exclude_pattern}).") + batch : int | None=None + ) -> FlatBugYOLODataset | FlatBugYOLOValidationDataset: + LOGGER.info( + f"Building dataset with max instances ({self._max_instances}), " + f"max images ({self._max_images}) and exclude pattern ({self.exclude_pattern})." + ) if mode == "train": dataset = FlatBugYOLODataset( data=yaml_load(self.args.data), @@ -294,9 +357,9 @@ def build_dataset( return dataset def get_dataloader( - self : Self, + self, dataset_path : str, - batch_size : Optional[int]=16, + batch_size : int | None=16, rank : int=0, mode : str="train" ) -> InfiniteDataLoader: @@ -312,9 +375,14 @@ def get_dataloader( return build_dataloader(dataset, batch_size, workers, shuffle, rank) # return dataloader @smart_inference_mode() - def validate(self : Self) -> Tuple[Dict, float]: - """ - Runs validation on test set using self.validator. The returned dict is expected to contain "fitness" key. + def validate(self) -> tuple[dict, float]: + """Run validation on test set using self.validator. + + The returned dict is expected to contain "fitness" key. + + Returns: + Validation metrics and the fitness if available (if not `fitness=float("nan")`). + """ if self.epoch % self.save_period == 0 or self._val_metrics is None: metrics, fitness = super().validate() @@ -326,34 +394,53 @@ def validate(self : Self) -> Tuple[Dict, float]: else: metrics, fitness = self._val_metrics, self._val_fitness LOGGER.info(f"Skipped validation at epoch {self.epoch}, using old values") + if fitness is None: + fitness = float("nan") + if not isinstance(fitness, float): + try: + fitness = float(fitness) + except Exception: + fitness = float("nan") return metrics, fitness @property - def training_image_paths(self : Self) -> List[str]: + def training_image_paths(self) -> list[str]: # noqa: D102 try: return self.train_loader.dataset.im_files except Exception as e: - logger.error("Perhaps the trainer has not been activated yet. Accessing the training image paths is not possible, while training has not started.") + logger.error( + "Perhaps the trainer has not been activated yet. " + "Accessing the training image paths is not possible, while training has not started." + ) raise e @property - def val_image_paths(self : Self) -> List[str]: + def val_image_paths(self) -> list[str]: # noqa: D102 try: return self.test_loader.dataset.im_files except Exception as e: - logger.error("Perhaps the trainer has not been activated yet. Accessing the validation image paths is not possible, while training has not started.") + logger.error( + "Perhaps the trainer has not been activated yet. " + "Accessing the validation image paths is not possible, while training has not started." + ) raise e - def _reproducibility_setup(self : Self): - if not RANK in {-1, 0}: + def _reproducibility_setup(self): + if RANK not in {-1, 0}: logger.warning("Reproducibility setup skipped for non-master rank.") return def log_data(self): with open(self.save_dir / "data_log.json", "w") as f: - json.dump({**{k : str(v) for k, v in self.data.items()}, **{"train_images" : self.training_image_paths, "val_images" : self.val_image_paths}}, f) + json.dump( + obj={ + **{k : str(v) for k, v in self.data.items()}, + **{"train_images" : self.training_image_paths, "val_images" : self.val_image_paths} + }, + fp=f + ) self.add_callback("on_train_start", log_data) - def get_validator(self : Self) -> yolo.segment.SegmentationValidator: + def get_validator(self) -> yolo.segment.SegmentationValidator: """Return an instance of SegmentationValidator for validation of YOLO model.""" self.loss_names = "box_loss", "seg_loss", "cls_loss", "dfl_loss" return yolo.segment.SegmentationValidator( diff --git a/src/flat_bug/yolo_helpers.py b/src/flat_bug/yolo_helpers.py index 74ee62e..cfc4cfc 100644 --- a/src/flat_bug/yolo_helpers.py +++ b/src/flat_bug/yolo_helpers.py @@ -1,46 +1,51 @@ +"""Utilities for working with raw and processed YOLO outputs.""" from argparse import Namespace -from typing import List, Optional, Tuple, Union +from collections.abc import Callable, Sequence +from functools import partial +from typing import Concatenate, TypeVar import numpy as np import torch import torch.nn.functional as F from ultralytics.engine.results import Masks, Results -from flat_bug.geometric import find_contours, resize_mask +from flat_bug.geometric import find_contours, resize_masks from flat_bug.nms import fancy_nms, get_overlap_fn, nms_boxes, nms_masks +class Polygons(list[torch.Tensor]): + """Convenience wrapper for a heterogenous list of torch.Tensor's used for storing polygon contours.""" + + def __getattr__(self, attr : str): + inner = getattr(torch.Tensor, attr) + return partial(self._apply, func=inner) + + def _apply(self, func : Callable[Concatenate[torch.Tensor, ...], torch.Tensor], *args, **kwargs): + return Polygons(func(x, *args, **kwargs) for x in self) + + def to_list(self): # noqa: D102 + return [e for e in self] + class ResultsWithTiles(Results): - def __init__(self, tiles : List[int]=None, polygons=None, *args, **kwargs): + """Container for YOLO results with segmentation polygons and corresponding tile indices.""" + + def __init__(self, tiles : list[int] | torch.Tensor, polygons : list[torch.Tensor], *args, **kwargs): # noqa: D107 super().__init__(*args, **kwargs) - self.tiles = tiles - self.polygons = polygons - - def new(self) -> 'ResultsWithTiles': - new = super().new() - new.tiles = self.tiles - new.polygons = self.polygons - return new - - def __getitem__(self, idx : Union[int, slice, List[int], Tuple[int], torch.Tensor]) -> 'ResultsWithTiles': - new = super().__getitem__(idx) - new.tiles = self.tiles[idx] - if isinstance(idx, int) or isinstance(idx, slice): - new.polygons = self.polygons[idx] - elif isinstance(idx, torch.Tensor) and not idx.dtype == torch.bool or isinstance(idx, list) or isinstance(idx, tuple): - new.polygons = [self.polygons[i] for i in idx] - elif isinstance(idx, torch.Tensor) and idx.dtype == torch.bool: - new.polygons = [self.polygons[i] for i in torch.where(idx)[0]] - else: - raise TypeError(f"idx must be an int, slice, list, tuple or torch.Tensor, not {type(idx)}") - return new + self.tiles = torch.as_tensor(tiles) + self.polygons = Polygons(polygons) + self._keys = tuple((*self._keys, "polygons", "tiles")) + + def __len__(self): + return super().__len__() or 0 + def offset_box( boxes : torch.Tensor, offset : torch.Tensor, - max_x : Optional[Union[int, float]] = None, - max_y : Optional[Union[int, float]] = None - ) -> torch.Tensor: + max_x : int | float | None = None, + max_y : int | float | None = None + ): + """Move bounding boxes.""" m = 4 / offset.shape[0] assert m // 1 == m, f"4 must be divisible by the number of offsets ({offset.shape[0]})" boxes[:, :4] += offset.unsqueeze(0).repeat(1, int(m)) @@ -53,21 +58,23 @@ def offset_box( def offset_mask( mask : torch.Tensor, offset : torch.Tensor, - new_shape : Optional[Union[Tuple[int, int], List[int]]]=None, + new_shape : tuple[int, int] | list[int] | None=None, max_size=700 - ) -> torch.Tensor: + ): + """Offset (move) binary masks in a new coordinate system.""" # Due to memory use, it is beneficial to restrict the maximum size of the masks. A 700x700 boolean tensor uses ~0.5 MB of memory n, h, w = mask.shape - if new_shape is not None: #isinstance(new_shape, tuple) or isinstance(new_shape, list) or isinstance(new_shape, torch.Tensor) and len(new_shape.shape) == 2: + if new_shape is None: + shape = n, h, w + else: assert len(new_shape) == 2, f"new_shape must be a tuple or list of length 2, not {len(new_shape)}" - new_shape = int(n), int(new_shape[0]), int(new_shape[1]) - elif new_shape is None: - raise ValueError("new_shape must be specified") - new_mask = torch.zeros(new_shape, dtype=torch.bool, device=mask.device) + shape = int(n), int(new_shape[0]), int(new_shape[1]) + + new_mask = torch.zeros(shape, dtype=torch.bool, device=mask.device) # Calculate the possible clamped size of the mask (if it needs to be clamped) - clamp_factor = (max(new_shape[1:]) / max_size) if max_size is not None else 1 - clamp_shape = [int(n), new_shape[1] / clamp_factor, new_shape[2] / clamp_factor] + clamp_factor = (max(shape[1:]) / max_size) if max_size is not None else 1 + clamp_shape = [int(n), shape[1] / clamp_factor, shape[2] / clamp_factor] # And ensure that both direction are rounded in the same direction (down or up) clamp_delta = round(sum([c % 1 for c in clamp_shape[1:]]) / 2) clamp_shape[1:] = [int(c) + clamp_delta for c in clamp_shape[1:]] @@ -79,34 +86,45 @@ def offset_mask( return torch.zeros(clamp_shape, dtype=torch.bool, device=mask.device) # Calculate the overlap of the mask with the new mask (in the new mask's coordinate system) - mask_overlap = [None, None] - for i, (mask_d, new_mask_d, offset_d) in enumerate(zip([h, w], new_shape[1:], offset)): + mask_overlap = [torch.empty((0, )), torch.empty((0, ))] + for i, (mask_d, new_mask_d, offset_d) in enumerate(zip([h, w], shape[1:], offset)): mask_overlap[i] = torch.arange(mask_d, device=mask.device) + offset_d mask_overlap[i] = mask_overlap[i][(mask_overlap[i] < new_mask_d) & (mask_overlap[i] >= 0)] mask_overlap[i] = mask_overlap[i][torch.tensor([0, -1], device=mask.device, dtype=torch.long)] # Insert the overlapping part of the old mask into the overlapping section of the new mask - new_mask[:, mask_overlap[0][0]:mask_overlap[0][1], mask_overlap[1][0]:mask_overlap[1][1]] = mask[:, (mask_overlap[0][0] - offset[0]):(mask_overlap[0][1] - offset[0]), (mask_overlap[1][0] - offset[1]):(mask_overlap[1][1] - offset[1])] + new_mask[ + :, + mask_overlap[0][0]:mask_overlap[0][1], + mask_overlap[1][0]:mask_overlap[1][1] + ] = mask[ + :, + (mask_overlap[0][0] - offset[0]):(mask_overlap[0][1] - offset[0]), + (mask_overlap[1][0] - offset[1]):(mask_overlap[1][1] - offset[1]) + ] # If the mask is larger than the maximum size, clamp it by downscaling it such that the largest dimension is max_size if clamp_factor > 1: - new_mask = resize_mask(new_mask, clamp_shape[1:]) # F.interpolate(new_mask.float().unsqueeze(0), clamp_shape[1:], mode='bilinear', align_corners=False, antialias=True).squeeze(0) > 0.25 + # F.interpolate( + # new_mask.float().unsqueeze(0), clamp_shape[1:], + # mode='bilinear', align_corners=False, antialias=True + # ).squeeze(0) > 0.25 + new_mask = resize_masks(new_mask, clamp_shape[1:]) return new_mask def merge_tile_results( - results = List[Results], - orig_img : Optional["torch.Tensor"]=None, - box_offsetters : Optional["torch.Tensor"]=None, - mask_offsetters : Optional["torch.Tensor"]=None, - new_shape : Union[Tuple[int, int], List[int]]=None, - clamp_boxes : Union[Tuple[int, int], List[int]]=(None, None), + results : list[Results], + orig_img : torch.Tensor | None=None, + box_offsetters : torch.Tensor | None=None, + mask_offsetters : torch.Tensor | None=None, + new_shape : tuple[int, int] | list[int] | None=None, + clamp_boxes : tuple[int | None, int | None] | list[int | None]=(None, None), max_mask_size : int =700, exclude_masks : bool=False - ) -> "ResultsWithTiles": - """ - Merges results from multiple images into a single Results object, possibly with a new image. - """ + ) : + """Merge results from multiple images into a single Results object, possibly with a new image.""" + assert isinstance(results[0].boxes, torch.Tensor) _device = results[0].boxes.data.device if orig_img is None: orig_img = results[0].orig_img @@ -120,80 +138,104 @@ def merge_tile_results( mx, my = clamp_boxes path = results[0].path names = results[0].names - tile_indices = torch.concatenate([torch.tensor([i] * len(r), dtype=torch.long, device=_device) for i, r in enumerate(results)]) - boxes = torch.cat([offset_box(r.boxes.data, o.flip(0), mx, my) for r, o in zip(results, box_offsetters)]) - polygons = [find_contours(resize_mask(mask, [256 * 3, 256 * 3]), True) * (1024 / 256) / 3 + o.flip(0).unsqueeze(0) for r, o in zip(results, box_offsetters) for mask in r.masks.data] + tile_indices = torch.concatenate([ + torch.tensor([i] * (r.__len__() or 0), dtype=torch.long, device=_device) + for i, r in enumerate(results) + ]) + boxes = torch.cat([ + offset_box(torch.as_tensor(r.boxes.data), o.flip(0), mx, my) + for r, o in zip(results, box_offsetters) if r.boxes is not None + ]) + polygons = [ + find_contours(resize_masks(mask, [256 * 3, 256 * 3]), True) * (1024 / 256) / 3 + o.flip(0).unsqueeze(0) + for r, o in zip(results, box_offsetters) if r.masks + for mask in torch.as_tensor(r.masks.data) + ] if exclude_masks: - masks = torch.cat([r.masks.data for r in results]) + masks = torch.cat([torch.as_tensor(r.masks.data) for r in results if r.masks is not None]) else: - masks = torch.cat([offset_mask(r.masks.data, o, new_shape, max_mask_size) for r, o in zip(results, mask_offsetters)]) + masks = torch.cat([ + offset_mask(torch.as_tensor(r.masks.data), o, new_shape, max_mask_size) + for r, o in zip(results, mask_offsetters) if r.masks is not None + ]) if len(masks.shape) == 2: masks = masks.unsqueeze(0) if not all([r.probs is None for r in results]): raise NotImplementedError("'Probs' not implemented yet") if not all([r.keypoints is None for r in results]): raise NotImplementedError("'Keypoints' not implemented yet") - return ResultsWithTiles(tiles=tile_indices, orig_img=Namespace(shape=orig_img.shape), path=path, names=names, boxes=boxes, masks=masks, polygons=polygons, probs=None, keypoints=None) + return ResultsWithTiles( + tiles=tile_indices, + orig_img=Namespace(shape=orig_img.shape), + path=path, + names=names, + boxes=boxes, + masks=masks, + polygons=polygons, + probs=None, + keypoints=None + ) def stack_masks( - masks : List["Masks"], - orig_shape : Optional[Union[Tuple[int, int], List[int]]]=None - ) -> "Masks": - """ - Stacks a list of ultralytics.engine.results.Masks objects (or torch.Tensor) into a single ultralytics.engine.results.Masks object. + masks : list[Masks | torch.Tensor], + orig_shape : tuple[int, int] | list[int] | None=None + ): + """Stacks a list of ultralytics.engine.results.Masks objects (or torch.Tensor) into a single ultralytics.engine.results.Masks object. If the masks are not all the same size, they are resized to the largest size in the list. Args: - masks (`list`): A list of ultralytics.engine.results.Masks objects (or torch.Tensor). - orig_shape (`tuple`, optional): The original shape of the image. Defaults to None. If None, the original shape is inferred from the first Masks object in the list if there is one, otherwise the original shape None. - antialias (`bool`, optional): A flag to indicate whether to use antialiasing when resizing the masks. Defaults to False. + masks: A list of ultralytics.engine.results.Masks objects (or torch.Tensor). + orig_shape: The original shape of the image. Defaults to None. + If None, the original shape is inferred from the first Masks object in the list if there is one. + antialias: A flag to indicate whether to use antialiasing when resizing the masks. Defaults to False. Returns: - out (`ultralytics.engine.results.Masks`): A Masks object containing the stacked masks. + A Masks object containing the stacked masks. + """ assert isinstance(masks, list), f"'masks' must be a list, not {type(masks)}" for m in masks: if isinstance(m, Masks): orig_shape = m.orig_shape break - masks = [m.data if isinstance(m, Masks) else m for m in masks] - assert all([isinstance(m, torch.Tensor) for m in masks]), f"'masks' must be a list of torch.Tensor, not {type(masks[0])}" - assert len(masks) != 0, f"'masks' ({masks}) must not be empty" - _device = masks[0].device + tensor_masks = [torch.as_tensor(m.data) if isinstance(m, Masks) else m for m in masks] + assert all([isinstance(m, torch.Tensor) for m in tensor_masks]), f"'masks' must be a list of torch.Tensor, not {type(tensor_masks[0])}" + assert len(tensor_masks) != 0, f"'masks' ({tensor_masks}) must not be empty" + _device = tensor_masks[0].device - max_h = max([m.shape[1] for m in masks]) - max_w = max([m.shape[2] for m in masks]) - masks_in_each = [len(m) for m in masks] + max_h = max([m.shape[1] for m in tensor_masks]) + max_w = max([m.shape[2] for m in tensor_masks]) + masks_in_each = [len(m) for m in tensor_masks] new_masks = torch.zeros((sum(masks_in_each), max_h, max_w), dtype=torch.bool, device=_device) i = 0 - for n, m in zip(masks_in_each, masks): + for n, m in zip(masks_in_each, tensor_masks): if n == 0: continue for j in range(n): if m[[j]].shape[1] == max_h and m[[j]].shape[2] == max_w: new_masks[i + j] = m[[j]] else: - new_masks[i + j] = resize_mask(m[[j]], (max_h, max_w)) + new_masks[i + j] = resize_masks(m[[j]], (max_h, max_w)) i += n return Masks(new_masks, orig_shape=orig_shape) -def crop_mask( - masks : "torch.Tensor", - boxes : "torch.Tensor" - ) -> "torch.Tensor": - """ - It takes a mask and a bounding box, and returns a mask that is cropped to the bounding box. +def crop_masks( + masks : torch.Tensor, + boxes : torch.Tensor + ): + """Crops masks to bounding boxes. Args: - masks (`torch.Tensor`): [n, h, w] tensor of masks - boxes (`torch.Tensor`): [n, 4] tensor of bbox coordinates in relative point form + masks: [n, h, w] tensor of masks + boxes: [n, 4] tensor of bbox coordinates in relative point form Returns: - out (`torch.Tensor`): The masks are being cropped to the bounding box. + masks: The masks cropped to the bounding box. + """ n, h, w = masks.shape x1, y1, x2, y2 = torch.chunk(boxes[:, :, None], 4, 1) # x1 shape(n,1,1) @@ -203,29 +245,33 @@ def crop_mask( return masks * ((r >= x1) * (r < x2) * (c >= y1) * (c < y2)) def process_mask( - protos : "torch.Tensor", - masks_in : "torch.Tensor", - bboxes : "torch.Tensor", - shape : Union[Tuple[int, int], List[int]], + protos : torch.Tensor, + masks_in : torch.Tensor, + bboxes : torch.Tensor, + shape : tuple[int, int] | tuple[int, ...] | list[int], upsample : bool=False - ) -> "torch.Tensor": - """ - Apply masks to bounding boxes using the output of the mask head. + ): + """Apply masks to bounding boxes using the output of the mask head. Args: - protos (`torch.Tensor`): A tensor of shape [mask_dim, mask_h, mask_w]. - masks_in (`torch.Tensor`): A tensor of shape [n, mask_dim], where n is the number of masks after NMS. - bboxes (`torch.Tensor`): A tensor of shape [n, 4], where n is the number of masks after NMS. - shape (`tuple`): A tuple of integers representing the size of the input image in the format (h, w). - upsample (`bool`, optional): A flag to indicate whether to upsample the mask to the original image size. Default is False. + protos: A tensor of shape [mask_dim, mask_h, mask_w]. + masks_in: A tensor of shape [n, mask_dim], where n is the number of masks after NMS. + bboxes: A tensor of shape [n, 4], where n is the number of masks after NMS. + shape: A tuple of integers representing the size of the input image in the format (h, w). + upsample: A flag to indicate whether to upsample the mask to the original image size. Default is False. Returns: - out (`torch.Tensor`): A binary mask tensor of shape [n, h, w], where n is the number of masks after NMS, and h and w + A binary mask tensor of shape [n, h, w], where n is the number of masks after NMS, and h and w are the height and width of the input image. The mask is applied to the bounding boxes. + """ c, mh, mw = protos.shape # CHW ih, iw = shape - masks = (masks_in.to(protos.dtype) @ protos.view(c, -1)).sigmoid().view(-1, mh, mw) # CHW <- This line has been changed from the original implementation, which had a superfluous type conversion which caused YOLOv8 to cast the masks to float32, this change simply removes the type conversion enabling support for other data types + + # CHW <- This line has been changed from the original implementation, + # which had a superfluous type conversion which caused YOLOv8 to cast the masks to float32, + # this change simply removes the type conversion enabling support for other data types + masks = (masks_in.to(protos.dtype) @ protos.view(c, -1)).sigmoid().view(-1, mh, mw) downsampled_bboxes = bboxes.clone() downsampled_bboxes[:, 0] *= mw / iw @@ -233,7 +279,7 @@ def process_mask( downsampled_bboxes[:, 3] *= mh / ih downsampled_bboxes[:, 1] *= mh / ih - masks = crop_mask(masks, downsampled_bboxes) # CHW + masks = crop_masks(masks, downsampled_bboxes) # CHW # masks = expand_bottom_right(masks) # HW @@ -245,33 +291,44 @@ def process_mask( return masks def expand_bottom_right(mask : torch.Tensor): - """ - Add an extra pixel above next to bottom/right edges of the region of 1s. + """Add an extra pixel above next to bottom/right edges of the region of 1s. Args: - mask (`torch.Tensor`): A binary mask tensor of shape [h, w]. + mask: A binary mask tensor of shape [h, w]. Returns: - out (`torch.Tensor`): A binary mask tensor of shape [h, w], where an extra pixel is added above next to left/top edges of the region of 1s. + A binary mask tensor of shape [h, w], where an extra pixel is added above next to left/top edges of the region of 1s. + """ - bottom_right_kernel = torch.tensor([[-1, -1, -1], [-1, -1, 1], [-1, 1, 1]], dtype=torch.float16, device=mask.device).t() - bottom_right = F.conv2d(mask.to(torch.float16).unsqueeze(1), bottom_right_kernel.unsqueeze(0).unsqueeze(0), padding=1).squeeze(1).clamp(0) + bottom_right_kernel = torch.tensor( + [ + [-1, -1, -1], + [-1, -1, 1], + [-1, 1, 1] + ], dtype=torch.float16, device=mask.device + ).t() + bottom_right = F.conv2d( + mask.to(torch.float16).unsqueeze(1), + bottom_right_kernel.unsqueeze(0).unsqueeze(0), + padding=1 + ).squeeze(1).clamp(0) return mask + bottom_right ## These are taken from ultralytics to avoid unnecessary dependencies +V = TypeVar("V", bound=torch.Tensor | np.ndarray) def clip_boxes( - boxes : Union[torch.Tensor, np.ndarray], - shape : Tuple[int, int] - ): - """ - Takes a list of bounding boxes and a shape (height, width) and clips the bounding boxes to the shape. + boxes : V, + shape : tuple[int, int] + ) -> V: + """Clips bounding boxes to a specified shape (height, width). Args: - boxes (`Union[torch.Tensor, np.ndarray]`): the bounding boxes to clip - shape (`tuple`): The maximum x and y values for the bounding boxes. + boxes: the bounding boxes to clip + shape: The maximum x and y values for the bounding boxes. Returns: - out (`Union[torch.Tensor, np.ndarray]`): Clipped boxes + Clipped boxes + """ if isinstance(boxes, torch.Tensor): # faster individually (WARNING: inplace .clamp_() Apple MPS bug) boxes[..., 0] = boxes[..., 0].clamp(0, shape[1]) # x1 @@ -284,29 +341,32 @@ def clip_boxes( return boxes def scale_boxes( - img1_shape : Tuple[int, int], + img1_shape : tuple[int, int], boxes : torch.Tensor, - img0_shape : Tuple[int, int], + img0_shape : tuple[int, int], ratio_pad=None, padding : bool=True, xywh : bool=False - ) -> torch.Tensor: - """ - Rescales bounding boxes (in the format of xyxy by default) from the shape of the image they were originally - specified in (img1_shape) to the shape of a different image (img0_shape). + ): + """Rescales bounding boxes. + + Bounding boxes are assumed to be in the "xyxy" format by default. + Reshapes bounding boxes from the shape of the image they were originally + specified in (img1_shape) to the shape of a different image (img0_shape). Args: - img1_shape (`tuple`): The shape of the image that the bounding boxes are for, in the format of (height, width). - boxes (`torch.Tensor`): the bounding boxes of the objects in the image, in the format of (x1, y1, x2, y2) - img0_shape (`tuple`): the shape of the target image, in the format of (height, width). - ratio_pad (`Optional[Tuple[float, Tuple[int, int]]]`, optional): a tuple of (ratio, pad) for scaling the boxes. If None, the ratio and pad will be + img1_shape: The shape of the image that the bounding boxes are for, in the format of (height, width). + boxes: the bounding boxes of the objects in the image, in the format of (x1, y1, x2, y2) + img0_shape: the shape of the target image, in the format of (height, width). + ratio_pad: a tuple of (ratio, pad) for scaling the boxes. If None, the ratio and pad will be calculated based on the size difference between the two images. Defaults to None. - padding (`bool`, optional): If True, assuming the boxes is based on image augmented by yolo style. If False then do regular - rescaling. Defaults to True. - xywh (`bool`, optional): The box format is xywh or not. Defaults to False. + padding: If True, assuming the boxes is based on image augmented by yolo style. + If False then do regular rescaling. Defaults to True. + xywh: The box format is xywh or not. Defaults to False. Returns: - boxes (`torch.Tensor`): The scaled bounding boxes, in the format of (x1, y1, x2, y2) + The scaled bounding boxes, in the format of (x1, y1, x2, y2). + """ if ratio_pad is None: # calculate from img0_shape gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1]) # gain = old / new @@ -330,34 +390,36 @@ def scale_boxes( # Revised from ultralytics def postprocess( preds, - imgs : List[torch.Tensor], + imgs : Sequence[torch.Tensor] | torch.Tensor, max_det : int=300, min_confidence : float=0, overlap_threshold : float=0.1, overlap_metric : str="IoU", nms : int=0, - valid_size_range : Optional[Union[Tuple[int, int], List[int]]]=None, - edge_margin : Optional[int]=None - ) -> List[Results]: - """ - Postprocesses the predictions of the model. + valid_size_range : tuple[int, int] | list[int] | None=None, + edge_margin : int | None=None + ) -> list[Results]: + """Postprocesses the predictions of the model. Args: - preds (`list`): A list of predictions from the model. - imgs (`List[torch.Tensor]`): A list of images that were passed to the model. - max_det (`int`, optional): The maximum number of detections to return. Defaults to 300. - min_confidence (`float`, optional): The minimum confidence of the predictions to return. Defaults to 0. - overlap_threshold (`float`, optional): The overlap (e.g. IoU) threshold for non-maximum suppression. Defaults to 0.1. - overlap_metric (`str`): Overlap metric to use for NMS. Default is "IoU". - nms (`int`, optional): The type of non-maximum suppression to use. Defaults to 0. 0 is no NMS, 1 is standard NMS, 2 is fancy NMS and 3 is mask NMS. - valid_size_range (`tuple`, optional): The range of valid sizes for the bounding boxes in pixels. Defaults to None (no valid size range). - edge_margin (`int`, optional): The minimum gap between the edge of the image and the bounding box in pixels for a prediction to be considered valid. Defaults to None (no edge margin). + preds: A list of predictions from the model. + imgs: A list of images that were passed to the model. + max_det: The maximum number of detections to return. Defaults to 300. + min_confidence: The minimum confidence of the predictions to return. Defaults to 0. + overlap_threshold: The overlap (e.g. IoU) threshold for non-maximum suppression. Defaults to 0.1. + overlap_metric: Overlap metric to use for NMS. Default is "IoU". + nms: The type of non-maximum suppression to use. Defaults to 0. 0 is no NMS, 1 is standard NMS, 2 is fancy NMS and 3 is mask NMS. + valid_size_range: The range of valid sizes for the bounding boxes in pixels. Defaults to None (no valid size range). + edge_margin: The minimum gap between the edge of the image and the bounding box in pixels for a prediction to be considered valid. + Defaults to None (no edge margin). Returns: - out (`List[ultralytics.engine.results.Results]`): A list of postprocessed predictions. + A list of postprocessed predictions. + """ tile_size = imgs[0].shape[-1] - p : torch.Tensor = preds[0] + p = preds[0] + assert isinstance(p, torch.Tensor) # Convert from xywh to xyxy p[:, :4, :] = torch.cat(( p[:, 0:2, :] - p[:, 2:4, :] / 2, # x_min, y_min @@ -368,7 +430,7 @@ def postprocess( raise ValueError("min_confidence must be between 0 and 1.") if min_confidence > 0: num_above_min_conf = (p[:, 4, :] > min_confidence).sum(dim=1) - max_det = min(max_det, num_above_min_conf.max().item()) + max_det = min(max_det, int(num_above_min_conf.max().item())) # Filter top-`max_det` predictions if max_det != 0: # Filter out the predictions with the lowest confidence @@ -385,7 +447,12 @@ def postprocess( # Remove predictions with a confidence below min_confidence if min_confidence != 0: pred = pred[pred[:, 4] > min_confidence] - boxes = scale_boxes((tile_size, tile_size), pred[:, :4], imgs[i].shape[-2:], padding=False) + boxes = scale_boxes( + (tile_size, tile_size), + pred[:, :4], + (imgs[i].shape[-2], imgs[i].shape[-1]), + padding=False + ) # Remove predictions outside the valid size range if valid_size_range is not None: if valid_size_range[0] >= tile_size and valid_size_range[1] < 0: @@ -403,25 +470,50 @@ def postprocess( # Deduplicate predictions if nms != 0: if nms == 1: - nms_ind = nms_boxes(boxes, pred[:, 4], overlap_threshold=overlap_threshold, overlap_fn=overlap_metric) + nms_ind = nms_boxes( + boxes, pred[:, 4], + overlap_threshold=overlap_threshold, overlap_fn=overlap_metric + ) elif nms == 2: - nms_ind = fancy_nms(boxes, get_overlap_fn("box", overlap_metric), pred[:, 4], overlap_threshold=overlap_threshold, return_indices=True) + nms_ind = fancy_nms( + boxes, get_overlap_fn("box", overlap_metric), pred[:, 4], + overlap_threshold=overlap_threshold, return_indices=True + ) elif nms == 3: - masks = process_mask(protos[min(i, len(protos)-1)], pred[:, -32:], boxes, imgs[i].shape[-2:], False) # pred[:, -32:] - not sure this is correct for more than one class - nms_ind = nms_masks(masks, pred[:, 4], overlap_threshold=overlap_threshold, overlap_fn=overlap_metric, return_indices=True, boxes=boxes / 4, group_first=False) + # pred[:, -32:] - not sure this is correct for more than one class + masks = process_mask( + protos[min(i, len(protos)-1)], + pred[:, -32:], + boxes, + (imgs[i].shape[-2], imgs[i].shape[-1]), + False + ) + nms_ind = nms_masks( + masks, pred[:, 4], + overlap_threshold=overlap_threshold, overlap_fn=overlap_metric, + return_indices=True, boxes=boxes / 4, group_first=False + ) # group_first is True, because nms_masks has vectorized IoU, - # meaning that the overhead of doing connected-component clustering is larger than the time-loss from redundant IoU calculations + # meaning that the overhead of doing connected-component clustering is + # larger than the time-loss from redundant IoU calculations masks = masks[nms_ind] else: raise ValueError(f"nms must be 0, 1, 2 or 3, not {nms}") pred = pred[nms_ind] boxes = boxes[nms_ind] if nms != 3: - masks = process_mask(protos[i], pred[:, -32:], boxes, imgs[i].shape[-2:], False) # pred[:, -32:] - not sure this is correct for more than one class + # pred[:, -32:] - not sure this is correct for more than one class + masks = process_mask(protos[i], pred[:, -32:], boxes, imgs[i].shape[-2:], False) too_small = masks.sum(dim=[1, 2]) < 3 pred = pred[~too_small] boxes = boxes[~too_small] masks = masks[~too_small] pred[:, :4] = boxes - results.append({"orig_img" : Namespace(shape=imgs[i].permute(1,2,0).shape), "path" : "", "names" : ["insect"], "boxes" : pred[:, :6], "masks" : masks}) + results.append({ + "orig_img" : Namespace(shape=imgs[i].permute(1,2,0).shape), + "path" : "", + "names" : ["insect"], + "boxes" : pred[:, :6], + "masks" : masks + }) return results \ No newline at end of file diff --git a/utils/pre-annotate.py b/utils/pre-annotate.py index ce9b869..7ebbf1f 100644 --- a/utils/pre-annotate.py +++ b/utils/pre-annotate.py @@ -1,13 +1,12 @@ # This is a sample Python script. import glob +import json import os.path # Press Shift+F10 to execute it or replace it with your code. # Press Double Shift to search everywhere for classes, files, tool windows, actions, and settings. - import cv2 -import numpy as np -import json + def annotations(file_path): # Use a breakpoint in the code line below to debug your script. diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..d5e179a --- /dev/null +++ b/uv.lock @@ -0,0 +1,3031 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version < '3.14' and sys_platform != 'win32'", +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, +] + +[[package]] +name = "argon2-cffi" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi-bindings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" }, + { url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" }, + { url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" }, + { url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" }, + { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, + { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, + { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, + { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, +] + +[[package]] +name = "arrow" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, +] + +[[package]] +name = "async-lru" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/1f/989ecfef8e64109a489fff357450cb73fa73a865a92bd8c272170a6922c2/async_lru-2.3.0.tar.gz", hash = "sha256:89bdb258a0140d7313cf8f4031d816a042202faa61d0ab310a0a538baa1c24b6", size = 16332, upload-time = "2026-03-19T01:04:32.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl", hash = "sha256:eea27b01841909316f2cc739807acea1c623df2be8c5cfad7583286397bb8315", size = 8403, upload-time = "2026-03-19T01:04:30.883Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + +[[package]] +name = "bleach" +version = "6.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/18/3c8523962314be6bf4c8989c79ad9531c825210dd13a8669f6b84336e8bd/bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22", size = 203533, upload-time = "2025-10-27T17:57:39.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6", size = 164437, upload-time = "2025-10-27T17:57:37.538Z" }, +] + +[package.optional-dependencies] +css = [ + { name = "tinycss2" }, +] + +[[package]] +name = "boto3" +version = "1.42.96" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/2d/69fb3acd50bab83fb295c167d33c4b653faeb5fb0f42bfca4d9b69d6fb68/boto3-1.42.96.tar.gz", hash = "sha256:b38a9e4a3fbbee9017252576f1379780d0a5814768676c08df2f539d31fcdd68", size = 113203, upload-time = "2026-04-24T19:47:18.677Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/9d/b3f617d011c42eb804d993103b8fa9acdce153e181a3042f58bfe33d7cb4/boto3-1.42.96-py3-none-any.whl", hash = "sha256:2f4566da2c209a98bdbfc874d813ef231c84ad24e4f815e9bc91de5f63351a24", size = 140557, upload-time = "2026-04-24T19:47:15.824Z" }, +] + +[[package]] +name = "botocore" +version = "1.42.96" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/77/2c333622a1d47cf5bf73cdcab0cb6c92addafbef2ec05f81b9f75687d9e5/botocore-1.42.96.tar.gz", hash = "sha256:75b3b841ffacaa944f645196655a21ca777591dd8911e732bfb6614545af0250", size = 15263344, upload-time = "2026-04-24T19:47:05.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/56/152c3a859ca1b9d77ed16deac3cf81682013677c68cf5715698781fc81bd/botocore-1.42.96-py3-none-any.whl", hash = "sha256:db2c3e2006628be6fde81a24124a6563c363d6982fb92728837cf174bad9d98a", size = 14945920, upload-time = "2026-04-24T19:47:00.323Z" }, +] + +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +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/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" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { 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" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "comm" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "13.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder", marker = "sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/a9/3a8241c6e19483ac1f1dcf5c10238205dcb8a6e9d0d4d4709240dff28ff4/cuda_bindings-13.2.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:721104c603f059780d287969be3d194a18d0cc3b713ed9049065a1107706759d", size = 5730273, upload-time = "2026-03-11T00:12:37.18Z" }, + { url = "https://files.pythonhosted.org/packages/e9/94/2748597f47bb1600cd466b20cab4159f1530a3a33fe7f70fee199b3abb9e/cuda_bindings-13.2.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1eba9504ac70667dd48313395fe05157518fd6371b532790e96fbb31bbb5a5e1", size = 6313924, upload-time = "2026-03-11T00:12:39.462Z" }, + { url = "https://files.pythonhosted.org/packages/52/c8/b2589d68acf7e3d63e2be330b84bc25712e97ed799affbca7edd7eae25d6/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e865447abfb83d6a98ad5130ed3c70b1fc295ae3eeee39fd07b4ddb0671b6788", size = 5722404, upload-time = "2026-03-11T00:12:44.041Z" }, + { url = "https://files.pythonhosted.org/packages/1f/92/f899f7bbb5617bb65ec52a6eac1e9a1447a86b916c4194f8a5001b8cde0c/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46d8776a55d6d5da9dd6e9858fba2efcda2abe6743871dee47dd06eb8cb6d955", size = 6320619, upload-time = "2026-03-11T00:12:45.939Z" }, + { url = "https://files.pythonhosted.org/packages/df/93/eef988860a3ca985f82c4f3174fc0cdd94e07331ba9a92e8e064c260337f/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6629ca2df6f795b784752409bcaedbd22a7a651b74b56a165ebc0c9dcbd504d0", size = 5614610, upload-time = "2026-03-11T00:12:50.337Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/6db3aba46864aee357ab2415135b3fe3da7e9f1fa0221fa2a86a5968099c/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dca0da053d3b4cc4869eff49c61c03f3c5dbaa0bcd712317a358d5b8f3f385d", size = 6149914, upload-time = "2026-03-11T00:12:52.374Z" }, + { url = "https://files.pythonhosted.org/packages/c0/87/87a014f045b77c6de5c8527b0757fe644417b184e5367db977236a141602/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6464b30f46692d6c7f65d4a0e0450d81dd29de3afc1bb515653973d01c2cd6e", size = 5685673, upload-time = "2026-03-11T00:12:56.371Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5e/c0fe77a73aaefd3fff25ffaccaac69c5a63eafdf8b9a4c476626ef0ac703/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4af9f3e1be603fa12d5ad6cfca7844c9d230befa9792b5abdf7dd79979c3626", size = 6191386, upload-time = "2026-03-11T00:12:58.965Z" }, + { url = "https://files.pythonhosted.org/packages/5f/58/ed2c3b39c8dd5f96aa7a4abef0d47a73932c7a988e30f5fa428f00ed0da1/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df850a1ff8ce1b3385257b08e47b70e959932f5f432d0a4e46a355962b4e4771", size = 5507469, upload-time = "2026-03-11T00:13:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/0c941b112ceeb21439b05895eace78ca1aa2eaaf695c8521a068fd9b4c00/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8a16384c6494e5485f39314b0b4afb04bee48d49edb16d5d8593fd35bbd231b", size = 6059693, upload-time = "2026-03-11T00:13:06.003Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/d6/ac63065d33dd700fee7ebd7d287332401b54e31b9346e142f871e1f0b116/cuda_pathfinder-1.5.3-py3-none-any.whl", hash = "sha256:dff021123aedbb4117cc7ec81717bbfe198fb4e8b5f1ee57e0e084fec5c8577d", size = 49991, upload-time = "2026-04-14T20:09:27.037Z" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.0.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364, upload-time = "2025-12-19T23:24:07.328Z" }, +] + +[package.optional-dependencies] +cublas = [ + { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, +] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux'" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "sys_platform == 'linux'" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux'" }, +] +curand = [ + { name = "nvidia-curand", marker = "sys_platform == 'linux'" }, +] +cusolver = [ + { name = "nvidia-cusolver", marker = "sys_platform == 'linux'" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "sys_platform == 'linux'" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux'" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "sys_platform == 'linux'" }, +] + +[[package]] +name = "cvat-sdk" +version = "2.63.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "platformdirs" }, + { name = "python-dateutil" }, + { name = "tqdm" }, + { name = "typing-extensions" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/55/621c58c7f33be2f9ef9e2d2465fe0dc65ed60175c4acc288c1313af23666/cvat_sdk-2.63.0.tar.gz", hash = "sha256:2f24cfc4e455ba6e8f5ffcb55cfe66b834d1bb944040b398b7cfd5d8615df737", size = 241064, upload-time = "2026-04-23T16:21:47.793Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/2f/7e6e660ad6e1a9be5430cac5b5d4c2537ac78e4fb6dfe73447cb1fac45ab/cvat_sdk-2.63.0-py3-none-any.whl", hash = "sha256:32d421f0c5641c0457a7f2c5b4d8c1c68d48a03fa097280c84f8d08d3bdd279d", size = 785896, upload-time = "2026-04-23T16:21:44.603Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "debugpy" +version = "1.8.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207, upload-time = "2026-01-29T23:03:28.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/56/c3baf5cbe4dd77427fd9aef99fcdade259ad128feeb8a786c246adb838e5/debugpy-1.8.20-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:eada6042ad88fa1571b74bd5402ee8b86eded7a8f7b827849761700aff171f1b", size = 2208318, upload-time = "2026-01-29T23:03:36.481Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7d/4fa79a57a8e69fe0d9763e98d1110320f9ecd7f1f362572e3aafd7417c9d/debugpy-1.8.20-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:7de0b7dfeedc504421032afba845ae2a7bcc32ddfb07dae2c3ca5442f821c344", size = 3171493, upload-time = "2026-01-29T23:03:37.775Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f2/1e8f8affe51e12a26f3a8a8a4277d6e60aa89d0a66512f63b1e799d424a4/debugpy-1.8.20-cp311-cp311-win32.whl", hash = "sha256:773e839380cf459caf73cc533ea45ec2737a5cc184cf1b3b796cd4fd98504fec", size = 5209240, upload-time = "2026-01-29T23:03:39.109Z" }, + { url = "https://files.pythonhosted.org/packages/d5/92/1cb532e88560cbee973396254b21bece8c5d7c2ece958a67afa08c9f10dc/debugpy-1.8.20-cp311-cp311-win_amd64.whl", hash = "sha256:1f7650546e0eded1902d0f6af28f787fa1f1dbdbc97ddabaf1cd963a405930cb", size = 5233481, upload-time = "2026-01-29T23:03:40.659Z" }, + { url = "https://files.pythonhosted.org/packages/14/57/7f34f4736bfb6e00f2e4c96351b07805d83c9a7b33d28580ae01374430f7/debugpy-1.8.20-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d", size = 2550686, upload-time = "2026-01-29T23:03:42.023Z" }, + { url = "https://files.pythonhosted.org/packages/ab/78/b193a3975ca34458f6f0e24aaf5c3e3da72f5401f6054c0dfd004b41726f/debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b", size = 4310588, upload-time = "2026-01-29T23:03:43.314Z" }, + { url = "https://files.pythonhosted.org/packages/c1/55/f14deb95eaf4f30f07ef4b90a8590fc05d9e04df85ee379712f6fb6736d7/debugpy-1.8.20-cp312-cp312-win32.whl", hash = "sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390", size = 5331372, upload-time = "2026-01-29T23:03:45.526Z" }, + { url = "https://files.pythonhosted.org/packages/a1/39/2bef246368bd42f9bd7cba99844542b74b84dacbdbea0833e610f384fee8/debugpy-1.8.20-cp312-cp312-win_amd64.whl", hash = "sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3", size = 5372835, upload-time = "2026-01-29T23:03:47.245Z" }, + { url = "https://files.pythonhosted.org/packages/15/e2/fc500524cc6f104a9d049abc85a0a8b3f0d14c0a39b9c140511c61e5b40b/debugpy-1.8.20-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:5dff4bb27027821fdfcc9e8f87309a28988231165147c31730128b1c983e282a", size = 2539560, upload-time = "2026-01-29T23:03:48.738Z" }, + { url = "https://files.pythonhosted.org/packages/90/83/fb33dcea789ed6018f8da20c5a9bc9d82adc65c0c990faed43f7c955da46/debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf", size = 4293272, upload-time = "2026-01-29T23:03:50.169Z" }, + { url = "https://files.pythonhosted.org/packages/a6/25/b1e4a01bfb824d79a6af24b99ef291e24189080c93576dfd9b1a2815cd0f/debugpy-1.8.20-cp313-cp313-win32.whl", hash = "sha256:da11dea6447b2cadbf8ce2bec59ecea87cc18d2c574980f643f2d2dfe4862393", size = 5331208, upload-time = "2026-01-29T23:03:51.547Z" }, + { url = "https://files.pythonhosted.org/packages/13/f7/a0b368ce54ffff9e9028c098bd2d28cfc5b54f9f6c186929083d4c60ba58/debugpy-1.8.20-cp313-cp313-win_amd64.whl", hash = "sha256:eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7", size = 5372930, upload-time = "2026-01-29T23:03:53.585Z" }, + { url = "https://files.pythonhosted.org/packages/33/2e/f6cb9a8a13f5058f0a20fe09711a7b726232cd5a78c6a7c05b2ec726cff9/debugpy-1.8.20-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9c74df62fc064cd5e5eaca1353a3ef5a5d50da5eb8058fcef63106f7bebe6173", size = 2538066, upload-time = "2026-01-29T23:03:54.999Z" }, + { url = "https://files.pythonhosted.org/packages/c5/56/6ddca50b53624e1ca3ce1d1e49ff22db46c47ea5fb4c0cc5c9b90a616364/debugpy-1.8.20-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:077a7447589ee9bc1ff0cdf443566d0ecf540ac8aa7333b775ebcb8ce9f4ecad", size = 4269425, upload-time = "2026-01-29T23:03:56.518Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d9/d64199c14a0d4c476df46c82470a3ce45c8d183a6796cfb5e66533b3663c/debugpy-1.8.20-cp314-cp314-win32.whl", hash = "sha256:352036a99dd35053b37b7803f748efc456076f929c6a895556932eaf2d23b07f", size = 5331407, upload-time = "2026-01-29T23:03:58.481Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d9/1f07395b54413432624d61524dfd98c1a7c7827d2abfdb8829ac92638205/debugpy-1.8.20-cp314-cp314-win_amd64.whl", hash = "sha256:a98eec61135465b062846112e5ecf2eebb855305acc1dfbae43b72903b8ab5be", size = 5372521, upload-time = "2026-01-29T23:03:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "fastjsonschema" +version = "2.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, +] + +[[package]] +name = "filelock" +version = "3.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, +] + +[[package]] +name = "flat-bug" +version = "1.0.0" +source = { editable = "." } +dependencies = [ + { name = "scikit-optimize" }, + { name = "scipy" }, + { name = "shapely" }, + { name = "torch" }, + { name = "torchvision" }, + { name = "ultralytics" }, +] + +[package.optional-dependencies] +cloud-datasets = [ + { name = "boto3" }, + { name = "cvat-sdk" }, +] +erda = [ + { name = "pyremotedata" }, +] +jupyter = [ + { name = "jupyter" }, +] + +[package.dev-dependencies] +dev = [ + { name = "flat-bug", extra = ["cloud-datasets", "erda"] }, +] + +[package.metadata] +requires-dist = [ + { name = "boto3", marker = "extra == 'cloud-datasets'", specifier = ">=1.40" }, + { name = "cvat-sdk", marker = "extra == 'cloud-datasets'", specifier = ">=2.47" }, + { name = "jupyter", marker = "extra == 'jupyter'", specifier = ">=1.0.0" }, + { name = "pyremotedata", marker = "extra == 'erda'", specifier = ">=0.0.16" }, + { name = "scikit-optimize", specifier = ">=0.10.1" }, + { name = "scipy", specifier = ">=1.14.1" }, + { name = "shapely", specifier = ">=2.0.2" }, + { name = "torch", specifier = ">=2.2.0" }, + { name = "torchvision", specifier = ">=0.17.0" }, + { name = "ultralytics", specifier = ">=8.2.16,<=8.3.124" }, +] +provides-extras = ["erda", "cloud-datasets", "jupyter"] + +[package.metadata.requires-dev] +dev = [{ name = "flat-bug", extras = ["notebook", "erda", "cloud-datasets"] }] + +[[package]] +name = "fonttools" +version = "4.62.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/23ff32561ec8d45a4d48578b4d241369d9270dc50926c017570e60893701/fonttools-4.62.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7", size = 2871039, upload-time = "2026-03-13T13:52:33.127Z" }, + { url = "https://files.pythonhosted.org/packages/24/7f/66d3f8a9338a9b67fe6e1739f47e1cd5cee78bd3bc1206ef9b0b982289a5/fonttools-4.62.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14", size = 2416346, upload-time = "2026-03-13T13:52:35.676Z" }, + { url = "https://files.pythonhosted.org/packages/aa/53/5276ceba7bff95da7793a07c5284e1da901cf00341ce5e2f3273056c0cca/fonttools-4.62.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6acb4109f8bee00fec985c8c7afb02299e35e9c94b57287f3ea542f28bd0b0a7", size = 5100897, upload-time = "2026-03-13T13:52:38.102Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a1/40a5c4d8e28b0851d53a8eeeb46fbd73c325a2a9a165f290a5ed90e6c597/fonttools-4.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b", size = 5071078, upload-time = "2026-03-13T13:52:41.305Z" }, + { url = "https://files.pythonhosted.org/packages/e3/be/d378fca4c65ea1956fee6d90ace6e861776809cbbc5af22388a090c3c092/fonttools-4.62.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a5d8825e1140f04e6c99bb7d37a9e31c172f3bc208afbe02175339e699c710e1", size = 5076908, upload-time = "2026-03-13T13:52:44.122Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d9/ae6a1d0693a4185a84605679c8a1f719a55df87b9c6e8e817bfdd9ef5936/fonttools-4.62.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:268abb1cb221e66c014acc234e872b7870d8b5d4657a83a8f4205094c32d2416", size = 5202275, upload-time = "2026-03-13T13:52:46.591Z" }, + { url = "https://files.pythonhosted.org/packages/54/6c/af95d9c4efb15cabff22642b608342f2bd67137eea6107202d91b5b03184/fonttools-4.62.1-cp311-cp311-win32.whl", hash = "sha256:942b03094d7edbb99bdf1ae7e9090898cad7bf9030b3d21f33d7072dbcb51a53", size = 2293075, upload-time = "2026-03-13T13:52:48.711Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/bf54c5b3f2be34e1f143e6db838dfdc54f2ffa3e68c738934c82f3b2a08d/fonttools-4.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2", size = 2344593, upload-time = "2026-03-13T13:52:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/dbacced3953544b9a93088cc10ef2b596d348c983d5c67a404fa41ec51ba/fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974", size = 2870219, upload-time = "2026-03-13T13:52:53.664Z" }, + { url = "https://files.pythonhosted.org/packages/66/9e/a769c8e99b81e5a87ab7e5e7236684de4e96246aae17274e5347d11ebd78/fonttools-4.62.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9", size = 2414891, upload-time = "2026-03-13T13:52:56.493Z" }, + { url = "https://files.pythonhosted.org/packages/69/64/f19a9e3911968c37e1e620e14dfc5778299e1474f72f4e57c5ec771d9489/fonttools-4.62.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936", size = 5033197, upload-time = "2026-03-13T13:52:59.179Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392", size = 4988768, upload-time = "2026-03-13T13:53:02.761Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/0f904540d3e6ab463c1243a0d803504826a11604c72dd58c2949796a1762/fonttools-4.62.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04", size = 4971512, upload-time = "2026-03-13T13:53:05.678Z" }, + { url = "https://files.pythonhosted.org/packages/29/0b/5cbef6588dc9bd6b5c9ad6a4d5a8ca384d0cea089da31711bbeb4f9654a6/fonttools-4.62.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d", size = 5122723, upload-time = "2026-03-13T13:53:08.662Z" }, + { url = "https://files.pythonhosted.org/packages/4a/47/b3a5342d381595ef439adec67848bed561ab7fdb1019fa522e82101b7d9c/fonttools-4.62.1-cp312-cp312-win32.whl", hash = "sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c", size = 2281278, upload-time = "2026-03-13T13:53:10.998Z" }, + { url = "https://files.pythonhosted.org/packages/28/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42", size = 2331414, upload-time = "2026-03-13T13:53:13.992Z" }, + { url = "https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79", size = 2865155, upload-time = "2026-03-13T13:53:16.132Z" }, + { url = "https://files.pythonhosted.org/packages/03/c5/0e3966edd5ec668d41dfe418787726752bc07e2f5fd8c8f208615e61fa89/fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe", size = 2412802, upload-time = "2026-03-13T13:53:18.878Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/e6ac4b44026de7786fe46e3bfa0c87e51d5d70a841054065d49cd62bb909/fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68", size = 5013926, upload-time = "2026-03-13T13:53:21.379Z" }, + { url = "https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1", size = 4964575, upload-time = "2026-03-13T13:53:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/46/76/7d051671e938b1881670528fec69cc4044315edd71a229c7fd712eaa5119/fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069", size = 4953693, upload-time = "2026-03-13T13:53:26.569Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ae/b41f8628ec0be3c1b934fc12b84f4576a5c646119db4d3bdd76a217c90b5/fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9", size = 5094920, upload-time = "2026-03-13T13:53:29.329Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/53a1e9469331a23dcc400970a27a4caa3d9f6edbf5baab0260285238b884/fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24", size = 2279928, upload-time = "2026-03-13T13:53:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056", size = 2330514, upload-time = "2026-03-13T13:53:34.991Z" }, + { url = "https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca", size = 2864442, upload-time = "2026-03-13T13:53:37.509Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b2/e521803081f8dc35990816b82da6360fa668a21b44da4b53fc9e77efcd62/fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca", size = 2410901, upload-time = "2026-03-13T13:53:40.55Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/8c3511ff06e53110039358dbbdc1a65d72157a054638387aa2ada300a8b8/fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782", size = 4999608, upload-time = "2026-03-13T13:53:42.798Z" }, + { url = "https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae", size = 4912726, upload-time = "2026-03-13T13:53:45.405Z" }, + { url = "https://files.pythonhosted.org/packages/70/b9/ac677cb07c24c685cf34f64e140617d58789d67a3dd524164b63648c6114/fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7", size = 4951422, upload-time = "2026-03-13T13:53:48.326Z" }, + { url = "https://files.pythonhosted.org/packages/e6/10/11c08419a14b85b7ca9a9faca321accccc8842dd9e0b1c8a72908de05945/fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a", size = 5060979, upload-time = "2026-03-13T13:53:51.366Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3c/12eea4a4cf054e7ab058ed5ceada43b46809fce2bf319017c4d63ae55bb4/fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800", size = 2283733, upload-time = "2026-03-13T13:53:53.606Z" }, + { url = "https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e", size = 2335663, upload-time = "2026-03-13T13:53:56.23Z" }, + { url = "https://files.pythonhosted.org/packages/42/c5/4d2ed3ca6e33617fc5624467da353337f06e7f637707478903c785bd8e20/fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82", size = 2947288, upload-time = "2026-03-13T13:53:59.397Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e9/7ab11ddfda48ed0f89b13380e5595ba572619c27077be0b2c447a63ff351/fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260", size = 2449023, upload-time = "2026-03-13T13:54:01.642Z" }, + { url = "https://files.pythonhosted.org/packages/b2/10/a800fa090b5e8819942e54e19b55fc7c21fe14a08757c3aa3ca8db358939/fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4", size = 5137599, upload-time = "2026-03-13T13:54:04.495Z" }, + { url = "https://files.pythonhosted.org/packages/37/dc/8ccd45033fffd74deb6912fa1ca524643f584b94c87a16036855b498a1ed/fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b", size = 4920933, upload-time = "2026-03-13T13:54:07.557Z" }, + { url = "https://files.pythonhosted.org/packages/99/eb/e618adefb839598d25ac8136cd577925d6c513dc0d931d93b8af956210f0/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87", size = 5016232, upload-time = "2026-03-13T13:54:10.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5f/9b5c9bfaa8ec82def8d8168c4f13615990d6ce5996fe52bd49bfb5e05134/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c", size = 5042987, upload-time = "2026-03-13T13:54:13.569Z" }, + { url = "https://files.pythonhosted.org/packages/90/aa/dfbbe24c6a6afc5c203d90cc0343e24bcbb09e76d67c4d6eef8c2558d7ba/fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a", size = 2348021, upload-time = "2026-03-13T13:54:16.98Z" }, + { url = "https://files.pythonhosted.org/packages/13/6f/ae9c4e4dd417948407b680855c2c7790efb52add6009aaecff1e3bc50e8e/fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e", size = 2414147, upload-time = "2026-03-13T13:54:19.416Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" }, +] + +[[package]] +name = "fqdn" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/3e/a80a8c077fd798951169626cde3e239adeba7dab75deb3555716415bd9b0/fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f", size = 6015, upload-time = "2021-03-11T07:16:29.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/cf/b50ddf667c15276a9ab15a70ef5f257564de271957933ffea49d2cdbcdfb/fsspec-2026.3.0.tar.gz", hash = "sha256:1ee6a0e28677557f8c2f994e3eea77db6392b4de9cd1f5d7a9e87a0ae9d01b41", size = 313547, upload-time = "2026-03-27T19:11:14.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl", hash = "sha256:d2ceafaad1b3457968ed14efa28798162f1638dbb5d2a6868a2db002a5ee39a4", size = 202595, upload-time = "2026-03-27T19:11:13.595Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, +] + +[[package]] +name = "ipykernel" +version = "7.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appnope", marker = "sys_platform == 'darwin'" }, + { name = "comm" }, + { name = "debugpy" }, + { name = "ipython" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "matplotlib-inline" }, + { name = "nest-asyncio" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/8d/b68b728e2d06b9e0051019640a40a9eb7a88fcd82c2e1b5ce70bef5ff044/ipykernel-7.2.0.tar.gz", hash = "sha256:18ed160b6dee2cbb16e5f3575858bc19d8f1fe6046a9a680c708494ce31d909e", size = 176046, upload-time = "2026-02-06T16:43:27.403Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl", hash = "sha256:3bbd4420d2b3cc105cbdf3756bfc04500b1e52f090a90716851f3916c62e1661", size = 118788, upload-time = "2026-02-06T16:43:25.149Z" }, +] + +[[package]] +name = "ipython" +version = "9.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "psutil" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/87cda5842cf5c31837c06ddb588e11c3c35d8ece89b7a0108c06b8c9b00a/ipython-9.13.0.tar.gz", hash = "sha256:7e834b6afc99f020e3f05966ced34792f40267d64cb1ea9043886dab0dde5967", size = 4430549, upload-time = "2026-04-24T12:24:55.221Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/86/3060e8029b7cc505cce9a0137431dda81d0a3fde93a8f0f50ee0bf37a795/ipython-9.13.0-py3-none-any.whl", hash = "sha256:57f9d4639e20818d328d287c7b549af3d05f12486ea8f2e7f73e52a36ec4d201", size = 627274, upload-time = "2026-04-24T12:24:53.038Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "ipywidgets" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "comm" }, + { name = "ipython" }, + { name = "jupyterlab-widgets" }, + { name = "traitlets" }, + { name = "widgetsnbextension" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/ae/c5ce1edc1afe042eadb445e95b0671b03cee61895264357956e61c0d2ac0/ipywidgets-8.1.8.tar.gz", hash = "sha256:61f969306b95f85fba6b6986b7fe45d73124d1d9e3023a8068710d47a22ea668", size = 116739, upload-time = "2025-11-01T21:18:12.393Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl", hash = "sha256:ecaca67aed704a338f88f67b1181b58f821ab5dc89c1f0f5ef99db43c1c2921e", size = 139808, upload-time = "2025-11-01T21:18:10.956Z" }, +] + +[[package]] +name = "isoduration" +version = "20.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "arrow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649, upload-time = "2020-11-01T11:00:00.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "json5" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/4b/6f8906aaf67d501e259b0adab4d312945bb7211e8b8d4dcc77c92320edaa/json5-0.14.0.tar.gz", hash = "sha256:b3f492fad9f6cdbced8b7d40b28b9b1c9701c5f561bef0d33b81c2ff433fefcb", size = 52656, upload-time = "2026-03-27T22:50:48.108Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/42/cf027b4ac873b076189d935b135397675dac80cb29acb13e1ab86ad6c631/json5-0.14.0-py3-none-any.whl", hash = "sha256:56cf861bab076b1178eb8c92e1311d273a9b9acea2ccc82c276abf839ebaef3a", size = 36271, upload-time = "2026-03-27T22:50:47.073Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[package.optional-dependencies] +format-nongpl = [ + { name = "fqdn" }, + { name = "idna" }, + { name = "isoduration" }, + { name = "jsonpointer" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "rfc3987-syntax" }, + { name = "uri-template" }, + { name = "webcolors" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "jupyter" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipykernel" }, + { name = "ipywidgets" }, + { name = "jupyter-console" }, + { name = "jupyterlab" }, + { name = "nbconvert" }, + { name = "notebook" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/f3/af28ea964ab8bc1e472dba2e82627d36d470c51f5cd38c37502eeffaa25e/jupyter-1.1.1.tar.gz", hash = "sha256:d55467bceabdea49d7e3624af7e33d59c37fff53ed3a350e1ac957bed731de7a", size = 5714959, upload-time = "2024-08-30T07:15:48.299Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/64/285f20a31679bf547b75602702f7800e74dbabae36ef324f716c02804753/jupyter-1.1.1-py2.py3-none-any.whl", hash = "sha256:7a59533c22af65439b24bbe60373a4e95af8f16ac65a6c00820ad378e3f7cc83", size = 2657, upload-time = "2024-08-30T07:15:47.045Z" }, +] + +[[package]] +name = "jupyter-client" +version = "8.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" }, +] + +[[package]] +name = "jupyter-console" +version = "6.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipykernel" }, + { name = "ipython" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "pyzmq" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/2d/e2fd31e2fc41c14e2bcb6c976ab732597e907523f6b2420305f9fc7fdbdb/jupyter_console-6.6.3.tar.gz", hash = "sha256:566a4bf31c87adbfadf22cdf846e3069b59a71ed5da71d6ba4d8aaad14a53539", size = 34363, upload-time = "2023-03-06T14:13:31.02Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/77/71d78d58f15c22db16328a476426f7ac4a60d3a5a7ba3b9627ee2f7903d4/jupyter_console-6.6.3-py3-none-any.whl", hash = "sha256:309d33409fcc92ffdad25f0bcdf9a4a9daa61b6f341177570fdac03de5352485", size = 24510, upload-time = "2023-03-06T14:13:28.229Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, +] + +[[package]] +name = "jupyter-events" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema", extra = ["format-nongpl"] }, + { name = "packaging" }, + { name = "python-json-logger" }, + { name = "pyyaml" }, + { name = "referencing" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/f8/475c4241b2b75af0deaae453ed003c6c851766dbc44d332d8baf245dc931/jupyter_events-0.12.1.tar.gz", hash = "sha256:faff25f77218335752f35f23c5fe6e4a392a7bd99a5939ccb9b8fbf594636cf3", size = 62854, upload-time = "2026-04-20T23:17:50.66Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/6c/6fcde0c8f616ed360ffd3587f7db9e225a7e62b583a04494d2f069cf64ea/jupyter_events-0.12.1-py3-none-any.whl", hash = "sha256:c366585253f537a627da52fa7ca7410c5b5301fe893f511e7b077c2d93ec8bcf", size = 19512, upload-time = "2026-04-20T23:17:48.927Z" }, +] + +[[package]] +name = "jupyter-lsp" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/ff/1e4a61f5170a9a1d978f3ac3872449de6c01fc71eaf89657824c878b1549/jupyter_lsp-2.3.1.tar.gz", hash = "sha256:fdf8a4aa7d85813976d6e29e95e6a2c8f752701f926f2715305249a3829805a6", size = 55677, upload-time = "2026-04-02T08:10:06.749Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/e8/9d61dcbd1dce8ef418f06befd4ac084b4720429c26b0b1222bc218685eff/jupyter_lsp-2.3.1-py3-none-any.whl", hash = "sha256:71b954d834e85ff3096400554f2eefaf7fe37053036f9a782b0f7c5e42dadb81", size = 77513, upload-time = "2026-04-02T08:10:01.753Z" }, +] + +[[package]] +name = "jupyter-server" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "argon2-cffi" }, + { name = "jinja2" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "jupyter-events" }, + { name = "jupyter-server-terminals" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "overrides", marker = "python_full_version < '3.12'" }, + { name = "packaging" }, + { name = "prometheus-client" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "pyzmq" }, + { name = "send2trash" }, + { name = "terminado" }, + { name = "tornado" }, + { name = "traitlets" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/ac/e040ec363d7b6b1f11304cc9f209dac4517ece5d5e01821366b924a64a50/jupyter_server-2.17.0.tar.gz", hash = "sha256:c38ea898566964c888b4772ae1ed58eca84592e88251d2cfc4d171f81f7e99d5", size = 731949, upload-time = "2025-08-21T14:42:54.042Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl", hash = "sha256:e8cb9c7db4251f51ed307e329b81b72ccf2056ff82d50524debde1ee1870e13f", size = 388221, upload-time = "2025-08-21T14:42:52.034Z" }, +] + +[[package]] +name = "jupyter-server-terminals" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "terminado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/a7/bcd0a9b0cbba88986fe944aaaf91bfda603e5a50bda8ed15123f381a3b2f/jupyter_server_terminals-0.5.4.tar.gz", hash = "sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5", size = 31770, upload-time = "2026-01-14T16:53:20.213Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl", hash = "sha256:55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14", size = 13704, upload-time = "2026-01-14T16:53:18.738Z" }, +] + +[[package]] +name = "jupyterlab" +version = "4.5.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-lru" }, + { name = "httpx" }, + { name = "ipykernel" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyter-lsp" }, + { name = "jupyter-server" }, + { name = "jupyterlab-server" }, + { name = "notebook-shim" }, + { name = "packaging" }, + { name = "setuptools" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/d5/730628e03fff2e8a8e8ccdaedde1489ab1309f9a4fa2536248884e30b7c7/jupyterlab-4.5.6.tar.gz", hash = "sha256:642fe2cfe7f0f5922a8a558ba7a0d246c7bc133b708dfe43f7b3a826d163cf42", size = 23970670, upload-time = "2026-03-11T14:17:04.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl", hash = "sha256:d6b3dac883aa4d9993348e0f8e95b24624f75099aed64eab6a4351a9cdd1e580", size = 12447124, upload-time = "2026-03-11T14:17:00.229Z" }, +] + +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, +] + +[[package]] +name = "jupyterlab-server" +version = "2.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "jinja2" }, + { name = "json5" }, + { name = "jsonschema" }, + { name = "jupyter-server" }, + { name = "packaging" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/2c/90153f189e421e93c4bb4f9e3f59802a1f01abd2ac5cf40b152d7f735232/jupyterlab_server-2.28.0.tar.gz", hash = "sha256:35baa81898b15f93573e2deca50d11ac0ae407ebb688299d3a5213265033712c", size = 76996, upload-time = "2025-10-22T13:59:18.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl", hash = "sha256:e4355b148fdcf34d312bbbc80f22467d6d20460e8b8736bf235577dd18506968", size = 59830, upload-time = "2025-10-22T13:59:16.767Z" }, +] + +[[package]] +name = "jupyterlab-widgets" +version = "3.0.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/2d/ef58fed122b268c69c0aa099da20bc67657cdfb2e222688d5731bd5b971d/jupyterlab_widgets-3.0.16.tar.gz", hash = "sha256:423da05071d55cf27a9e602216d35a3a65a3e41cdf9c5d3b643b814ce38c19e0", size = 897423, upload-time = "2025-11-01T21:11:29.724Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl", hash = "sha256:45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8", size = 914926, upload-time = "2025-11-01T21:11:28.008Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/dd/a495a9c104be1c476f0386e714252caf2b7eca883915422a64c50b88c6f5/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c", size = 122798, upload-time = "2026-03-09T13:12:58.963Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/37b4047a2af0cf5ef6d8b4b26e91829ae6fc6a2d1f74524bcb0e7cd28a32/kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb", size = 66216, upload-time = "2026-03-09T13:13:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/510dc933d87767584abfe03efa445889996c70c2990f6f87c3ebaa0a18c5/kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac", size = 63911, upload-time = "2026-03-09T13:13:01.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/46/bddc13df6c2a40741e0cc7865bb1c9ed4796b6760bd04ce5fae3928ef917/kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27", size = 1438209, upload-time = "2026-03-09T13:13:03.385Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/76621246f5165e5372f02f5e6f3f48ea336a8f9e96e43997d45b240ed8cd/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398", size = 1248888, upload-time = "2026-03-09T13:13:05.231Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c1/31559ec6fb39a5b48035ce29bb63ade628f321785f38c384dee3e2c08bc1/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db", size = 1266304, upload-time = "2026-03-09T13:13:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ef/1cb8276f2d29cc6a41e0a042f27946ca347d3a4a75acf85d0a16aa6dcc82/kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc", size = 1319650, upload-time = "2026-03-09T13:13:08.607Z" }, + { url = "https://files.pythonhosted.org/packages/4c/e4/5ba3cecd7ce6236ae4a80f67e5d5531287337d0e1f076ca87a5abe4cd5d0/kiwisolver-1.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679", size = 970949, upload-time = "2026-03-09T13:13:10.299Z" }, + { url = "https://files.pythonhosted.org/packages/5a/69/dc61f7ae9a2f071f26004ced87f078235b5507ab6e5acd78f40365655034/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309", size = 2199125, upload-time = "2026-03-09T13:13:11.841Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7b/abbe0f1b5afa85f8d084b73e90e5f801c0939eba16ac2e49af7c61a6c28d/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2", size = 2293783, upload-time = "2026-03-09T13:13:14.399Z" }, + { url = "https://files.pythonhosted.org/packages/8a/80/5908ae149d96d81580d604c7f8aefd0e98f4fd728cf172f477e9f2a81744/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c", size = 1960726, upload-time = "2026-03-09T13:13:16.047Z" }, + { url = "https://files.pythonhosted.org/packages/84/08/a78cb776f8c085b7143142ce479859cfec086bd09ee638a317040b6ef420/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08", size = 2464738, upload-time = "2026-03-09T13:13:17.897Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e1/65584da5356ed6cb12c63791a10b208860ac40a83de165cb6a6751a686e3/kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4", size = 2270718, upload-time = "2026-03-09T13:13:19.421Z" }, + { url = "https://files.pythonhosted.org/packages/be/6c/28f17390b62b8f2f520e2915095b3c94d88681ecf0041e75389d9667f202/kiwisolver-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b", size = 73480, upload-time = "2026-03-09T13:13:20.818Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0e/2ee5debc4f77a625778fec5501ff3e8036fe361b7ee28ae402a485bb9694/kiwisolver-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac", size = 64930, upload-time = "2026-03-09T13:13:21.997Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" }, + { url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" }, + { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" }, + { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" }, + { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" }, + { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" }, + { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" }, + { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" }, + { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" }, + { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" }, + { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" }, + { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" }, + { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" }, + { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" }, + { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" }, + { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" }, + { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" }, + { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" }, + { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" }, + { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" }, + { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" }, + { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" }, + { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" }, + { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" }, + { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, + { url = "https://files.pythonhosted.org/packages/e9/eb/5fcbbbf9a0e2c3a35effb88831a483345326bbc3a030a3b5b69aee647f84/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232", size = 59532, upload-time = "2026-03-09T13:15:47.047Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9b/e17104555bb4db148fd52327feea1e96be4b88e8e008b029002c281a21ab/kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a", size = 57420, upload-time = "2026-03-09T13:15:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/48/44/2b5b95b7aa39fb2d8d9d956e0f3d5d45aef2ae1d942d4c3ffac2f9cfed1a/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737", size = 79892, upload-time = "2026-03-09T13:15:49.694Z" }, + { url = "https://files.pythonhosted.org/packages/52/7d/7157f9bba6b455cfb4632ed411e199fc8b8977642c2b12082e1bd9e6d173/kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16", size = 77603, upload-time = "2026-03-09T13:15:50.945Z" }, + { url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" }, +] + +[[package]] +name = "lark" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732, upload-time = "2025-10-27T18:25:56.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/8c/290f021104741fea63769c31494f5324c0cd249bf536a65a4350767b1f22/matplotlib-3.10.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:68cfdcede415f7c8f5577b03303dd94526cdb6d11036cecdc205e08733b2d2bb", size = 8306860, upload-time = "2026-04-24T00:12:01.207Z" }, + { url = "https://files.pythonhosted.org/packages/51/18/325cd32ece1120d1da51cc4e4294c6580190699490183fc2fe8cb6d61ec5/matplotlib-3.10.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfca0129678bd56379db26c52b5d77ed7de314c047492fbdc763aa7501710cfb", size = 8199254, upload-time = "2026-04-24T00:12:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/79/db/e28c1b83e3680740aa78925f5fb2ae4d16207207419ad75ea9fe604f8676/matplotlib-3.10.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e436d155fa8a3399dc62683f8f5d0e2e50d25d0144a73edd73f82eec8f4abfb", size = 8777092, upload-time = "2026-04-24T00:12:06.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/fa/3ce7adfe9ba101748f465211660d9c6374c876b671bdb8c2bb6d347e8b94/matplotlib-3.10.9-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56fc0bd271b00025c6edfdc7c2dcd247372c8e1544971d62e1dc7c17367e8bf9", size = 9595691, upload-time = "2026-04-24T00:12:09.706Z" }, + { url = "https://files.pythonhosted.org/packages/36/c4/6960a76686ed668f2c60f84e9799ba4c0d56abdb36b1577b60c1d061d1ec/matplotlib-3.10.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5a6104ed666402ba5106d7f36e0e0cdca4e8d7fa4d39708ca88019e2835a2eb", size = 9659771, upload-time = "2026-04-24T00:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0d/271aace3342157c64700c9ff4c59c7b392f3dbab393692e8db6fbe7ab96c/matplotlib-3.10.9-cp311-cp311-win_amd64.whl", hash = "sha256:d730e984eddf56974c3e72b6129c7ca462ac38dc624338f4b0b23eb23ecba00f", size = 8205112, upload-time = "2026-04-24T00:12:15.773Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ee/cb57ad4754f3e7b9174ce6ce66d9205fb827067e48a9f58ac09d7e7d6b77/matplotlib-3.10.9-cp311-cp311-win_arm64.whl", hash = "sha256:51bf0ddbdc598e060d46c16b5590708f81a1624cefbaaf62f6a81bf9285b8c80", size = 8132310, upload-time = "2026-04-24T00:12:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/35/c6/5581e26c72233ebb2a2a6fed2d24fb7c66b4700120b813f51b0555acf0b6/matplotlib-3.10.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1", size = 8319908, upload-time = "2026-04-24T00:12:21.323Z" }, + { url = "https://files.pythonhosted.org/packages/b7/18/4880dd762e40cd360c1bf06e890c5a97b997e91cb324602b1a19950ad5ce/matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320", size = 8216016, upload-time = "2026-04-24T00:12:23.4Z" }, + { url = "https://files.pythonhosted.org/packages/32/91/d024616abdba99e83120e07a20658976f6a343646710760c4a51df126029/matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285", size = 8789336, upload-time = "2026-04-24T00:12:26.096Z" }, + { url = "https://files.pythonhosted.org/packages/5c/04/030a2f61ef2158f5e4c259487a92ac877732499fb33d871585d89e03c42d/matplotlib-3.10.9-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2", size = 9604602, upload-time = "2026-04-24T00:12:29.052Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c2/541e4d09d87bb6b5830fc28b4c887a9a8cf4e1c6cee698a8c05552ae2003/matplotlib-3.10.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf", size = 9670966, upload-time = "2026-04-24T00:12:32.131Z" }, + { url = "https://files.pythonhosted.org/packages/04/a1/4571fc46e7702de8d0c2dc54ad1b2f8e29328dea3ee90831181f7353d93c/matplotlib-3.10.9-cp312-cp312-win_amd64.whl", hash = "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6", size = 8217462, upload-time = "2026-04-24T00:12:35.226Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d0/2269edb12aa30c13c8bcc9382892e39943ce1d28aab4ec296e0381798e81/matplotlib-3.10.9-cp312-cp312-win_arm64.whl", hash = "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42", size = 8136688, upload-time = "2026-04-24T00:12:37.442Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d3/8d4f6afbecb49fc04e060a57c0fce39ea51cc163a6bd87303ccd698e4fa6/matplotlib-3.10.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f", size = 8320331, upload-time = "2026-04-24T00:12:39.688Z" }, + { url = "https://files.pythonhosted.org/packages/63/d9/9e14bc7564bf92d5ffa801ae5fac819ce74b925dfb55e3ebde61a3bbad3e/matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e", size = 8216461, upload-time = "2026-04-24T00:12:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/8a/17/4402d0d14ccf1dfc70932600b68097fbbf9c898a4871d2cbbe79c7801a32/matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f", size = 8790091, upload-time = "2026-04-24T00:12:44.789Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0b/322aeec06dd9b91411f92028b37d447342770a24392aa4813e317064dad5/matplotlib-3.10.9-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838", size = 9605027, upload-time = "2026-04-24T00:12:47.583Z" }, + { url = "https://files.pythonhosted.org/packages/74/88/5f13482f55e7b00bcfc09838b093c2456e1379978d2a146844aae05350ad/matplotlib-3.10.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2", size = 9671269, upload-time = "2026-04-24T00:12:50.878Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/0840fd2f93da988ec660b8ad1984abe9f25d2aed22a5e394ff1c68c88307/matplotlib-3.10.9-cp313-cp313-win_amd64.whl", hash = "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921", size = 8217588, upload-time = "2026-04-24T00:12:53.784Z" }, + { url = "https://files.pythonhosted.org/packages/47/b9/d706d06dd605c49b9f83a2aed8c13e3e5db70697d7a80b7e3d7915de6b17/matplotlib-3.10.9-cp313-cp313-win_arm64.whl", hash = "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8", size = 8136913, upload-time = "2026-04-24T00:12:56.501Z" }, + { url = "https://files.pythonhosted.org/packages/9b/45/6e32d96978264c8ca8c4b1010adb955a1a49cfaf314e212bbc8908f04a61/matplotlib-3.10.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9", size = 8368019, upload-time = "2026-04-24T00:12:58.896Z" }, + { url = "https://files.pythonhosted.org/packages/86/0a/c8e3d3bba245f0f7fc424937f8ff7ef77291a36af3edb97ccd78aa93d84f/matplotlib-3.10.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4", size = 8264645, upload-time = "2026-04-24T00:13:01.406Z" }, + { url = "https://files.pythonhosted.org/packages/3d/aa/5bf5a14fe4fed73a4209a155606f8096ff797aad89c6c35179026571133e/matplotlib-3.10.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc", size = 8802194, upload-time = "2026-04-24T00:13:03.702Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5e/b4be852d6bba6fd15893fadf91ff26ae49cb91aac789e95dde9d342e664f/matplotlib-3.10.9-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99", size = 9622684, upload-time = "2026-04-24T00:13:06.647Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/ed428c971139112ef730f62770654d609467346d09d4b62617e1afd68a5a/matplotlib-3.10.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d", size = 9680790, upload-time = "2026-04-24T00:13:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/e7/09/052e884aaf2b985c63cb79f715f1d5b6a3eaa7de78f6a52b9dbc077d5b53/matplotlib-3.10.9-cp313-cp313t-win_amd64.whl", hash = "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8", size = 8287571, upload-time = "2026-04-24T00:13:13.087Z" }, + { url = "https://files.pythonhosted.org/packages/f4/38/ae27288e788c35a4250491422f3db7750366fc8c97d6f36fbdecfc1f5518/matplotlib-3.10.9-cp313-cp313t-win_arm64.whl", hash = "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38", size = 8188292, upload-time = "2026-04-24T00:13:15.546Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e6/3bd8afd04949f02eabc1c17115ea5255e19cacd4d06fc5abdde4eeb0052c/matplotlib-3.10.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d", size = 8321276, upload-time = "2026-04-24T00:13:18.318Z" }, + { url = "https://files.pythonhosted.org/packages/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f", size = 8218218, upload-time = "2026-04-24T00:13:20.974Z" }, + { url = "https://files.pythonhosted.org/packages/85/8f/becc9722cafc64f5d2eb0b7c1bf5f585271c618a45dbd8fabeb021f898b6/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b", size = 9608145, upload-time = "2026-04-24T00:13:23.228Z" }, + { url = "https://files.pythonhosted.org/packages/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2", size = 9885085, upload-time = "2026-04-24T00:13:25.849Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fd/fa69f2221534e80cc5772ac2b7d222011a2acafc2ec7216d5dd174c864ae/matplotlib-3.10.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716", size = 9672358, upload-time = "2026-04-24T00:13:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-win_amd64.whl", hash = "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f", size = 8349970, upload-time = "2026-04-24T00:13:31.904Z" }, + { url = "https://files.pythonhosted.org/packages/64/dc/95d60ecaefe30680a154b52ea96ab4b0dab547f1fd6aa12f5fb655e89cae/matplotlib-3.10.9-cp314-cp314-win_arm64.whl", hash = "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456", size = 8272785, upload-time = "2026-04-24T00:13:34.511Z" }, + { url = "https://files.pythonhosted.org/packages/70/a0/005d68bc8b8418300ce6591f18586910a8526806e2ab663933d9f20a41e9/matplotlib-3.10.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe", size = 8367999, upload-time = "2026-04-24T00:13:36.962Z" }, + { url = "https://files.pythonhosted.org/packages/22/05/1236cc9290be70b2498af20ca348add76e3fffe7f67b477db5133a84f3ea/matplotlib-3.10.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6", size = 8264543, upload-time = "2026-04-24T00:13:39.851Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c2/071f5a5ff6c5bd63aaaf2f45c811d9bf2ced94bde188d9e1a519e21d0cba/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c", size = 9622800, upload-time = "2026-04-24T00:13:42.296Z" }, + { url = "https://files.pythonhosted.org/packages/95/57/da7d1f10a85624b9e7db68e069dd94e58dc41dbf9463c5921632ecbe3661/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4", size = 9888561, upload-time = "2026-04-24T00:13:45.026Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/ef8d6bb59b0edb6c16c968b70f548aa13b54348972def5aa6ac85df67145/matplotlib-3.10.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf", size = 9680884, upload-time = "2026-04-24T00:13:48.066Z" }, + { url = "https://files.pythonhosted.org/packages/61/1c/d21bfeb9931881ebe96bcfcff27c7ae4b160ae0ec291a714c42641a56d75/matplotlib-3.10.9-cp314-cp314t-win_amd64.whl", hash = "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39", size = 8432333, upload-time = "2026-04-24T00:13:51.008Z" }, + { url = "https://files.pythonhosted.org/packages/78/23/92493c3e6e1b635ccfff146f7b99e674808787915420373ac399283764c2/matplotlib-3.10.9-cp314-cp314t-win_arm64.whl", hash = "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c", size = 8324785, upload-time = "2026-04-24T00:13:53.633Z" }, + { url = "https://files.pythonhosted.org/packages/63/e2/9f66ca6a651a52abfe0d4964ce01439ed34f3f1e119de10ff3a07f403043/matplotlib-3.10.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:42fb814efabe95c06c1994d8ab5a8385f43a249e23badd3ba931d4308e5bca20", size = 8304420, upload-time = "2026-04-24T00:14:04.57Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e8/467c03568218792906aa87b5e7bb379b605e056ed0c74fe00c051786d925/matplotlib-3.10.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f76e640a5268850bfda54b5131b1b1941cc685e42c5fa98ed9f2d64038308cba", size = 8197981, upload-time = "2026-04-24T00:14:07.233Z" }, + { url = "https://files.pythonhosted.org/packages/6f/87/afead29192170917537934c6aff4b008c805fff7b1ccea0c79120d96beda/matplotlib-3.10.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3fc0364dfbe1d07f6d15c5ebd0c5bf89e126916e5a8667dd4a7a6e84c36653d4", size = 8774002, upload-time = "2026-04-24T00:14:09.816Z" }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, +] + +[[package]] +name = "mistune" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/55/d01f0c4b45ade6536c51170b9043db8b2ec6ddf4a35c7ea3f5f559ac935b/mistune-3.2.0.tar.gz", hash = "sha256:708487c8a8cdd99c9d90eb3ed4c3ed961246ff78ac82f03418f5183ab70e398a", size = 95467, upload-time = "2025-12-23T11:36:34.994Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "nbclient" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbformat" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/91/1c1d5a4b9a9ebba2b4e32b8c852c2975c872aec1fe42ab5e516b2cecd193/nbclient-0.10.4.tar.gz", hash = "sha256:1e54091b16e6da39e297b0ece3e10f6f29f4ac4e8ee515d29f8a7099bd6553c9", size = 62554, upload-time = "2025-12-23T07:45:46.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl", hash = "sha256:9162df5a7373d70d606527300a95a975a47c137776cd942e52d9c7e29ff83440", size = 25465, upload-time = "2025-12-23T07:45:44.51Z" }, +] + +[[package]] +name = "nbconvert" +version = "7.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "bleach", extra = ["css"] }, + { name = "defusedxml" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyterlab-pygments" }, + { name = "markupsafe" }, + { name = "mistune" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pandocfilters" }, + { name = "pygments" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/b1/708e53fe2e429c103c6e6e159106bcf0357ac41aa4c28772bd8402339051/nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2", size = 865311, upload-time = "2026-04-08T00:44:14.914Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl", hash = "sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8", size = 261927, upload-time = "2026-04-08T00:44:12.845Z" }, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "notebook" +version = "7.5.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, + { name = "jupyterlab" }, + { name = "jupyterlab-server" }, + { name = "notebook-shim" }, + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/6d/41052c48d6f6349ca0a7c4d1f6a78464de135e6d18f5829ba2510e62184c/notebook-7.5.5.tar.gz", hash = "sha256:dc0bfab0f2372c8278c457423d3256c34154ac2cc76bf20e9925260c461013c3", size = 14169167, upload-time = "2026-03-11T16:32:51.922Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/aa/cbd1deb9f07446241e88f8d5fecccd95b249bca0b4e5482214a4d1714c49/notebook-7.5.5-py3-none-any.whl", hash = "sha256:a7c14dbeefa6592e87f72290ca982e0c10f5bbf3786be2a600fda9da2764a2b8", size = 14578929, upload-time = "2026-03-11T16:32:48.021Z" }, +] + +[[package]] +name = "notebook-shim" +version = "0.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/d2/92fa3243712b9a3e8bafaf60aac366da1cada3639ca767ff4b5b3654ec28/notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb", size = 13167, upload-time = "2024-02-14T23:35:18.353Z" } +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.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/c6/4218570d8c8ecc9704b5157a3348e486e84ef4be0ed3e38218ab473c83d2/numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db", size = 16976799, upload-time = "2026-03-29T13:18:15.438Z" }, + { url = "https://files.pythonhosted.org/packages/dd/92/b4d922c4a5f5dab9ed44e6153908a5c665b71acf183a83b93b690996e39b/numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0", size = 14971552, upload-time = "2026-03-29T13:18:18.606Z" }, + { url = "https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015", size = 5476566, upload-time = "2026-03-29T13:18:21.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/34/b3fdcec6e725409223dd27356bdf5a3c2cc2282e428218ecc9cb7acc9763/numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40", size = 6806482, upload-time = "2026-03-29T13:18:23.634Z" }, + { url = "https://files.pythonhosted.org/packages/68/62/63417c13aa35d57bee1337c67446761dc25ea6543130cf868eace6e8157b/numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d", size = 15973376, upload-time = "2026-03-29T13:18:26.677Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502", size = 16925137, upload-time = "2026-03-29T13:18:30.14Z" }, + { url = "https://files.pythonhosted.org/packages/7e/43/80020edacb3f84b9efdd1591120a4296462c23fd8db0dde1666f6ef66f13/numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd", size = 17329414, upload-time = "2026-03-29T13:18:33.733Z" }, + { url = "https://files.pythonhosted.org/packages/fd/06/af0658593b18a5f73532d377188b964f239eb0894e664a6c12f484472f97/numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5", size = 18658397, upload-time = "2026-03-29T13:18:37.511Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ce/13a09ed65f5d0ce5c7dd0669250374c6e379910f97af2c08c57b0608eee4/numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", size = 6239499, upload-time = "2026-03-29T13:18:40.372Z" }, + { url = "https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", size = 12614257, upload-time = "2026-03-29T13:18:42.95Z" }, + { url = "https://files.pythonhosted.org/packages/87/c5/8168052f080c26fa984c413305012be54741c9d0d74abd7fbeeccae3889f/numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e", size = 10486775, upload-time = "2026-03-29T13:18:45.835Z" }, + { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, + { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, + { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, + { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, + { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, + { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, + { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, + { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, + { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/6b/33/8fae8f964a4f63ed528264ddf25d2b683d0b663e3cba26961eb838a7c1bd/numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4", size = 16854491, upload-time = "2026-03-29T13:21:38.03Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d0/1aabee441380b981cf8cdda3ae7a46aa827d1b5a8cce84d14598bc94d6d9/numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e", size = 14895830, upload-time = "2026-03-29T13:21:41.509Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b8/aafb0d1065416894fccf4df6b49ef22b8db045187949545bced89c034b8e/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c", size = 5400927, upload-time = "2026-03-29T13:21:44.747Z" }, + { url = "https://files.pythonhosted.org/packages/d6/77/063baa20b08b431038c7f9ff5435540c7b7265c78cf56012a483019ca72d/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3", size = 6715557, upload-time = "2026-03-29T13:21:47.406Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a8/379542d45a14f149444c5c4c4e7714707239ce9cc1de8c2803958889da14/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7", size = 15804253, upload-time = "2026-03-29T13:21:50.753Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/f0a45426d6d21e7ea3310a15cf90c43a14d9232c31a837702dba437f3373/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", size = 16753552, upload-time = "2026-03-29T13:21:54.344Z" }, + { url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" }, +] + +[[package]] +name = "nvidia-cublas" +version = "13.1.0.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/a5/fce49e2ae977e0ccc084e5adafceb4f0ac0c8333cb6863501618a7277f67/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c86fc7f7ae36d7528288c5d88098edcb7b02c633d262e7ddbb86b0ad91be5df2", size = 542851226, upload-time = "2025-10-09T08:59:04.818Z" }, + { url = "https://files.pythonhosted.org/packages/e7/44/423ac00af4dd95a5aeb27207e2c0d9b7118702149bf4704c3ddb55bb7429/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:ee8722c1f0145ab246bccb9e452153b5e0515fd094c3678df50b2a0888b8b171", size = 423133236, upload-time = "2025-10-09T08:59:32.536Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.0.96" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu13" +version = "9.19.0.56" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/84/26025437c1e6b61a707442184fa0c03d083b661adf3a3eecfd6d21677740/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:6ed29ffaee1176c612daf442e4dd6cfeb6a0caa43ddcbeb59da94953030b1be4", size = 433781201, upload-time = "2026-02-03T20:40:53.805Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/0b4b932655d17a6da1b92fa92ab12844b053bb2ac2475e179ba6f043da1e/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:d20e1734305e9d68889a96e3f35094d733ff1f83932ebe462753973e53a572bf", size = 366066321, upload-time = "2026-02-03T20:44:52.837Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, +] + +[[package]] +name = "nvidia-cufile" +version = "1.15.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, +] + +[[package]] +name = "nvidia-curand" +version = "10.4.0.35" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, +] + +[[package]] +name = "nvidia-cusolver" +version = "12.0.4.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "sys_platform != 'win32'" }, + { name = "nvidia-cusparse", marker = "sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, +] + +[[package]] +name = "nvidia-cusparse" +version = "12.6.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu13" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/10/8dcd1175260706a2fc92a16a52e306b71d4c1ea0b0cc4a9484183399818a/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:400c6ed1cf6780fc6efedd64ec9f1345871767e6a1a0a552a1ea0578117ea77c", size = 220791277, upload-time = "2025-08-13T19:22:40.982Z" }, + { url = "https://files.pythonhosted.org/packages/fd/53/43b0d71f4e702fa9733f8b4571fdca50a8813f1e450b656c239beff12315/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:25e30a8a7323935d4ad0340b95a0b69926eee755767e8e0b1cf8dd85b197d3fd", size = 169884119, upload-time = "2025-08-13T19:23:41.967Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.28.9" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/55/1920646a2e43ffd4fc958536b276197ed740e9e0c54105b4bb3521591fc7/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:01c873ba1626b54caa12272ed228dc5b2781545e0ae8ba3f432a8ef1c6d78643", size = 196561677, upload-time = "2025-11-18T05:49:03.45Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b4/878fefaad5b2bcc6fcf8d474a25e3e3774bc5133e4b58adff4d0bca238bc/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:e4553a30f34195f3fa1da02a6da3d6337d28f2003943aa0a3d247bbc25fefc42", size = 196493177, upload-time = "2025-11-18T05:49:17.677Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu13" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, +] + +[[package]] +name = "nvidia-nvtx" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, +] + +[[package]] +name = "opencv-python" +version = "4.13.0.92" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/6f/5a28fef4c4a382be06afe3938c64cc168223016fa520c5abaf37e8862aa5/opencv_python-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:caf60c071ec391ba51ed00a4a920f996d0b64e3e46068aac1f646b5de0326a19", size = 46247052, upload-time = "2026-02-05T07:01:25.046Z" }, + { url = "https://files.pythonhosted.org/packages/08/ac/6c98c44c650b8114a0fb901691351cfb3956d502e8e9b5cd27f4ee7fbf2f/opencv_python-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:5868a8c028a0b37561579bfb8ac1875babdc69546d236249fff296a8c010ccf9", size = 32568781, upload-time = "2026-02-05T07:01:41.379Z" }, + { url = "https://files.pythonhosted.org/packages/3e/51/82fed528b45173bf629fa44effb76dff8bc9f4eeaee759038362dfa60237/opencv_python-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bc2596e68f972ca452d80f444bc404e08807d021fbba40df26b61b18e01838a", size = 47685527, upload-time = "2026-02-05T06:59:11.24Z" }, + { url = "https://files.pythonhosted.org/packages/db/07/90b34a8e2cf9c50fe8ed25cac9011cde0676b4d9d9c973751ac7616223a2/opencv_python-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:402033cddf9d294693094de5ef532339f14ce821da3ad7df7c9f6e8316da32cf", size = 70460872, upload-time = "2026-02-05T06:59:19.162Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/7a9cc719b3eaf4377b9c2e3edeb7ed3a81de41f96421510c0a169ca3cfd4/opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bccaabf9eb7f897ca61880ce2869dcd9b25b72129c28478e7f2a5e8dee945616", size = 46708208, upload-time = "2026-02-05T06:59:15.419Z" }, + { url = "https://files.pythonhosted.org/packages/fd/55/b3b49a1b97aabcfbbd6c7326df9cb0b6fa0c0aefa8e89d500939e04aa229/opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:620d602b8f7d8b8dab5f4b99c6eb353e78d3fb8b0f53db1bd258bb1aa001c1d5", size = 72927042, upload-time = "2026-02-05T06:59:23.389Z" }, + { url = "https://files.pythonhosted.org/packages/fb/17/de5458312bcb07ddf434d7bfcb24bb52c59635ad58c6e7c751b48949b009/opencv_python-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:372fe164a3148ac1ca51e5f3ad0541a4a276452273f503441d718fab9c5e5f59", size = 30932638, upload-time = "2026-02-05T07:02:14.98Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a5/1be1516390333ff9be3a9cb648c9f33df79d5096e5884b5df71a588af463/opencv_python-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:423d934c9fafb91aad38edf26efb46da91ffbc05f3f59c4b0c72e699720706f5", size = 40212062, upload-time = "2026-02-05T07:02:12.724Z" }, +] + +[[package]] +name = "overrides" +version = "7.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/35/6411db530c618e0e0005187e35aa02ce60ae4c4c4d206964a2f978217c27/pandas-3.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a727a73cbdba2f7458dc82449e2315899d5140b449015d822f515749a46cbbe0", size = 10326926, upload-time = "2026-03-31T06:46:08.29Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d3/b7da1d5d7dbdc5ef52ed7debd2b484313b832982266905315dad5a0bf0b1/pandas-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbbd4aa20ca51e63b53bbde6a0fa4254b1aaabb74d2f542df7a7959feb1d760c", size = 9926987, upload-time = "2026-03-31T06:46:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/52/77/9b1c2d6070b5dbe239a7bc889e21bfa58720793fb902d1e070695d87c6d0/pandas-3.0.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:339dda302bd8369dedeae979cb750e484d549b563c3f54f3922cb8ff4978c5eb", size = 10757067, upload-time = "2026-03-31T06:46:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/20/17/ec40d981705654853726e7ac9aea9ddbb4a5d9cf54d8472222f4f3de06c2/pandas-3.0.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61c2fd96d72b983a9891b2598f286befd4ad262161a609c92dc1652544b46b76", size = 11258787, upload-time = "2026-03-31T06:46:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/90/e3/3f1126d43d3702ca8773871a81c9f15122a1f412342cc56284ffda5b1f70/pandas-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c934008c733b8bbea273ea308b73b3156f0181e5b72960790b09c18a2794fe1e", size = 11771616, upload-time = "2026-03-31T06:46:20.532Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cf/0f4e268e1f5062e44a6bda9f925806721cd4c95c2b808a4c82ebe914f96b/pandas-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:60a80bb4feacbef5e1447a3f82c33209c8b7e07f28d805cfd1fb951e5cb443aa", size = 12337623, upload-time = "2026-03-31T06:46:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/97a6339859d4acb2536efb24feb6708e82f7d33b2ed7e036f2983fcced82/pandas-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:ed72cb3f45190874eb579c64fa92d9df74e98fd63e2be7f62bce5ace0ade61df", size = 9897372, upload-time = "2026-03-31T06:46:26.703Z" }, + { url = "https://files.pythonhosted.org/packages/8f/eb/781516b808a99ddf288143cec46b342b3016c3414d137da1fdc3290d8860/pandas-3.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:f12b1a9e332c01e09510586f8ca9b108fd631fd656af82e452d7315ef6df5f9f", size = 9154922, upload-time = "2026-03-31T06:46:30.284Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b0/c20bd4d6d3f736e6bd6b55794e9cd0a617b858eaad27c8f410ea05d953b7/pandas-3.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:232a70ebb568c0c4d2db4584f338c1577d81e3af63292208d615907b698a0f18", size = 10347921, upload-time = "2026-03-31T06:46:33.36Z" }, + { url = "https://files.pythonhosted.org/packages/35/d0/4831af68ce30cc2d03c697bea8450e3225a835ef497d0d70f31b8cdde965/pandas-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14", size = 9888127, upload-time = "2026-03-31T06:46:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/61/a9/16ea9346e1fc4a96e2896242d9bc674764fb9049b0044c0132502f7a771e/pandas-3.0.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aff4e6f4d722e0652707d7bcb190c445fe58428500c6d16005b02401764b1b3d", size = 10399577, upload-time = "2026-03-31T06:46:39.224Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a8/3a61a721472959ab0ce865ef05d10b0d6bfe27ce8801c99f33d4fa996e65/pandas-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef8b27695c3d3dc78403c9a7d5e59a62d5464a7e1123b4e0042763f7104dc74f", size = 10880030, upload-time = "2026-03-31T06:46:42.412Z" }, + { url = "https://files.pythonhosted.org/packages/da/65/7225c0ea4d6ce9cb2160a7fb7f39804871049f016e74782e5dade4d14109/pandas-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f8d68083e49e16b84734eb1a4dcae4259a75c90fb6e2251ab9a00b61120c06ab", size = 11409468, upload-time = "2026-03-31T06:46:45.2Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/46e7c76032639f2132359b5cf4c785dd8cf9aea5ea64699eac752f02b9db/pandas-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:32cc41f310ebd4a296d93515fcac312216adfedb1894e879303987b8f1e2b97d", size = 11936381, upload-time = "2026-03-31T06:46:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/7b/8b/721a9cff6fa6a91b162eb51019c6243b82b3226c71bb6c8ef4a9bd65cbc6/pandas-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4", size = 9744993, upload-time = "2026-03-31T06:46:51.488Z" }, + { url = "https://files.pythonhosted.org/packages/d5/18/7f0bd34ae27b28159aa80f2a6799f47fda34f7fb938a76e20c7b7fe3b200/pandas-3.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:08504503f7101300107ecdc8df73658e4347586db5cfdadabc1592e9d7e7a0fd", size = 9056118, upload-time = "2026-03-31T06:46:54.548Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ca/3e639a1ea6fcd0617ca4e8ca45f62a74de33a56ae6cd552735470b22c8d3/pandas-3.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5918ba197c951dec132b0c5929a00c0bf05d5942f590d3c10a807f6e15a57d3", size = 10321105, upload-time = "2026-03-31T06:46:57.327Z" }, + { url = "https://files.pythonhosted.org/packages/0b/77/dbc82ff2fb0e63c6564356682bf201edff0ba16c98630d21a1fb312a8182/pandas-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d606a041c89c0a474a4702d532ab7e73a14fe35c8d427b972a625c8e46373668", size = 9864088, upload-time = "2026-03-31T06:46:59.935Z" }, + { url = "https://files.pythonhosted.org/packages/5c/2b/341f1b04bbca2e17e13cd3f08c215b70ef2c60c5356ef1e8c6857449edc7/pandas-3.0.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:710246ba0616e86891b58ab95f2495143bb2bc83ab6b06747c74216f583a6ac9", size = 10369066, upload-time = "2026-03-31T06:47:02.792Z" }, + { url = "https://files.pythonhosted.org/packages/12/c5/cbb1ffefb20a93d3f0e1fdcda699fb84976210d411b008f97f48bf6ce27e/pandas-3.0.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d3cfe227c725b1f3dff4278b43d8c784656a42a9325b63af6b1492a8232209e", size = 10876780, upload-time = "2026-03-31T06:47:06.205Z" }, + { url = "https://files.pythonhosted.org/packages/98/fe/2249ae5e0a69bd0ddf17353d0a5d26611d70970111f5b3600cdc8be883e7/pandas-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c3b723df9087a9a9a840e263ebd9f88b64a12075d1bf2ea401a5a42f254f084d", size = 11375181, upload-time = "2026-03-31T06:47:09.383Z" }, + { url = "https://files.pythonhosted.org/packages/de/64/77a38b09e70b6464883b8d7584ab543e748e42c1b5d337a2ee088e0df741/pandas-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3096110bf9eac0070b7208465f2740e2d8a670d5cb6530b5bb884eca495fd39", size = 11928899, upload-time = "2026-03-31T06:47:12.686Z" }, + { url = "https://files.pythonhosted.org/packages/5e/52/42855bf626868413f761addd574acc6195880ae247a5346477a4361c3acb/pandas-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:07a10f5c36512eead51bc578eb3354ad17578b22c013d89a796ab5eee90cd991", size = 9746574, upload-time = "2026-03-31T06:47:15.64Z" }, + { url = "https://files.pythonhosted.org/packages/88/39/21304ae06a25e8bf9fc820d69b29b2c495b2ae580d1e143146c309941760/pandas-3.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:5fdbfa05931071aba28b408e59226186b01eb5e92bea2ab78b65863ca3228d84", size = 9047156, upload-time = "2026-03-31T06:47:18.595Z" }, + { url = "https://files.pythonhosted.org/packages/72/20/7defa8b27d4f330a903bb68eea33be07d839c5ea6bdda54174efcec0e1d2/pandas-3.0.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:dbc20dea3b9e27d0e66d74c42b2d0c1bed9c2ffe92adea33633e3bedeb5ac235", size = 10756238, upload-time = "2026-03-31T06:47:22.012Z" }, + { url = "https://files.pythonhosted.org/packages/e9/95/49433c14862c636afc0e9b2db83ff16b3ad92959364e52b2955e44c8e94c/pandas-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b75c347eff42497452116ce05ef461822d97ce5b9ff8df6edacb8076092c855d", size = 10408520, upload-time = "2026-03-31T06:47:25.197Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f8/462ad2b5881d6b8ec8e5f7ed2ea1893faa02290d13870a1600fe72ad8efc/pandas-3.0.2-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1478075142e83a5571782ad007fb201ed074bdeac7ebcc8890c71442e96adf7", size = 10324154, upload-time = "2026-03-31T06:47:28.097Z" }, + { url = "https://files.pythonhosted.org/packages/0a/65/d1e69b649cbcddda23ad6e4c40ef935340f6f652a006e5cbc3555ac8adb3/pandas-3.0.2-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5880314e69e763d4c8b27937090de570f1fb8d027059a7ada3f7f8e98bdcb677", size = 10714449, upload-time = "2026-03-31T06:47:30.85Z" }, + { url = "https://files.pythonhosted.org/packages/47/a4/85b59bc65b8190ea3689882db6cdf32a5003c0ccd5a586c30fdcc3ffc4fc/pandas-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b5329e26898896f06035241a626d7c335daa479b9bbc82be7c2742d048e41172", size = 11338475, upload-time = "2026-03-31T06:47:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c4/bc6966c6e38e5d9478b935272d124d80a589511ed1612a5d21d36f664c68/pandas-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:81526c4afd31971f8b62671442a4b2b51e0aa9acc3819c9f0f12a28b6fcf85f1", size = 11786568, upload-time = "2026-03-31T06:47:36.941Z" }, + { url = "https://files.pythonhosted.org/packages/e8/74/09298ca9740beed1d3504e073d67e128aa07e5ca5ca2824b0c674c0b8676/pandas-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:7cadd7e9a44ec13b621aec60f9150e744cfc7a3dd32924a7e2f45edff31823b0", size = 10488652, upload-time = "2026-03-31T06:47:40.612Z" }, + { url = "https://files.pythonhosted.org/packages/bb/40/c6ea527147c73b24fc15c891c3fcffe9c019793119c5742b8784a062c7db/pandas-3.0.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db0dbfd2a6cdf3770aa60464d50333d8f3d9165b2f2671bcc299b72de5a6677b", size = 10326084, upload-time = "2026-03-31T06:47:43.834Z" }, + { url = "https://files.pythonhosted.org/packages/95/25/bdb9326c3b5455f8d4d3549fce7abcf967259de146fe2cf7a82368141948/pandas-3.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0555c5882688a39317179ab4a0ed41d3ebc8812ab14c69364bbee8fb7a3f6288", size = 9914146, upload-time = "2026-03-31T06:47:46.67Z" }, + { url = "https://files.pythonhosted.org/packages/8d/77/3a227ff3337aa376c60d288e1d61c5d097131d0ac71f954d90a8f369e422/pandas-3.0.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01f31a546acd5574ef77fe199bc90b55527c225c20ccda6601cf6b0fd5ed597c", size = 10444081, upload-time = "2026-03-31T06:47:49.681Z" }, + { url = "https://files.pythonhosted.org/packages/15/88/3cdd54fa279341afa10acf8d2b503556b1375245dccc9315659f795dd2e9/pandas-3.0.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:deeca1b5a931fdf0c2212c8a659ade6d3b1edc21f0914ce71ef24456ca7a6535", size = 10897535, upload-time = "2026-03-31T06:47:53.033Z" }, + { url = "https://files.pythonhosted.org/packages/06/9d/98cc7a7624f7932e40f434299260e2917b090a579d75937cb8a57b9d2de3/pandas-3.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f48afd9bb13300ffb5a3316973324c787054ba6665cda0da3fbd67f451995db", size = 11446992, upload-time = "2026-03-31T06:47:56.193Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cd/19ff605cc3760e80602e6826ddef2824d8e7050ed80f2e11c4b079741dc3/pandas-3.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6c4d8458b97a35717b62469a4ea0e85abd5ed8687277f5ccfc67f8a5126f8c53", size = 11968257, upload-time = "2026-03-31T06:47:59.137Z" }, + { url = "https://files.pythonhosted.org/packages/db/60/aba6a38de456e7341285102bede27514795c1eaa353bc0e7638b6b785356/pandas-3.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:b35d14bb5d8285d9494fe93815a9e9307c0876e10f1e8e89ac5b88f728ec8dcf", size = 9865893, upload-time = "2026-03-31T06:48:02.038Z" }, + { url = "https://files.pythonhosted.org/packages/08/71/e5ec979dd2e8a093dacb8864598c0ff59a0cee0bbcdc0bfec16a51684d4f/pandas-3.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:63d141b56ef686f7f0d714cfb8de4e320475b86bf4b620aa0b7da89af8cbdbbb", size = 9188644, upload-time = "2026-03-31T06:48:05.045Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6c/7b45d85db19cae1eb524f2418ceaa9d85965dcf7b764ed151386b7c540f0/pandas-3.0.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:140f0cffb1fa2524e874dde5b477d9defe10780d8e9e220d259b2c0874c89d9d", size = 10776246, upload-time = "2026-03-31T06:48:07.789Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3e/7b00648b086c106e81766f25322b48aa8dfa95b55e621dbdf2fdd413a117/pandas-3.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae37e833ff4fed0ba352f6bdd8b73ba3ab3256a85e54edfd1ab51ae40cca0af8", size = 10424801, upload-time = "2026-03-31T06:48:10.897Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/558dd09a71b53b4008e7fc8a98ec6d447e9bfb63cdaeea10e5eb9b2dabe8/pandas-3.0.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d888a5c678a419a5bb41a2a93818e8ed9fd3172246555c0b37b7cc27027effd", size = 10345643, upload-time = "2026-03-31T06:48:13.7Z" }, + { url = "https://files.pythonhosted.org/packages/be/e3/921c93b4d9a280409451dc8d07b062b503bbec0531d2627e73a756e99a82/pandas-3.0.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b444dc64c079e84df91baa8bf613d58405645461cabca929d9178f2cd392398d", size = 10743641, upload-time = "2026-03-31T06:48:16.659Z" }, + { url = "https://files.pythonhosted.org/packages/56/ca/fd17286f24fa3b4d067965d8d5d7e14fe557dd4f979a0b068ac0deaf8228/pandas-3.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4544c7a54920de8eeacaa1466a6b7268ecfbc9bc64ab4dbb89c6bbe94d5e0660", size = 11361993, upload-time = "2026-03-31T06:48:19.475Z" }, + { url = "https://files.pythonhosted.org/packages/e4/a5/2f6ed612056819de445a433ca1f2821ac3dab7f150d569a59e9cc105de1d/pandas-3.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:734be7551687c00fbd760dc0522ed974f82ad230d4a10f54bf51b80d44a08702", size = 11815274, upload-time = "2026-03-31T06:48:22.695Z" }, + { url = "https://files.pythonhosted.org/packages/00/2f/b622683e99ec3ce00b0854bac9e80868592c5b051733f2cf3a868e5fea26/pandas-3.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:57a07209bebcbcf768d2d13c9b78b852f9a15978dac41b9e6421a81ad4cdd276", size = 10888530, upload-time = "2026-03-31T06:48:25.806Z" }, + { url = "https://files.pythonhosted.org/packages/cb/2b/f8434233fab2bd66a02ec014febe4e5adced20e2693e0e90a07d118ed30e/pandas-3.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:5371b72c2d4d415d08765f32d689217a43227484e81b2305b52076e328f6f482", size = 9455341, upload-time = "2026-03-31T06:48:28.418Z" }, +] + +[[package]] +name = "pandocfilters" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" }, +] + +[[package]] +name = "parso" +version = "0.8.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess", marker = "sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab", size = 5354347, upload-time = "2026-04-01T14:42:44.255Z" }, + { url = "https://files.pythonhosted.org/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65", size = 4695873, upload-time = "2026-04-01T14:42:46.452Z" }, + { url = "https://files.pythonhosted.org/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7", size = 6280168, upload-time = "2026-04-01T14:42:49.228Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e", size = 8088188, upload-time = "2026-04-01T14:42:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705", size = 6394401, upload-time = "2026-04-01T14:42:54.343Z" }, + { url = "https://files.pythonhosted.org/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176", size = 7079655, upload-time = "2026-04-01T14:42:56.954Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b", size = 6503105, upload-time = "2026-04-01T14:42:59.847Z" }, + { url = "https://files.pythonhosted.org/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909", size = 7203402, upload-time = "2026-04-01T14:43:02.664Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808", size = 6378149, upload-time = "2026-04-01T14:43:05.274Z" }, + { url = "https://files.pythonhosted.org/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60", size = 7082626, upload-time = "2026-04-01T14:43:08.557Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe", size = 2463531, upload-time = "2026-04-01T14:43:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f", size = 5273969, upload-time = "2026-04-01T14:45:55.538Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d", size = 4659674, upload-time = "2026-04-01T14:45:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f", size = 5288479, upload-time = "2026-04-01T14:46:01.141Z" }, + { url = "https://files.pythonhosted.org/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e", size = 7032230, upload-time = "2026-04-01T14:46:03.874Z" }, + { url = "https://files.pythonhosted.org/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0", size = 5355404, upload-time = "2026-04-01T14:46:06.33Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1", size = 6002215, upload-time = "2026-04-01T14:46:08.83Z" }, + { url = "https://files.pythonhosted.org/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e", size = 7080946, upload-time = "2026-04-01T14:46:11.734Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "prometheus-client" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/fb/d9aa83ffe43ce1f19e557c0971d04b90561b0cfd50762aafb01968285553/prometheus_client-0.25.0.tar.gz", hash = "sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28", size = 86035, upload-time = "2026-04-09T19:53:42.359Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/9b/d4b1e644385499c8346fa9b622a3f030dce14cd6ef8a1871c221a17a67e7/prometheus_client-0.25.0-py3-none-any.whl", hash = "sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1", size = 64154, upload-time = "2026-04-09T19:53:41.324Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + +[[package]] +name = "py-cpuinfo" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" }, +] + +[[package]] +name = "pyaml" +version = "26.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/fb/2b9590512a9d7763620d87171c7531d5295678ce96e57393614b91da8998/pyaml-26.2.1.tar.gz", hash = "sha256:489dd82997235d4cfcf76a6287fce2f075487d77a6567c271e8d790583690c68", size = 30653, upload-time = "2026-02-06T13:49:30.769Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/f3/1f8651f23101e6fae41d0d504414c9722b0140bf0fc6acf87ac52e18aa41/pyaml-26.2.1-py3-none-any.whl", hash = "sha256:6261c2f0a2f33245286c794ad6ec234be33a73d2b05427079fd343e2812a87cf", size = 27211, upload-time = "2026-02-06T13:49:29.652Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pyremotedata" +version = "0.1.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/05/f3530ee8d90e3339699554c8eec707ad5264c1251802f075edfdb1cf3bb6/pyremotedata-0.1.13.tar.gz", hash = "sha256:c1c20ee9aa11630d5926eb67ba39200cd3f1fb8de2002d34f134474ef9229869", size = 87261, upload-time = "2026-04-16T13:34:06.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/2a/917b538610294ab56afca0fb9111930c0d1ffb6ee2b0d40cbc33db0193fc/pyremotedata-0.1.13-py3-none-any.whl", hash = "sha256:488b2e33c6c28155e91cb98e27c87627a87c44d89ab945684d59b8d308fe8f33", size = 29055, upload-time = "2026-04-16T13:34:07.673Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-json-logger" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/ff/3cc9165fd44106973cd7ac9facb674a65ed853494592541d339bdc9a30eb/python_json_logger-4.1.0.tar.gz", hash = "sha256:b396b9e3ed782b09ff9d6e4f1683d46c83ad0d35d2e407c09a9ebbf038f88195", size = 17573, upload-time = "2026-03-29T04:39:56.805Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/be/0631a861af4d1c875f096c07d34e9a63639560a717130e7a87cbc82b7e3f/python_json_logger-4.1.0-py3-none-any.whl", hash = "sha256:132994765cf75bf44554be9aa49b06ef2345d23661a96720262716438141b6b2", size = 15021, upload-time = "2026-03-29T04:39:55.266Z" }, +] + +[[package]] +name = "pywinpty" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/54/37c7370ba91f579235049dc26cd2c5e657d2a943e01820844ffc81f32176/pywinpty-3.0.3.tar.gz", hash = "sha256:523441dc34d231fb361b4b00f8c99d3f16de02f5005fd544a0183112bcc22412", size = 31309, upload-time = "2026-02-04T21:51:09.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/c3/3e75075c7f71735f22b66fab0481f2c98e3a4d58cba55cb50ba29114bcf6/pywinpty-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:dff25a9a6435f527d7c65608a7e62783fc12076e7d44487a4911ee91be5a8ac8", size = 2114430, upload-time = "2026-02-04T21:54:19.485Z" }, + { url = "https://files.pythonhosted.org/packages/8d/1e/8a54166a8c5e4f5cb516514bdf4090be4d51a71e8d9f6d98c0aa00fe45d4/pywinpty-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:fbc1e230e5b193eef4431cba3f39996a288f9958f9c9f092c8a961d930ee8f68", size = 236191, upload-time = "2026-02-04T21:50:36.239Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d4/aeb5e1784d2c5bff6e189138a9ca91a090117459cea0c30378e1f2db3d54/pywinpty-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c9081df0e49ffa86d15db4a6ba61530630e48707f987df42c9d3313537e81fc0", size = 2113098, upload-time = "2026-02-04T21:54:37.711Z" }, + { url = "https://files.pythonhosted.org/packages/b9/53/7278223c493ccfe4883239cf06c823c56460a8010e0fc778eef67858dc14/pywinpty-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:15e79d870e18b678fb8a5a6105fd38496b55697c66e6fc0378236026bc4d59e9", size = 234901, upload-time = "2026-02-04T21:53:31.35Z" }, + { url = "https://files.pythonhosted.org/packages/e5/cb/58d6ed3fd429c96a90ef01ac9a617af10a6d41469219c25e7dc162abbb71/pywinpty-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9c91dbb026050c77bdcef964e63a4f10f01a639113c4d3658332614544c467ab", size = 2112686, upload-time = "2026-02-04T21:52:03.035Z" }, + { url = "https://files.pythonhosted.org/packages/fd/50/724ed5c38c504d4e58a88a072776a1e880d970789deaeb2b9f7bd9a5141a/pywinpty-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:fe1f7911805127c94cf51f89ab14096c6f91ffdcacf993d2da6082b2142a2523", size = 234591, upload-time = "2026-02-04T21:52:29.821Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ad/90a110538696b12b39fd8758a06d70ded899308198ad2305ac68e361126e/pywinpty-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:3f07a6cf1c1d470d284e614733c3d0f726d2c85e78508ea10a403140c3c0c18a", size = 2112360, upload-time = "2026-02-04T21:55:33.397Z" }, + { url = "https://files.pythonhosted.org/packages/44/0f/7ffa221757a220402bc79fda44044c3f2cc57338d878ab7d622add6f4581/pywinpty-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:15c7c0b6f8e9d87aabbaff76468dabf6e6121332c40fc1d83548d02a9d6a3759", size = 233107, upload-time = "2026-02-04T21:51:45.455Z" }, + { url = "https://files.pythonhosted.org/packages/28/88/2ff917caff61e55f38bcdb27de06ee30597881b2cae44fbba7627be015c4/pywinpty-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:d4b6b7b0fe0cdcd02e956bd57cfe9f4e5a06514eecf3b5ae174da4f951b58be9", size = 2113282, upload-time = "2026-02-04T21:52:08.188Z" }, + { url = "https://files.pythonhosted.org/packages/63/32/40a775343ace542cc43ece3f1d1fce454021521ecac41c4c4573081c2336/pywinpty-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:34789d685fc0d547ce0c8a65e5a70e56f77d732fa6e03c8f74fefb8cbb252019", size = 234207, upload-time = "2026-02-04T21:51:58.687Z" }, + { url = "https://files.pythonhosted.org/packages/8d/54/5d5e52f4cb75028104ca6faf36c10f9692389b1986d34471663b4ebebd6d/pywinpty-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:0c37e224a47a971d1a6e08649a1714dac4f63c11920780977829ed5c8cadead1", size = 2112910, upload-time = "2026-02-04T21:52:30.976Z" }, + { url = "https://files.pythonhosted.org/packages/0a/44/dcd184824e21d4620b06c7db9fbb15c3ad0a0f1fa2e6de79969fb82647ec/pywinpty-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c4e9c3dff7d86ba81937438d5819f19f385a39d8f592d4e8af67148ceb4f6ab5", size = 233425, upload-time = "2026-02-04T21:51:56.754Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, + { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, +] + +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, +] + +[[package]] +name = "rfc3986-validator" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/88/f270de456dd7d11dcc808abfa291ecdd3f45ff44e3b549ffa01b126464d0/rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055", size = 6760, upload-time = "2019-10-28T16:00:19.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", size = 4242, upload-time = "2019-10-28T16:00:13.976Z" }, +] + +[[package]] +name = "rfc3987-syntax" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lark" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/06/37c1a5557acf449e8e406a830a05bf885ac47d33270aec454ef78675008d/rfc3987_syntax-1.1.0.tar.gz", hash = "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d", size = 14239, upload-time = "2025-07-18T01:05:05.015Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/29/af14f4ef3c11a50435308660e2cc68761c9a7742475e0585cd4396b91777/s3transfer-0.16.1.tar.gz", hash = "sha256:8e424355754b9ccb32467bdc568edf55be82692ef2002d934b1311dbb3b9e524", size = 154801, upload-time = "2026-04-22T20:36:06.475Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/19/90d7d4ed51932c022d53f1d02d564b62d10e272692a1f9b76425c1ad2a02/s3transfer-0.16.1-py3-none-any.whl", hash = "sha256:61bcd00ccb83b21a0fe7e91a553fff9729d46c83b4e0106e7c314a733891f7c2", size = 86825, upload-time = "2026-04-22T20:36:04.992Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/92/53ea2181da8ac6bf27170191028aee7251f8f841f8d3edbfdcaf2008fde9/scikit_learn-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:146b4d36f800c013d267b29168813f7a03a43ecd2895d04861f1240b564421da", size = 8595835, upload-time = "2025-12-10T07:07:39.385Z" }, + { url = "https://files.pythonhosted.org/packages/01/18/d154dc1638803adf987910cdd07097d9c526663a55666a97c124d09fb96a/scikit_learn-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f984ca4b14914e6b4094c5d52a32ea16b49832c03bd17a110f004db3c223e8e1", size = 8080381, upload-time = "2025-12-10T07:07:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/44/226142fcb7b7101e64fdee5f49dbe6288d4c7af8abf593237b70fca080a4/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e30adb87f0cc81c7690a84f7932dd66be5bac57cfe16b91cb9151683a4a2d3b", size = 8799632, upload-time = "2025-12-10T07:07:43.899Z" }, + { url = "https://files.pythonhosted.org/packages/36/4d/4a67f30778a45d542bbea5db2dbfa1e9e100bf9ba64aefe34215ba9f11f6/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ada8121bcb4dac28d930febc791a69f7cb1673c8495e5eee274190b73a4559c1", size = 9103788, upload-time = "2025-12-10T07:07:45.982Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/45c352094cfa60050bcbb967b1faf246b22e93cb459f2f907b600f2ceda5/scikit_learn-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:c57b1b610bd1f40ba43970e11ce62821c2e6569e4d74023db19c6b26f246cb3b", size = 8081706, upload-time = "2025-12-10T07:07:48.111Z" }, + { url = "https://files.pythonhosted.org/packages/3d/46/5416595bb395757f754feb20c3d776553a386b661658fb21b7c814e89efe/scikit_learn-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:2838551e011a64e3053ad7618dda9310175f7515f1742fa2d756f7c874c05961", size = 7688451, upload-time = "2025-12-10T07:07:49.873Z" }, + { url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" }, + { url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" }, + { url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" }, + { url = "https://files.pythonhosted.org/packages/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770, upload-time = "2025-12-10T07:08:03.251Z" }, + { url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458, upload-time = "2025-12-10T07:08:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341, upload-time = "2025-12-10T07:08:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022, upload-time = "2025-12-10T07:08:09.862Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409, upload-time = "2025-12-10T07:08:12.028Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760, upload-time = "2025-12-10T07:08:13.688Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045, upload-time = "2025-12-10T07:08:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324, upload-time = "2025-12-10T07:08:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651, upload-time = "2025-12-10T07:08:19.952Z" }, + { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" }, + { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" }, + { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" }, +] + +[[package]] +name = "scikit-optimize" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyaml" }, + { name = "scikit-learn" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/95/1b433b9eb9eb653fb97fd525552fd027886e3812d7d20d843994263340aa/scikit_optimize-0.10.2.tar.gz", hash = "sha256:00a3d91bf9015e292b6e7aaefe7e6cb95e8d25ce19adafd2cd88849e1a0b0da0", size = 86202, upload-time = "2024-06-04T19:12:56.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/cd/15c9ebea645cc9860aa71fe0474f4be981f10ed8e19e1fb0ef1027d4966e/scikit_optimize-0.10.2-py2.py3-none-any.whl", hash = "sha256:45bc7e879b086133984721f2f6735a86c085073f6c481c2ec665b5c67b44d723", size = 107794, upload-time = "2024-06-04T19:12:54.592Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, + { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "seaborn" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy" }, + { name = "pandas" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696, upload-time = "2024-01-25T13:21:52.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914, upload-time = "2024-01-25T13:21:49.598Z" }, +] + +[[package]] +name = "send2trash" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/f0/184b4b5f8d00f2a92cf96eec8967a3d550b52cf94362dad1100df9e48d57/send2trash-2.1.0.tar.gz", hash = "sha256:1c72b39f09457db3c05ce1d19158c2cbef4c32b8bedd02c155e49282b7ea7459", size = 17255, upload-time = "2026-01-14T06:27:36.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl", hash = "sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c", size = 17610, upload-time = "2026-01-14T06:27:35.218Z" }, +] + +[[package]] +name = "setuptools" +version = "81.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" }, +] + +[[package]] +name = "shapely" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489, upload-time = "2025-09-24T13:51:41.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/8d/1ff672dea9ec6a7b5d422eb6d095ed886e2e523733329f75fdcb14ee1149/shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91121757b0a36c9aac3427a651a7e6567110a4a67c97edf04f8d55d4765f6618", size = 1820038, upload-time = "2025-09-24T13:50:15.628Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ce/28fab8c772ce5db23a0d86bf0adaee0c4c79d5ad1db766055fa3dab442e2/shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:16a9c722ba774cf50b5d4541242b4cce05aafd44a015290c82ba8a16931ff63d", size = 1626039, upload-time = "2025-09-24T13:50:16.881Z" }, + { url = "https://files.pythonhosted.org/packages/70/8b/868b7e3f4982f5006e9395c1e12343c66a8155c0374fdc07c0e6a1ab547d/shapely-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cc4f7397459b12c0b196c9efe1f9d7e92463cbba142632b4cc6d8bbbbd3e2b09", size = 3001519, upload-time = "2025-09-24T13:50:18.606Z" }, + { url = "https://files.pythonhosted.org/packages/13/02/58b0b8d9c17c93ab6340edd8b7308c0c5a5b81f94ce65705819b7416dba5/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136ab87b17e733e22f0961504d05e77e7be8c9b5a8184f685b4a91a84efe3c26", size = 3110842, upload-time = "2025-09-24T13:50:21.77Z" }, + { url = "https://files.pythonhosted.org/packages/af/61/8e389c97994d5f331dcffb25e2fa761aeedfb52b3ad9bcdd7b8671f4810a/shapely-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:16c5d0fc45d3aa0a69074979f4f1928ca2734fb2e0dde8af9611e134e46774e7", size = 4021316, upload-time = "2025-09-24T13:50:23.626Z" }, + { url = "https://files.pythonhosted.org/packages/d3/d4/9b2a9fe6039f9e42ccf2cb3e84f219fd8364b0c3b8e7bbc857b5fbe9c14c/shapely-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ddc759f72b5b2b0f54a7e7cde44acef680a55019eb52ac63a7af2cf17cb9cd2", size = 4178586, upload-time = "2025-09-24T13:50:25.443Z" }, + { url = "https://files.pythonhosted.org/packages/16/f6/9840f6963ed4decf76b08fd6d7fed14f8779fb7a62cb45c5617fa8ac6eab/shapely-2.1.2-cp311-cp311-win32.whl", hash = "sha256:2fa78b49485391224755a856ed3b3bd91c8455f6121fee0db0e71cefb07d0ef6", size = 1543961, upload-time = "2025-09-24T13:50:26.968Z" }, + { url = "https://files.pythonhosted.org/packages/38/1e/3f8ea46353c2a33c1669eb7327f9665103aa3a8dfe7f2e4ef714c210b2c2/shapely-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:c64d5c97b2f47e3cd9b712eaced3b061f2b71234b3fc263e0fcf7d889c6559dc", size = 1722856, upload-time = "2025-09-24T13:50:28.497Z" }, + { url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550, upload-time = "2025-09-24T13:50:30.019Z" }, + { url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556, upload-time = "2025-09-24T13:50:32.291Z" }, + { url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308, upload-time = "2025-09-24T13:50:33.862Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/e781683abac55dde9771e086b790e554811a71ed0b2b8a1e789b7430dd44/shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b", size = 3099844, upload-time = "2025-09-24T13:50:35.459Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f3/9876b64d4a5a321b9dc482c92bb6f061f2fa42131cba643c699f39317cb9/shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc", size = 3988842, upload-time = "2025-09-24T13:50:37.478Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/704c7292f7014c7e74ec84eddb7b109e1fbae74a16deae9c1504b1d15565/shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d", size = 4152714, upload-time = "2025-09-24T13:50:39.9Z" }, + { url = "https://files.pythonhosted.org/packages/53/46/319c9dc788884ad0785242543cdffac0e6530e4d0deb6c4862bc4143dcf3/shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454", size = 1542745, upload-time = "2025-09-24T13:50:41.414Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861, upload-time = "2025-09-24T13:50:43.35Z" }, + { url = "https://files.pythonhosted.org/packages/c3/90/98ef257c23c46425dc4d1d31005ad7c8d649fe423a38b917db02c30f1f5a/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8", size = 1832644, upload-time = "2025-09-24T13:50:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a", size = 1642887, upload-time = "2025-09-24T13:50:46.735Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5e/7d7f54ba960c13302584c73704d8c4d15404a51024631adb60b126a4ae88/shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e", size = 2970931, upload-time = "2025-09-24T13:50:48.374Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6", size = 3082855, upload-time = "2025-09-24T13:50:50.037Z" }, + { url = "https://files.pythonhosted.org/packages/44/2b/578faf235a5b09f16b5f02833c53822294d7f21b242f8e2d0cf03fb64321/shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af", size = 3979960, upload-time = "2025-09-24T13:50:51.74Z" }, + { url = "https://files.pythonhosted.org/packages/4d/04/167f096386120f692cc4ca02f75a17b961858997a95e67a3cb6a7bbd6b53/shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd", size = 4142851, upload-time = "2025-09-24T13:50:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/48/74/fb402c5a6235d1c65a97348b48cdedb75fb19eca2b1d66d04969fc1c6091/shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350", size = 1541890, upload-time = "2025-09-24T13:50:55.337Z" }, + { url = "https://files.pythonhosted.org/packages/41/47/3647fe7ad990af60ad98b889657a976042c9988c2807cf322a9d6685f462/shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715", size = 1722151, upload-time = "2025-09-24T13:50:57.153Z" }, + { url = "https://files.pythonhosted.org/packages/3c/49/63953754faa51ffe7d8189bfbe9ca34def29f8c0e34c67cbe2a2795f269d/shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40", size = 1834130, upload-time = "2025-09-24T13:50:58.49Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ee/dce001c1984052970ff60eb4727164892fb2d08052c575042a47f5a9e88f/shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b", size = 1642802, upload-time = "2025-09-24T13:50:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/da/e7/fc4e9a19929522877fa602f705706b96e78376afb7fad09cad5b9af1553c/shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801", size = 3018460, upload-time = "2025-09-24T13:51:02.08Z" }, + { url = "https://files.pythonhosted.org/packages/a1/18/7519a25db21847b525696883ddc8e6a0ecaa36159ea88e0fef11466384d0/shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0", size = 3095223, upload-time = "2025-09-24T13:51:04.472Z" }, + { url = "https://files.pythonhosted.org/packages/48/de/b59a620b1f3a129c3fecc2737104a0a7e04e79335bd3b0a1f1609744cf17/shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c", size = 4030760, upload-time = "2025-09-24T13:51:06.455Z" }, + { url = "https://files.pythonhosted.org/packages/96/b3/c6655ee7232b417562bae192ae0d3ceaadb1cc0ffc2088a2ddf415456cc2/shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99", size = 4170078, upload-time = "2025-09-24T13:51:08.584Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8e/605c76808d73503c9333af8f6cbe7e1354d2d238bda5f88eea36bfe0f42a/shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf", size = 1559178, upload-time = "2025-09-24T13:51:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/36/f7/d317eb232352a1f1444d11002d477e54514a4a6045536d49d0c59783c0da/shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c", size = 1739756, upload-time = "2025-09-24T13:51:12.105Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c4/3ce4c2d9b6aabd27d26ec988f08cb877ba9e6e96086eff81bfea93e688c7/shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223", size = 1831290, upload-time = "2025-09-24T13:51:13.56Z" }, + { url = "https://files.pythonhosted.org/packages/17/b9/f6ab8918fc15429f79cb04afa9f9913546212d7fb5e5196132a2af46676b/shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c", size = 1641463, upload-time = "2025-09-24T13:51:14.972Z" }, + { url = "https://files.pythonhosted.org/packages/a5/57/91d59ae525ca641e7ac5551c04c9503aee6f29b92b392f31790fcb1a4358/shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df", size = 2970145, upload-time = "2025-09-24T13:51:16.961Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cb/4948be52ee1da6927831ab59e10d4c29baa2a714f599f1f0d1bc747f5777/shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf", size = 3073806, upload-time = "2025-09-24T13:51:18.712Z" }, + { url = "https://files.pythonhosted.org/packages/03/83/f768a54af775eb41ef2e7bec8a0a0dbe7d2431c3e78c0a8bdba7ab17e446/shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4", size = 3980803, upload-time = "2025-09-24T13:51:20.37Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/559c7c195807c91c79d38a1f6901384a2878a76fbdf3f1048893a9b7534d/shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc", size = 4133301, upload-time = "2025-09-24T13:51:21.887Z" }, + { url = "https://files.pythonhosted.org/packages/80/cd/60d5ae203241c53ef3abd2ef27c6800e21afd6c94e39db5315ea0cbafb4a/shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566", size = 1583247, upload-time = "2025-09-24T13:51:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/74/d4/135684f342e909330e50d31d441ace06bf83c7dc0777e11043f99167b123/shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c", size = 1773019, upload-time = "2025-09-24T13:51:24.873Z" }, + { url = "https://files.pythonhosted.org/packages/a3/05/a44f3f9f695fa3ada22786dc9da33c933da1cbc4bfe876fe3a100bafe263/shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a", size = 1834137, upload-time = "2025-09-24T13:51:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/4d57db45bf314573427b0a70dfca15d912d108e6023f623947fa69f39b72/shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076", size = 1642884, upload-time = "2025-09-24T13:51:28.029Z" }, + { url = "https://files.pythonhosted.org/packages/5a/27/4e29c0a55d6d14ad7422bf86995d7ff3f54af0eba59617eb95caf84b9680/shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1", size = 3018320, upload-time = "2025-09-24T13:51:29.903Z" }, + { url = "https://files.pythonhosted.org/packages/9f/bb/992e6a3c463f4d29d4cd6ab8963b75b1b1040199edbd72beada4af46bde5/shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0", size = 3094931, upload-time = "2025-09-24T13:51:32.699Z" }, + { url = "https://files.pythonhosted.org/packages/9c/16/82e65e21070e473f0ed6451224ed9fa0be85033d17e0c6e7213a12f59d12/shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26", size = 4030406, upload-time = "2025-09-24T13:51:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/7c/75/c24ed871c576d7e2b64b04b1fe3d075157f6eb54e59670d3f5ffb36e25c7/shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0", size = 4169511, upload-time = "2025-09-24T13:51:36.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f7/b3d1d6d18ebf55236eec1c681ce5e665742aab3c0b7b232720a7d43df7b6/shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735", size = 1602607, upload-time = "2025-09-24T13:51:37.757Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/f09272a71976dfc138129b8faf435d064a811ae2f708cb147dccdf7aacdb/shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9", size = 1796682, upload-time = "2025-09-24T13:51:39.233Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "terminado" +version = "0.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess", marker = "os_name != 'nt'" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, + { 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" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", size = 14154, upload-time = "2024-03-12T14:34:36.569Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tinycss2" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, +] + +[[package]] +name = "torch" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/0d/98b410492609e34a155fa8b121b55c7dca229f39636851c3a9ec20edea21/torch-2.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7b6a60d48062809f58595509c524b88e6ddec3ebe25833d6462eeab81e5f2ce4", size = 80529712, upload-time = "2026-03-23T18:12:02.608Z" }, + { url = "https://files.pythonhosted.org/packages/84/03/acea680005f098f79fd70c1d9d5ccc0cb4296ec2af539a0450108232fc0c/torch-2.11.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:d91aac77f24082809d2c5a93f52a5f085032740a1ebc9252a7b052ef5a4fddc6", size = 419718178, upload-time = "2026-03-23T18:10:46.675Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8b/d7be22fbec9ffee6cff31a39f8750d4b3a65d349a286cf4aec74c2375662/torch-2.11.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7aa2f9bbc6d4595ba72138026b2074be1233186150e9292865e04b7a63b8c67a", size = 530604548, upload-time = "2026-03-23T18:10:03.569Z" }, + { url = "https://files.pythonhosted.org/packages/d1/bd/9912d30b68845256aabbb4a40aeefeef3c3b20db5211ccda653544ada4b6/torch-2.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:73e24aaf8f36ab90d95cd1761208b2eb70841c2a9ca1a3f9061b39fc5331b708", size = 114519675, upload-time = "2026-03-23T18:11:52.995Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8b/69e3008d78e5cee2b30183340cc425081b78afc5eff3d080daab0adda9aa/torch-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b5866312ee6e52ea625cd211dcb97d6a2cdc1131a5f15cc0d87eec948f6dd34", size = 80606338, upload-time = "2026-03-23T18:11:34.781Z" }, + { url = "https://files.pythonhosted.org/packages/13/16/42e5915ebe4868caa6bac83a8ed59db57f12e9a61b7d749d584776ed53d5/torch-2.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f99924682ef0aa6a4ab3b1b76f40dc6e273fca09f367d15a524266db100a723f", size = 419731115, upload-time = "2026-03-23T18:11:06.944Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c9/82638ef24d7877510f83baf821f5619a61b45568ce21c0a87a91576510aa/torch-2.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0f68f4ac6d95d12e896c3b7a912b5871619542ec54d3649cf48cc1edd4dd2756", size = 530712279, upload-time = "2026-03-23T18:10:31.481Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ff/6756f1c7ee302f6d202120e0f4f05b432b839908f9071157302cedfc5232/torch-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:fbf39280699d1b869f55eac536deceaa1b60bd6788ba74f399cc67e60a5fab10", size = 114556047, upload-time = "2026-03-23T18:10:55.931Z" }, + { url = "https://files.pythonhosted.org/packages/87/89/5ea6722763acee56b045435fb84258db7375c48165ec8be7880ab2b281c5/torch-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6debd97ccd3205bbb37eb806a9d8219e1139d15419982c09e23ef7d4369d18", size = 80606801, upload-time = "2026-03-23T18:10:18.649Z" }, + { url = "https://files.pythonhosted.org/packages/32/d1/8ed2173589cbfe744ed54e5a73efc107c0085ba5777ee93a5f4c1ab90553/torch-2.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:63a68fa59de8f87acc7e85a5478bb2dddbb3392b7593ec3e78827c793c4b73fd", size = 419732382, upload-time = "2026-03-23T18:08:30.835Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e1/b73f7c575a4b8f87a5928f50a1e35416b5e27295d8be9397d5293e7e8d4c/torch-2.11.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:cc89b9b173d9adfab59fd227f0ab5e5516d9a52b658ae41d64e59d2e55a418db", size = 530711509, upload-time = "2026-03-23T18:08:47.213Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/3e3fcdd388fbe54e29fd3f991f36846ff4ac90b0d0181e9c8f7236565f82/torch-2.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:4dda3b3f52d121063a731ddb835f010dc137b920d7fec2778e52f60d8e4bf0cd", size = 114555842, upload-time = "2026-03-23T18:09:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/db/38/8ac78069621b8c2b4979c2f96dc8409ef5e9c4189f6aac629189a78677ca/torch-2.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8b394322f49af4362d4f80e424bcaca7efcd049619af03a4cf4501520bdf0fb4", size = 80959574, upload-time = "2026-03-23T18:10:14.214Z" }, + { url = "https://files.pythonhosted.org/packages/6d/6c/56bfb37073e7136e6dd86bfc6af7339946dd684e0ecf2155ac0eee687ae1/torch-2.11.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:2658f34ce7e2dabf4ec73b45e2ca68aedad7a5be87ea756ad656eaf32bf1e1ea", size = 419732324, upload-time = "2026-03-23T18:09:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/07/f4/1b666b6d61d3394cca306ea543ed03a64aad0a201b6cd159f1d41010aeb1/torch-2.11.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:98bb213c3084cfe176302949bdc360074b18a9da7ab59ef2edc9d9f742504778", size = 530596026, upload-time = "2026-03-23T18:09:20.842Z" }, + { url = "https://files.pythonhosted.org/packages/48/6b/30d1459fa7e4b67e9e3fe1685ca1d8bb4ce7c62ef436c3a615963c6c866c/torch-2.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a97b94bbf62992949b4730c6cd2cc9aee7b335921ee8dc207d930f2ed09ae2db", size = 114793702, upload-time = "2026-03-23T18:09:47.304Z" }, + { url = "https://files.pythonhosted.org/packages/26/0d/8603382f61abd0db35841148ddc1ffd607bf3100b11c6e1dab6d2fc44e72/torch-2.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01018087326984a33b64e04c8cb5c2795f9120e0d775ada1f6638840227b04d7", size = 80573442, upload-time = "2026-03-23T18:09:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/c7/86/7cd7c66cb9cec6be330fff36db5bd0eef386d80c031b581ec81be1d4b26c/torch-2.11.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:2bb3cc54bd0dea126b0060bb1ec9de0f9c7f7342d93d436646516b0330cd5be7", size = 419749385, upload-time = "2026-03-23T18:07:33.77Z" }, + { url = "https://files.pythonhosted.org/packages/47/e8/b98ca2d39b2e0e4730c0ee52537e488e7008025bc77ca89552ff91021f7c/torch-2.11.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4dc8b3809469b6c30b411bb8c4cad3828efd26236153d9beb6a3ec500f211a60", size = 530716756, upload-time = "2026-03-23T18:07:50.02Z" }, + { url = "https://files.pythonhosted.org/packages/78/88/d4a4cda8362f8a30d1ed428564878c3cafb0d87971fbd3947d4c84552095/torch-2.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:2b4e811728bd0cc58fb2b0948fe939a1ee2bf1422f6025be2fca4c7bd9d79718", size = 114552300, upload-time = "2026-03-23T18:09:05.617Z" }, + { url = "https://files.pythonhosted.org/packages/bf/46/4419098ed6d801750f26567b478fc185c3432e11e2cad712bc6b4c2ab0d0/torch-2.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8245477871c3700d4370352ffec94b103cfcb737229445cf9946cddb7b2ca7cd", size = 80959460, upload-time = "2026-03-23T18:09:00.818Z" }, + { url = "https://files.pythonhosted.org/packages/fd/66/54a56a4a6ceaffb567231994a9745821d3af922a854ed33b0b3a278e0a99/torch-2.11.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:ab9a8482f475f9ba20e12db84b0e55e2f58784bdca43a854a6ccd3fd4b9f75e6", size = 419735835, upload-time = "2026-03-23T18:07:18.974Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e7/0b6665f533aa9e337662dc190425abc0af1fe3234088f4454c52393ded61/torch-2.11.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:563ed3d25542d7e7bbc5b235ccfacfeb97fb470c7fee257eae599adb8005c8a2", size = 530613405, upload-time = "2026-03-23T18:08:07.014Z" }, + { url = "https://files.pythonhosted.org/packages/cf/bf/c8d12a2c86dbfd7f40fb2f56fbf5a505ccf2d9ce131eb559dfc7c51e1a04/torch-2.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b2a43985ff5ef6ddd923bbcf99943e5f58059805787c5c9a2622bf05ca2965b0", size = 114792991, upload-time = "2026-03-23T18:08:19.216Z" }, +] + +[[package]] +name = "torchvision" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, + { name = "torch" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/bd/d552a2521bade3295b2c6e7a4a0d1022261cab7ca7011f4e2a330dbb3caa/torchvision-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55bd6ad4ae77be01ba67a410b05b51f53b0d0ee45f146eb6a0dfb9007e70ab3c", size = 1863499, upload-time = "2026-03-23T18:12:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/33/bf/21b899792b08cae7a298551c68398a79e333697479ed311b3b067aab4bdc/torchvision-0.26.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:1c55dc8affbcc0eb2060fbabbe996ae9e5839b24bb6419777f17848945a411b1", size = 7767527, upload-time = "2026-03-23T18:12:44.348Z" }, + { url = "https://files.pythonhosted.org/packages/9a/45/57bbf9e216850d065e66dd31a50f57424b607f1d878ab8956e56a1f4e36b/torchvision-0.26.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:fd10b5f994c210f4f6d6761cf686f82d748554adf486cb0979770c3252868c8f", size = 7519925, upload-time = "2026-03-23T18:12:53.283Z" }, + { url = "https://files.pythonhosted.org/packages/10/58/ed8f7754299f3e91d6414b6dc09f62b3fa7c6e5d63dfe48d69ab81498a37/torchvision-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:de6424b12887ad884f39a0ee446994ae3cd3b6a00a9cafe1bead85a031132af0", size = 3983834, upload-time = "2026-03-23T18:13:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e7/56b47cc3b132aea90ccce22bcb8975dec688b002150012acc842846039d0/torchvision-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c409e1c3fdebec7a3834465086dbda8bf7680eff79abf7fd2f10c6b59520a7a4", size = 1863502, upload-time = "2026-03-23T18:12:57.326Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ec/5c31c92c08b65662fe9604a4067ae8232582805949f11ddc042cebe818ed/torchvision-0.26.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:406557718e62fdf10f5706e88d8a5ec000f872da913bf629aab9297622585547", size = 7767944, upload-time = "2026-03-23T18:12:42.805Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d8/cb6ccda1a1f35a6597645818641701207b3e8e13553e75fce5d86bac74b2/torchvision-0.26.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d61a5abb6b42a0c0c311996c2ac4b83a94418a97182c83b055a2a4ae985e05aa", size = 7522205, upload-time = "2026-03-23T18:12:54.654Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a9/c272623a0f735c35f0f6cd6dc74784d4f970e800cf063bb76687895a2ab9/torchvision-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:7993c01648e7c61d191b018e84d38fe0825c8fcb2720cd0f37caf7ba14404aa1", size = 4255155, upload-time = "2026-03-23T18:12:32.652Z" }, + { url = "https://files.pythonhosted.org/packages/da/80/0762f77f53605d10c9477be39bb47722cc8e383bbbc2531471ce0e396c07/torchvision-0.26.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5d63dd43162691258b1b3529b9041bac7d54caa37eae0925f997108268cbf7c4", size = 1860809, upload-time = "2026-03-23T18:12:47.629Z" }, + { url = "https://files.pythonhosted.org/packages/e6/81/0b3e58d1478c660a5af4268713486b2df7203f35abd9195fea87348a5178/torchvision-0.26.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a39c7a26538c41fda453f9a9692b5ff9b35a5437db1d94f3027f6f509c160eac", size = 7727494, upload-time = "2026-03-23T18:12:46.062Z" }, + { url = "https://files.pythonhosted.org/packages/b6/dc/d9ab5d29115aa05e12e30f1397a3eeae1d88a511241dc3bce48dc4342675/torchvision-0.26.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:b7e6213620bbf97742e5f79832f9e9d769e6cf0f744c5b53dad80b76db633691", size = 7521747, upload-time = "2026-03-23T18:12:36.815Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1b/f1bc86a918c5f6feab1eeff11982e2060f4704332e96185463d27855bdf5/torchvision-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:4280c35ec8cba1fcc8294fb87e136924708726864c379e4c54494797d86bc474", size = 4319880, upload-time = "2026-03-23T18:12:38.168Z" }, + { url = "https://files.pythonhosted.org/packages/66/28/b4ad0a723ed95b003454caffcc41894b34bd8379df340848cae2c33871de/torchvision-0.26.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:358fc4726d0c08615b6d83b3149854f11efb2a564ed1acb6fce882e151412d23", size = 1951973, upload-time = "2026-03-23T18:12:48.781Z" }, + { url = "https://files.pythonhosted.org/packages/71/e2/7a89096e6cf2f3336353b5338ba925e0addf9d8601920340e6bdf47e8eb3/torchvision-0.26.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:3daf9cc149cf3cdcbd4df9c59dae69ffca86c6823250442c3bbfd63fc2e26c61", size = 7728679, upload-time = "2026-03-23T18:12:26.196Z" }, + { url = "https://files.pythonhosted.org/packages/69/1d/4e1eebc17d18ce080a11dcf3df3f8f717f0efdfa00983f06e8ba79259f61/torchvision-0.26.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:82c3965eca27e86a316e31e4c3e5a16d353e0bcbe0ef8efa2e66502c54493c4b", size = 7609138, upload-time = "2026-03-23T18:12:35.327Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a4/f1155e943ae5b32400d7000adc81c79bb0392b16ceb33bcf13e02e48cced/torchvision-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ebc043cc5a4f0bf22e7680806dbba37ffb19e70f6953bbb44ed1a90aeb5c9bea", size = 4248202, upload-time = "2026-03-23T18:12:41.423Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c8/9bffa9c7f7bdf95b2a0a2dc535c290b9f1cc580c3fb3033ab1246ffffdeb/torchvision-0.26.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:eb61804eb9dbe88c5a2a6c4da8dec1d80d2d0a6f18c999c524e32266cb1ebcd3", size = 1860813, upload-time = "2026-03-23T18:12:39.636Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ac/48f28ffd227991f2e14f4392dde7e8dc14352bb9428c1ef4a4bbf5f7ed85/torchvision-0.26.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:9a904f2131cbfadab4df828088a9f66291ad33f49ff853872aed1f86848ef776", size = 7727777, upload-time = "2026-03-23T18:12:22.549Z" }, + { url = "https://files.pythonhosted.org/packages/a4/21/a2266f7f1b0e58e624ff15fd6f01041f59182c49551ece0db9a183071329/torchvision-0.26.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:0f3e572efe62ad645017ea847e0b5e4f2f638d4e39f05bc011d1eb9ac68d4806", size = 7522174, upload-time = "2026-03-23T18:12:29.565Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ba/1666f90bc0bdd77aaa11dcc42bb9f621a9c3668819c32430452e3d404730/torchvision-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:114bec0c0e98aa4ba446f63e2fe7a2cbca37b39ac933987ee4804f65de121800", size = 4348469, upload-time = "2026-03-23T18:12:24.44Z" }, + { url = "https://files.pythonhosted.org/packages/45/8f/1f0402ac55c2ae15651ff831957d083fe70b2d12282e72612a30ba601512/torchvision-0.26.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:b7d3e295624a28b3b1769228ce1345d94cf4d390dd31136766f76f2d20f718da", size = 1860826, upload-time = "2026-03-23T18:12:34.1Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6a/18a582fe3c5ee26f49b5c9fb21ad8016b4d1c06d10178894a58653946fda/torchvision-0.26.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:7058c5878262937e876f20c25867b33724586aa4499e2853b2d52b99a5e51953", size = 7729089, upload-time = "2026-03-23T18:12:31.394Z" }, + { url = "https://files.pythonhosted.org/packages/c5/9b/f7e119b59499edc00c55c03adc9ec3bd96144d9b81c46852c431f9c64a9a/torchvision-0.26.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8008474855623c6ba52876589dc52df0aa66e518c25eca841445348e5f79844c", size = 7522704, upload-time = "2026-03-23T18:12:20.301Z" }, + { url = "https://files.pythonhosted.org/packages/d0/6a/09f3844c10643f6c0de5d95abc863420cfaf194c88c7dffd0ac523e2015f/torchvision-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e9d0e022c19a78552fb055d0414d47fecb4a649309b9968573daea160ba6869c", size = 4454275, upload-time = "2026-03-23T18:12:27.487Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521", size = 444246, upload-time = "2026-03-10T21:30:46.571Z" }, + { url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" }, + { url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" }, + { url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" }, + { url = "https://files.pythonhosted.org/packages/5a/76/4921c00511f88af86a33de770d64141170f1cfd9c00311aea689949e274e/tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7", size = 448582, upload-time = "2026-03-10T21:30:57.142Z" }, + { url = "https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b", size = 448990, upload-time = "2026-03-10T21:30:58.857Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c8/876602cbc96469911f0939f703453c1157b0c826ecb05bdd32e023397d4e/tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6", size = 448016, upload-time = "2026-03-10T21:31:00.43Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + +[[package]] +name = "triton" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/2c/96f92f3c60387e14cc45aed49487f3486f89ea27106c1b1376913c62abe4/triton-3.6.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49df5ef37379c0c2b5c0012286f80174fcf0e073e5ade1ca9a86c36814553651", size = 176081190, upload-time = "2026-01-20T16:16:00.523Z" }, + { url = "https://files.pythonhosted.org/packages/e0/12/b05ba554d2c623bffa59922b94b0775673de251f468a9609bc9e45de95e9/triton-3.6.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8e323d608e3a9bfcc2d9efcc90ceefb764a82b99dea12a86d643c72539ad5d3", size = 188214640, upload-time = "2026-01-20T16:00:35.869Z" }, + { url = "https://files.pythonhosted.org/packages/17/5d/08201db32823bdf77a0e2b9039540080b2e5c23a20706ddba942924ebcd6/triton-3.6.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:374f52c11a711fd062b4bfbb201fd9ac0a5febd28a96fb41b4a0f51dde3157f4", size = 176128243, upload-time = "2026-01-20T16:16:07.857Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/3c/12/34d71b350e89a204c2c7777a9bba0dcf2f19a5bfdd70b57c4dbc5ffd7154/triton-3.6.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448e02fe6dc898e9e5aa89cf0ee5c371e99df5aa5e8ad976a80b93334f3494fd", size = 176133521, upload-time = "2026-01-20T16:16:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0b/37d991d8c130ce81a8728ae3c25b6e60935838e9be1b58791f5997b24a54/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9", size = 188289450, upload-time = "2026-01-20T16:00:49.136Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4e/41b0c8033b503fd3cfcd12392cdd256945026a91ff02452bef40ec34bee7/triton-3.6.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1722e172d34e32abc3eb7711d0025bb69d7959ebea84e3b7f7a341cd7ed694d6", size = 176276087, upload-time = "2026-01-20T16:16:18.989Z" }, + { url = "https://files.pythonhosted.org/packages/35/f8/9c66bfc55361ec6d0e4040a0337fb5924ceb23de4648b8a81ae9d33b2b38/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f", size = 188400296, upload-time = "2026-01-20T16:00:56.042Z" }, + { url = "https://files.pythonhosted.org/packages/49/55/5ecf0dcaa0f2fbbd4420f7ef227ee3cb172e91e5fede9d0ecaddc43363b4/triton-3.6.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5523241e7d1abca00f1d240949eebdd7c673b005edbbce0aca95b8191f1d43", size = 176138577, upload-time = "2026-01-20T16:16:25.426Z" }, + { url = "https://files.pythonhosted.org/packages/df/3d/9e7eee57b37c80cec63322c0231bb6da3cfe535a91d7a4d64896fcb89357/triton-3.6.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803", size = 188273063, upload-time = "2026-01-20T16:01:07.278Z" }, + { url = "https://files.pythonhosted.org/packages/48/db/56ee649cab5eaff4757541325aca81f52d02d4a7cd3506776cad2451e060/triton-3.6.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b3a97e8ed304dfa9bd23bb41ca04cdf6b2e617d5e782a8653d616037a5d537d", size = 176274804, upload-time = "2026-01-20T16:16:31.528Z" }, + { url = "https://files.pythonhosted.org/packages/f6/56/6113c23ff46c00aae423333eb58b3e60bdfe9179d542781955a5e1514cb3/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7", size = 188397994, upload-time = "2026-01-20T16:01:14.236Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, +] + +[[package]] +name = "ultralytics" +version = "8.3.124" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy" }, + { name = "opencv-python" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "psutil" }, + { name = "py-cpuinfo" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "scipy" }, + { name = "seaborn" }, + { name = "torch" }, + { name = "torchvision" }, + { name = "tqdm" }, + { name = "ultralytics-thop" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/8f/163d06fd8154e7ddf7a4b9c311d68e38d600b187cd84bbc3909322eea611/ultralytics-8.3.124.tar.gz", hash = "sha256:5f49cee43ed03252dfd2e929027ccc357e164435ab5b847de3f0831a429f261f", size = 864356, upload-time = "2025-05-02T18:56:10.723Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/aa/417553f825f7a5c50d9e535b40aaa7edd7869462ab27093775eb6f417e48/ultralytics-8.3.124-py3-none-any.whl", hash = "sha256:fdae7406fffef54829bac09af7948c6052f13260b6f1c544dd1b5f7f6d5a341d", size = 1006359, upload-time = "2025-05-02T18:56:08.149Z" }, +] + +[[package]] +name = "ultralytics-thop" +version = "2.0.19" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/ab/1cc7edfd5241ed9abb2e8ee0d5088d2800c989df9baa43e05706a87f765b/ultralytics_thop-2.0.19.tar.gz", hash = "sha256:df547d93ebdec4c9d2a9375f2e21d483547a462c08814e8810dc73498dd29ba9", size = 34665, upload-time = "2026-04-24T12:21:23.844Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/74/af3e40919305f16968ea3ab88d84b511d710dd281eb5dafaf4897579dd22/ultralytics_thop-2.0.19-py3-none-any.whl", hash = "sha256:a180baeb3b83f0f2ad3f3572338cef406b221a57c6d10ca09cfd90f9c892a739", size = 28952, upload-time = "2026-04-24T12:21:22.207Z" }, +] + +[[package]] +name = "uri-template" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/c7/0336f2bd0bcbada6ccef7aaa25e443c118a704f828a0620c6fa0207c1b64/uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7", size = 21678, upload-time = "2023-06-21T01:49:05.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363", size = 11140, upload-time = "2023-06-21T01:49:03.467Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, +] + +[[package]] +name = "webcolors" +version = "25.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/7a/eb316761ec35664ea5174709a68bbd3389de60d4a1ebab8808bfc264ed67/webcolors-25.10.0.tar.gz", hash = "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf", size = 53491, upload-time = "2025-10-31T07:51:03.977Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d", size = 14905, upload-time = "2025-10-31T07:51:01.778Z" }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +] + +[[package]] +name = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, +] + +[[package]] +name = "widgetsnbextension" +version = "4.0.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/f4/c67440c7fb409a71b7404b7aefcd7569a9c0d6bd071299bf4198ae7a5d95/widgetsnbextension-4.0.15.tar.gz", hash = "sha256:de8610639996f1567952d763a5a41af8af37f2575a41f9852a38f947eb82a3b9", size = 1097402, upload-time = "2025-11-01T21:15:55.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl", hash = "sha256:8156704e4346a571d9ce73b84bee86a29906c9abfd7223b7228a28899ccf3366", size = 2196503, upload-time = "2025-11-01T21:15:53.565Z" }, +] From 906c7aa8d3cc9e9b4de518f7b78f0c6726402f9c Mon Sep 17 00:00:00 2001 From: asgersvenning Date: Tue, 28 Apr 2026 15:41:56 +0200 Subject: [PATCH 10/41] Refactor unittests to use pytest instead of unittest --- .gitignore | 3 - pyproject.toml | 22 +- src/flat_bug/geometric.py | 268 ++++++++++++++---- src/flat_bug/nms.py | 8 +- src/flat_bug/predictor.py | 27 +- ...0_11_03_4545_UUID_ChangeThisTEMPORARY.json | 1 - ...ids_Unknown_2020_11_03_4545_UUID_XXXX.json | 2 - src/flat_bug/yolo_helpers.py | 6 +- {src/flat_bug/tests => tests}/README.txt | 14 +- {src/flat_bug/tests => tests}/__init__.py | 0 tests/assets/.gitignore | 2 + ...-miteArachnids_Unknown_2020_11_03_4545.jpg | Bin .../assets/pyramid_output_length.txt | 0 .../assets/single_scale_output_length.txt | 0 tests/conftest.py | 15 + .../tests => tests}/generate_model_outputs.py | 2 +- .../tests => tests}/remote_lfs_fallback.py | 0 .../tests => tests}/restore_assets.py | 4 +- .../tests => tests}/test_augmentations.py | 52 ++-- {src/flat_bug/tests => tests}/test_config.py | 30 +- {src/flat_bug/tests => tests}/test_dataset.py | 52 ++-- .../tests => tests}/test_predictor.py | 47 ++- uv.lock | 58 +++- 23 files changed, 403 insertions(+), 210 deletions(-) delete mode 100644 src/flat_bug/tests/assets/metadata_ALUS_Non-miteArachnids_Unknown_2020_11_03_4545_UUID_ChangeThisTEMPORARY.json delete mode 100644 src/flat_bug/tests/assets/metadata_ALUS_Non-miteArachnids_Unknown_2020_11_03_4545_UUID_XXXX.json rename {src/flat_bug/tests => tests}/README.txt (62%) rename {src/flat_bug/tests => tests}/__init__.py (100%) create mode 100644 tests/assets/.gitignore rename {src/flat_bug/tests => tests}/assets/ALUS_Non-miteArachnids_Unknown_2020_11_03_4545.jpg (100%) rename {src/flat_bug/tests => tests}/assets/pyramid_output_length.txt (100%) rename {src/flat_bug/tests => tests}/assets/single_scale_output_length.txt (100%) create mode 100644 tests/conftest.py rename {src/flat_bug/tests => tests}/generate_model_outputs.py (94%) rename {src/flat_bug/tests => tests}/remote_lfs_fallback.py (100%) rename {src/flat_bug/tests => tests}/restore_assets.py (67%) rename {src/flat_bug/tests => tests}/test_augmentations.py (79%) rename {src/flat_bug/tests => tests}/test_config.py (86%) rename {src/flat_bug/tests => tests}/test_dataset.py (80%) rename {src/flat_bug/tests => tests}/test_predictor.py (84%) diff --git a/.gitignore b/.gitignore index cbb1864..44aa243 100644 --- a/.gitignore +++ b/.gitignore @@ -191,9 +191,6 @@ utils/eval_slurm.sh utils/tune_slurm.sh utils/zip_output_for_ERDA.sh -src/flat_bug/tests/assets/ALUS_Non-miteArachnids_Unknown_2020_11_03_4545.txt -!src/flat_bug/tests/assets/ALUS_Non-miteArachnids_Unknown_2020_11_03_4545.jpg - install_digital_research_alliance_canada.sh pyvenv.cfg *.whl diff --git a/pyproject.toml b/pyproject.toml index 346136a..5c636ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "flat-bug" -version = "1.0.0" +version = "1.1.0" authors = [ {name="Asger Svenning", email="asgersvenning@ecos.au.dk"}, {name="Quentin Geissman"}, @@ -34,9 +34,6 @@ cloud_datasets = [ "boto3>=1.40", "cvat_sdk>=2.47" ] -jupyter = [ - "jupyter>=1.0.0", -] [project.urls] "Homepage" = "https://github.com/darsa-group/flat-bug" @@ -51,8 +48,13 @@ fb_prepare_data = "bin.fb_prepare_data:main" fb_clone_data = "bin.fb_clone_data:main" [dependency-groups] -dev = [ - "flat-bug[notebook,erda,cloud_datasets]", +notebook = [ + "ipykernel>=7.2.0", + "ipywidgets>=8.1.8", + "jupyter>=1.0.0", +] +tests = [ + "pytest>=9.0.3", ] [tool.ruff] @@ -72,6 +74,12 @@ extend-exclude = [ select = ["E", "F", "UP", "I", "D"] ignore = ["D105"] +[tool.pytest.ini_options] +filterwarnings = [ + "ignore:.*The NVIDIA driver on your system is too old.*:UserWarning", + "ignore:.*`torch.jit.script.*` is deprecated.*:DeprecationWarning" +] + [build-system] requires = ["uv_build>=0.10.7,<0.11.0"] -build-backend = "uv_build" \ No newline at end of file +build-backend = "uv_build" diff --git a/src/flat_bug/geometric.py b/src/flat_bug/geometric.py index 6a8a02b..1ac1011 100644 --- a/src/flat_bug/geometric.py +++ b/src/flat_bug/geometric.py @@ -2,7 +2,7 @@ import math from collections.abc import Sequence from itertools import accumulate -from typing import Literal, TypeVar, overload +from typing import Literal, TypeVar, overload, Never import cv2 import numpy as np @@ -12,7 +12,7 @@ from flat_bug import logger -V = TypeVar("V", bound=torch.Tensor | np.ndarray) +V = TypeVar("V", torch.Tensor, np.ndarray, covariant=True) def equal_allocate_overlaps(total: int, segments: int, size: int) -> list[int]: @@ -114,19 +114,25 @@ def create_contour_mask( @overload -def find_contours(mask : torch.Tensor, largest_only : Literal[True], simplify : bool=True) -> torch.Tensor: ... +def find_contours(mask : V, largest_only : Literal[True]=True, simplify : bool=True) -> V: ... @overload -def find_contours(mask : torch.Tensor, largest_only : Literal[False], simplify : bool=True) -> list[torch.Tensor]: ... +def find_contours(mask : V, largest_only : Literal[False]=False, simplify : bool=True) -> list[V]: ... def find_contours( - mask : torch.Tensor, + mask : V, largest_only : bool=True, simplify : bool=True - ) -> torch.Tensor | list[torch.Tensor]: + ) -> V | list[V]: """Extract polygons from a boolean mask.""" - contour = cv2.findContours(mask.to(torch.uint8).cpu().numpy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[0] + contour = list(cv2.findContours( + mask.to(torch.uint8).cpu().numpy() if isinstance(mask, torch.Tensor) else mask.astype(np.uint8), + cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE + ))[0] if len(contour) == 0: logger.info("No contours found; mask shape:", mask.shape, "mask sum:", mask.sum()) - return torch.tensor([[0, 0]], device=mask.device, dtype=torch.long) + if isinstance(mask, torch.Tensor): + return torch.tensor([[0, 0]], device=mask.device, dtype=torch.long) + else: + return np.array([[0, 0]], dtype=int) if largest_only: # Calculate areas of each contour areas = np.array([cv2.contourArea(c) for c in contour]) @@ -134,11 +140,18 @@ def find_contours( contour = contour[np.argmax(areas).item()] if simplify: contour = simplify_contour(contour, tolerance=1 if isinstance(simplify, bool) else simplify) + # Convert to tensor if isinstance(contour, list): - return [torch.tensor(c, dtype=torch.long, device=mask.device).squeeze(1) for c in contour] + contour = [np.asarray(c).squeeze(axis=1) for c in contour] else: - return torch.tensor(contour, dtype=torch.long, device=mask.device).squeeze(1) + contour = np.asarray(contour).squeeze(axis=1) + if not isinstance(mask, torch.Tensor): + return contour + elif isinstance(contour, list): + return [torch.tensor(c, dtype=torch.long, device=mask.device) for c in contour] + else: + return torch.tensor(contour, dtype=torch.long, device=mask.device) @overload def simplify_contour(contour : V, tolerance : float=1.0) -> V: ... @@ -177,78 +190,117 @@ def simplify_contour( 'expected a torch.Tensor or np.ndarray or an iterable of such.' ) - +@overload def contours_to_masks( - contours : list[torch.Tensor], + contours : Sequence[V], height : int | torch.Tensor, width : int | torch.Tensor - ) -> torch.Tensor: - """Rasterize a list of countors to a NxHxW boolean tensor stack. + ) -> V: ... +@overload +def contours_to_masks( + contours : Sequence[Never], + height : int | torch.Tensor, + width : int | torch.Tensor + ) -> torch.Tensor: ... +def contours_to_masks( + contours : Sequence[V], + height : int | torch.Tensor, + width : int | torch.Tensor + ) -> V | torch.Tensor: + """Rasterize a list of countors to a NxHxW boolean tensor/array stack. Contours should be represented as (i, j) index-coordinates in a Xx2 tensor. Args: - contours: List of contours represented as (i, j) index-coordinates in a Nx2 tensor (OBS: dtype=torch.long). + contours: List of contours represented as (i, j) index-coordinates in a Nx2 tensor or array. height: The height of the masks. width: The width of the masks. Returns: - NxHxW tensor of boolean masks with the contours filled in. - + NxHxW tensor or array of boolean masks with the contours filled in. """ - device = contours[0].device N = len(contours) - # Type checking - assert all(c.dtype == torch.long for c in contours), "All contours must be of dtype=torch.long" - assert all(c.device == device for c in contours), "All contours must be on the same device" - assert all(len(c.shape) == 2 and c.shape[1] == 2 for c in contours), "All contours must be Xx2 tensors" + if isinstance(height, torch.Tensor): assert height.numel() == 1, f"Height must be a scalar tensor not {height.shape}" - int_height = height.item() + int_height = int(height.item()) else: - int_height = height - int_height = int(int_height) + int_height = int(height) + if isinstance(width, torch.Tensor): assert width.numel() == 1, f"Width must be a scalar tensor not {width.shape}" - int_width = width.item() + int_width = int(width.item()) else: - int_width = width - int_width = int(int_width) + int_width = int(width) + assert int_height > 0 and int_width > 0, f"Height and width must be positive not {int_height} and {int_width}" # Initialize the masks as UMATs masks = np.zeros((N, int_height, int_width), dtype=np.uint8) - # If there are no contours, return the empty masks - if N == 0: - # Convert to tensors - return torch.tensor(masks, dtype=torch.bool, device=device) + # If there are no contours, return the empty masks gracefully + if N == 0: + return torch.as_tensor(masks, dtype=torch.bool) + + is_tensor = isinstance(contours[0], torch.Tensor) + + # Type checking + if is_tensor: + device = contours[0].device + assert all(c.dtype == torch.long for c in contours), "All tensors must be of dtype=torch.long" + assert all(c.device == device for c in contours), "All tensors must be on the same device" + assert all(len(c.shape) == 2 and c.shape[1] == 2 for c in contours), "All tensors must be Xx2" + else: + assert all(len(c.shape) == 2 and c.shape[1] == 2 for c in contours), "All arrays must be Xx2" + # Filling in the masks for i, contour in enumerate(contours): - masks[i] = cv2.drawContours(masks[i], [contour.cpu().numpy()], -1, 1, -1) + c_arr = contour.cpu().numpy() if isinstance(contour, torch.Tensor) else contour + masks[i] = cv2.drawContours(masks[i], [c_arr], -1, 1, -1) - # Convert to tensors - return torch.tensor(masks, dtype=torch.bool, device=device) + if isinstance(contours[0], torch.Tensor): + return torch.as_tensor(masks, dtype=torch.bool, device=contours[0].device) + return masks.astype(bool) -@torch.jit.script -def poly_area(poly : torch.Tensor) -> float: +@torch.compile +def _poly_area_tensor(poly : torch.Tensor): + if len(poly) < 10e4: + poly = poly.cpu() + poly_r = poly.roll(1, 0) + return (poly[:, 0] @ poly_r[:, 1] - poly[:, 1] @ poly_r[:, 0]) / 2.0 + + +def poly_area(poly : torch.Tensor | np.ndarray) -> float: """Calculate the area of a 2D simple polygon represented by a positively oriented (counter clock wise) sequence of points. See https://en.wikipedia.org/wiki/Shoelace_formula#Shoelace_formula for details. Args: - poly: A tensor of shape (n, 2), where n is the number of vertices and the 2 columns are the x and y coordinates of the vertices. + poly: A tensor or array of shape (n, 2), where n is the number of vertices and the 2 columns are the x and y coordinates of the vertices. Returns: The area of the polygon - """ - if len(poly) < 10e4: - poly = poly.cpu() - poly_r = poly.roll(1, 0) - return (poly[:, 0] @ poly_r[:, 1] - poly[:, 1] @ poly_r[:, 0]).item() / 2 + if isinstance(poly, torch.Tensor): + return float(_poly_area_tensor(poly)) + else: + poly_r = np.roll(poly, 1, axis=0) + return float((poly[:, 0] @ poly_r[:, 1] - poly[:, 1] @ poly_r[:, 0]) / 2.0) + +def _poly_normals_arr(polygon : np.ndarray) -> np.ndarray: + v = np.roll(polygon, -1, axis=0) - polygon + n = np.column_stack([v[:, 1], -v[:, 0]]) + n = (n + np.roll(n, 1, axis=0)) / 2 + return n -def poly_normals(polygon : torch.Tensor | np.ndarray) -> torch.Tensor: +@torch.compile +def _poly_normals_tensor(polygon: torch.Tensor) -> torch.Tensor: + v = torch.roll(polygon, shifts=-1, dims=0) - polygon + n = torch.column_stack([v[:, 1], -v[:, 0]]) + n = (n + torch.roll(n, shifts=1, dims=0)) / 2 + return n + +def poly_normals(polygon : V) -> V: """Calculate the normals of a polygon. Args: @@ -256,18 +308,14 @@ def poly_normals(polygon : torch.Tensor | np.ndarray) -> torch.Tensor: Returns: A tensor of shape (n, 2), where n is the number of vertices and the 2 columns are the x and y coordinates of the normals. - """ - v = np.roll(polygon, -1, axis=0) - polygon - n = np.column_stack([v[:, 1], -v[:, 0]]) - n = (n + np.roll(n, 1, axis=0)) / 2 - return torch.as_tensor(n) + if isinstance(polygon, torch.Tensor): + return _poly_normals_tensor(polygon) + else: + return _poly_normals_arr(polygon) -def linear_interpolate( - poly : np.ndarray, - scale : int - ) -> np.ndarray: - """Linearly interpolates a N x 2 polygon to have N x scale vertices.""" + +def _linear_interpolate_arr(poly: np.ndarray, scale: int) -> np.ndarray: if scale < 1: raise ValueError(f"Scale must be at least 1, not {scale}") if len(poly) == 0: @@ -281,20 +329,53 @@ def linear_interpolate( new_poly[-scale:] = np.linspace(poly[-1], poly[0], scale, endpoint=False) return new_poly[~(new_poly == np.roll(new_poly, -1, axis=0)).all(axis=1)] -def scale_contour( # noqa: D103 - contour : np.ndarray, - scale : list[float | int] | np.ndarray | float | int, - expand_by_one : bool=False +@torch.compile +def _linear_interpolate_tensor(poly: torch.Tensor, scale: int) -> torch.Tensor: + if scale < 1: + raise ValueError(f"Scale must be at least 1, not {scale}") + if len(poly) == 0: + return poly + if scale == 1: + return poly + + new_poly = torch.zeros((poly.shape[0] * scale, 2), dtype=torch.float32, device=poly.device) + # Using vector math to precisely replicate np.linspace(..., endpoint=False) + weights = torch.arange(scale, dtype=torch.float32, device=poly.device).unsqueeze(1) / scale + for i in range(poly.shape[0] - 1): + new_poly[i*scale:(i+1)*scale] = poly[i] + (poly[i+1] - poly[i]) * weights + new_poly[-scale:] = poly[-1] + (poly[0] - poly[-1]) * weights + + mask = ~(new_poly == torch.roll(new_poly, shifts=-1, dims=0)).all(dim=1) + return new_poly[mask] + +def linear_interpolate( + poly: V, + scale: int | np.ndarray | torch.Tensor + ) -> V: + """Linearly interpolates a N x 2 polygon to have N x scale vertices.""" + if not isinstance(scale, int): + scale = int(scale.item()) + if isinstance(poly, torch.Tensor): + return _linear_interpolate_tensor(poly, scale) + else: + return _linear_interpolate_arr(poly, scale) + + +def _scale_contour_arr( + contour: np.ndarray, + scale: Sequence[float | int] | np.ndarray | torch.Tensor | float | int, + expand_by_one: bool=False ) -> np.ndarray: if len(contour.shape) != 2 or contour.shape[1] != 2: if contour.shape[0] == 2: contour = contour.reshape(1, 2) else: raise ValueError(f"Contour must be a Nx2 array, not {contour.shape}") + if isinstance(scale, (int, float)): scale = [scale, scale] - if isinstance(scale, list): - scale = np.array(scale, dtype=np.float32) + scale = np.asarray(scale, dtype=np.float32) + if len(scale) != 2: raise ValueError(f"Scale must be a scalar or a list of 2 scalars, not {scale}") @@ -304,11 +385,14 @@ def scale_contour( # noqa: D103 return np.round(contour * scale).astype(np.int32) if np.all(scale == 1): return contour + contour = contour * scale centroid = contour.mean(axis=0) n_interp = max(1, int(np.ceil(scale.max())) * 2) - contour = linear_interpolate(contour, n_interp) - contour_normals = poly_normals(contour) + + contour = _linear_interpolate_arr(contour, n_interp) + contour_normals = _poly_normals_arr(contour) + if expand_by_one: expand_one = np.sign(contour_normals) * (np.abs(contour_normals) > 0) contour -= expand_one @@ -322,8 +406,68 @@ def scale_contour( # noqa: D103 contour[contour_normals < 0] = np.ceil(contour[contour_normals < 0]) contour = contour.round() drift = centroid - contour.mean(axis=0) + return (contour + drift).round().astype(np.int32)[(n_interp // 2)::n_interp].copy() +def _scale_contour_tensor( + contour: torch.Tensor, + scale: Sequence[float | int] | np.ndarray | torch.Tensor | float | int, + expand_by_one: bool=False + ) -> torch.Tensor: + if len(contour.shape) != 2 or contour.shape[1] != 2: + if contour.shape[0] == 2: + contour = contour.reshape(1, 2) + else: + raise ValueError(f"Contour must be a Nx2 tensor, not {contour.shape}") + + if isinstance(scale, (int, float)): + scale = [scale, scale] + + scale = torch.as_tensor(scale, dtype=torch.float32, device=contour.device) + + if len(scale) != 2: + raise ValueError(f"Scale must be a scalar or a list of 2 scalars, not {scale}") + + if len(contour) == 0: + return contour + if len(contour) == 1: + return torch.round(contour * scale).to(torch.int32) + if torch.all(scale == 1): + return contour + + contour = contour * scale + centroid = contour.mean(dim=0) + n_interp = max(1, int(torch.ceil(scale.max()).item()) * 2) + + contour = _linear_interpolate_tensor(contour, n_interp) + contour_normals = _poly_normals_tensor(contour) + + if expand_by_one: + expand_one = torch.sign(contour_normals) * (torch.abs(contour_normals) > 0) + contour -= expand_one + + if scale[0] < 1: + contour[:, 0] += contour_normals[:, 0] / scale[0] / 2 + if scale[1] < 1: + contour[:, 1] += contour_normals[:, 1] / scale[1] / 2 + + contour[contour_normals > 0] = torch.floor(contour[contour_normals > 0]) + contour[contour_normals < 0] = torch.ceil(contour[contour_normals < 0]) + contour = contour.round() + drift = centroid - contour.mean(dim=0) + + return torch.round(contour + drift).to(torch.int32)[(n_interp // 2)::n_interp].clone() + +def scale_contour( # noqa: D103 + contour: V, + scale: Sequence[float | int] | np.ndarray | torch.Tensor | float | int, + expand_by_one: bool=False + ) -> V: + if isinstance(contour, torch.Tensor): + return _scale_contour_tensor(contour, scale, expand_by_one) + else: + return _scale_contour_arr(contour, scale, expand_by_one) + def resize_masks( masks : torch.Tensor, new_shape : tuple[int, int] | list[int] | int diff --git a/src/flat_bug/nms.py b/src/flat_bug/nms.py index 67e500f..ad0e5b8 100644 --- a/src/flat_bug/nms.py +++ b/src/flat_bug/nms.py @@ -87,7 +87,7 @@ def ios_boxes( ios = intersections / (sareas + 1e-6) return ios -@torch.jit.script +@torch.compile def iou_masks( m1s : torch.Tensor, m2s : torch.Tensor, @@ -144,7 +144,7 @@ def iou_masks( return intersections / (unions + 1e-6) -@torch.jit.script +@torch.compile def ios_masks( # noqa: D103 m1s : torch.Tensor, m2s : torch.Tensor, @@ -458,7 +458,7 @@ def fancy_nms( else: return objects[indices], scores[indices] -# @torch.jit.script +# @torch.compile def nms_masks_( masks : torch.Tensor, scores : torch.Tensor, @@ -689,7 +689,7 @@ def nms_masks( )=iou_masks, overlap_fn_boxes : Callable[..., torch.Tensor] | str | None=None ) -> torch.Tensor: ... -# @torch.jit.script +# @torch.compile def nms_masks( masks : torch.Tensor, scores : torch.Tensor, diff --git a/src/flat_bug/predictor.py b/src/flat_bug/predictor.py index 876b980..bc23036 100644 --- a/src/flat_bug/predictor.py +++ b/src/flat_bug/predictor.py @@ -120,7 +120,7 @@ class Prepared_Results: def __init__(self, predictions: ResultsWithTiles, scale: tuple[float, float], device, dtype): # noqa: D107 self.wh_scale = torch.tensor(scale, device=device, dtype=dtype).unsqueeze(0) self._predictions = predictions - assert self._predictions.boxes is not None and isinstance(self._predictions.boxes, torch.Tensor) + assert self._predictions.boxes is not None and isinstance(self._predictions.boxes.data, torch.Tensor) self._predictions.boxes.data[:, :4] /= self.wh_scale.repeat(1, 2) self._predictions.polygons = self._predictions.polygons._apply( lambda poly : (poly + torch.roll(poly, 1, dims=0)) / (2 * self.wh_scale) @@ -240,7 +240,7 @@ def __init__( # Device and dtype are None by default, but they may be set by the user or # passed by **kwargs, so we check if they are None and if so set them to the default values # Then we check that they are the same for all predictions and the image (if they are not None) - if predictions is not None and len(predictions) >= 0: + if predictions is not None and len(predictions) > 0: # Check that all predictions have the same device and dtype elem = predictions[0] if self.device is None: @@ -269,7 +269,7 @@ def __init__( self.image = image.to(self.device) # Combine the predictions - if predictions is not None and len(predictions) >= 0: + if predictions is not None and len(predictions) > 0: self._combine_predictions(predictions) else: # If there are no predictions, set other attributes to empty tensors or lists. @@ -1210,8 +1210,9 @@ def serialize( with open(json_path, 'w') as f: json.dump(json_data, f) + @classmethod def load( - self, + cls, data: str | dict, device : DeviceLikeType | None=None, dtype : torch.types._dtype | None=None @@ -1249,18 +1250,18 @@ def load( dtype = torch.float32 empty_image = torch.zeros((3, data["image_height"], data["image_width"]), device=device, dtype=dtype) + 255 # type: ignore - self.__init__(image=empty_image, device=device, dtype=dtype) - setattr(self, "PREFER_POLYGONS", True) # Since we only store contours in the .json file, we prefer polygons on loading + inst = cls(image=empty_image, device=device, dtype=dtype) + setattr(inst, "PREFER_POLYGONS", True) # Since we only store contours in the .json file, we prefer polygons on loading # Load constants for k, v in data.items(): - if k in self.CONSTANTS: - setattr(self, k, v) + if k in inst.CONSTANTS: + setattr(inst, k, v) # Load the data for k, v in data.items(): # Skip constants in second round - if k in self.CONSTANTS: + if k in inst.CONSTANTS: continue # Skip dynamically computed class property attributes if k in ["areas"]: @@ -1273,16 +1274,16 @@ def load( pass # Bounding boxes are easy (as usual) elif k == "boxes": - v = torch.tensor(v, device=self.device, dtype=self.dtype) + v = torch.tensor(v, device=inst.device, dtype=inst.dtype) # While masks are a bit more complicated # Confidences and classes are 1-d tensors (arrays) elif k in ["confs", "classes"]: - v = torch.tensor(v, device=self.device, dtype=self.dtype) + v = torch.tensor(v, device=inst.device, dtype=inst.dtype) else: raise RuntimeError(f"Unknown key in json file: {k}") - setattr(self, k, v) + setattr(inst, k, v) - return self + return inst def save( self, diff --git a/src/flat_bug/tests/assets/metadata_ALUS_Non-miteArachnids_Unknown_2020_11_03_4545_UUID_ChangeThisTEMPORARY.json b/src/flat_bug/tests/assets/metadata_ALUS_Non-miteArachnids_Unknown_2020_11_03_4545_UUID_ChangeThisTEMPORARY.json deleted file mode 100644 index 6e0e0be..0000000 --- a/src/flat_bug/tests/assets/metadata_ALUS_Non-miteArachnids_Unknown_2020_11_03_4545_UUID_ChangeThisTEMPORARY.json +++ /dev/null @@ -1 +0,0 @@ -{"boxes": [[1058, 19, 1202, 183], [587, 606, 957, 936], [1567, 1343, 1661, 1413], [181, 355, 501, 689], [1092, 700, 1364, 1022], [1735, 867, 1939, 1172], [533, 173, 979, 566], [187, 1021, 935, 1699], [710, 1118, 1346, 1785], [1208, 1494, 1594, 1869]], "contours": [[[1174.666748046875, 1165.3333740234375, 1156.0, 1154.0, 1152.0, 1150.0, 1148.0, 1146.0, 1144.0, 1142.0, 1140.0, 1138.0, 1136.0, 1134.0, 1132.0, 1130.0, 1128.0, 1126.0, 1123.3333740234375, 1119.3333740234375, 1115.3333740234375, 1109.3333740234375, 1104.0, 1102.0, 1100.0, 1098.0, 1096.0, 1094.0, 1092.0, 1090.0, 1088.0, 1086.0, 1084.0, 1082.0, 1080.0, 1078.0, 1075.3333740234375, 1073.3333740234375, 1072.0, 1070.0, 1067.3333740234375, 1065.3333740234375, 1064.0, 1065.3333740234375, 1067.3333740234375, 1069.3333740234375, 1070.666748046875, 1068.666748046875, 1066.666748046875, 1068.666748046875, 1070.666748046875, 1072.666748046875, 1074.666748046875, 1076.666748046875, 1078.666748046875, 1080.666748046875, 1082.666748046875, 1084.666748046875, 1086.666748046875, 1088.666748046875, 1090.666748046875, 1092.666748046875, 1095.3333740234375, 1099.3333740234375, 1103.3333740234375, 1106.0, 1108.0, 1110.0, 1112.0, 1114.0, 1116.0, 1117.3333740234375, 1119.3333740234375, 1121.3333740234375, 1123.3333740234375, 1126.0, 1128.0, 1131.3333740234375, 1135.3333740234375, 1138.0, 1140.0, 1142.0, 1144.0, 1145.3333740234375, 1147.3333740234375, 1151.3333740234375, 1154.666748046875, 1156.666748046875, 1158.666748046875, 1160.666748046875, 1162.666748046875, 1164.666748046875, 1166.666748046875, 1168.666748046875, 1170.666748046875, 1172.666748046875, 1174.666748046875, 1176.666748046875, 1178.666748046875, 1180.666748046875, 1183.3333740234375, 1187.3333740234375, 1190.666748046875, 1192.666748046875, 1194.666748046875, 1192.666748046875, 1190.666748046875, 1188.666748046875, 1186.666748046875, 1185.3333740234375, 1183.3333740234375, 1181.3333740234375, 1179.3333740234375, 1176.666748046875], [31.333343505859375, 28.0, 29.333343505859375, 31.333343505859375, 33.333343505859375, 35.333343505859375, 37.333343505859375, 39.333343505859375, 41.333343505859375, 43.333343505859375, 45.333343505859375, 47.333343505859375, 49.333343505859375, 51.333343505859375, 53.333343505859375, 55.333343505859375, 57.333343505859375, 59.333343505859375, 62.0, 64.0, 66.0, 68.0, 73.33334350585938, 79.33334350585938, 81.33334350585938, 83.33334350585938, 85.33334350585938, 87.33334350585938, 89.33334350585938, 91.33334350585938, 93.33334350585938, 95.33334350585938, 97.33334350585938, 99.33334350585938, 101.33334350585938, 103.33334350585938, 106.0, 108.0, 111.33334350585938, 115.33334350585938, 118.0, 120.0, 131.3333740234375, 142.66668701171875, 144.66668701171875, 146.66668701171875, 143.3333740234375, 139.3333740234375, 135.3333740234375, 131.3333740234375, 127.33334350585938, 123.33334350585938, 119.33334350585938, 115.33334350585938, 113.33334350585938, 111.33334350585938, 109.33334350585938, 107.33334350585938, 105.33334350585938, 103.33334350585938, 101.33334350585938, 99.33334350585938, 96.66668701171875, 94.66668701171875, 104.66668701171875, 115.33334350585938, 119.33334350585938, 123.33334350585938, 129.3333740234375, 135.3333740234375, 139.3333740234375, 142.66668701171875, 144.66668701171875, 146.66668701171875, 148.66668701171875, 151.3333740234375, 153.3333740234375, 154.66668701171875, 156.66668701171875, 159.3333740234375, 163.3333740234375, 167.3333740234375, 171.3333740234375, 174.66668701171875, 176.66668701171875, 178.66668701171875, 175.3333740234375, 171.3333740234375, 165.3333740234375, 159.3333740234375, 147.3333740234375, 135.3333740234375, 129.3333740234375, 123.33334350585938, 119.33334350585938, 115.33334350585938, 107.33334350585938, 99.33334350585938, 97.33334350585938, 95.33334350585938, 92.66668701171875, 90.66668701171875, 89.33334350585938, 87.33334350585938, 75.33334350585938, 63.333343505859375, 59.333343505859375, 55.333343505859375, 49.333343505859375, 44.0, 42.0, 40.0, 38.0, 35.333343505859375]], [[918.666748046875, 903.3333740234375, 887.3333740234375, 883.3333740234375, 879.3333740234375, 873.3333740234375, 867.3333740234375, 863.3333740234375, 860.0, 858.0, 855.3333740234375, 851.3333740234375, 848.0, 846.0, 844.0, 842.0, 839.3333740234375, 837.3333740234375, 836.0, 834.0, 832.0, 833.3333740234375, 835.3333740234375, 837.3333740234375, 839.3333740234375, 841.3333740234375, 843.3333740234375, 846.0, 848.0, 851.3333740234375, 855.3333740234375, 857.3333740234375, 859.3333740234375, 861.3333740234375, 863.3333740234375, 866.0, 868.0, 869.3333740234375, 871.3333740234375, 875.3333740234375, 878.666748046875, 877.3333740234375, 875.3333740234375, 873.3333740234375, 871.3333740234375, 869.3333740234375, 867.3333740234375, 865.3333740234375, 863.3333740234375, 861.3333740234375, 859.3333740234375, 857.3333740234375, 855.3333740234375, 853.3333740234375, 851.3333740234375, 848.666748046875, 846.666748046875, 843.3333740234375, 839.3333740234375, 837.3333740234375, 835.3333740234375, 836.666748046875, 839.3333740234375, 847.3333740234375, 855.3333740234375, 861.3333740234375, 867.3333740234375, 871.3333740234375, 875.3333740234375, 879.3333740234375, 883.3333740234375, 891.3333740234375, 899.3333740234375, 902.0, 904.0, 902.0, 900.0, 898.0, 896.0, 898.0, 900.0, 898.0, 895.3333740234375, 889.3333740234375, 883.3333740234375, 865.3333740234375, 847.3333740234375, 841.3333740234375, 835.3333740234375, 827.3333740234375, 819.3333740234375, 815.3333740234375, 811.3333740234375, 805.3333740234375, 799.3333740234375, 795.3333740234375, 791.3333740234375, 785.3333740234375, 779.3333740234375, 777.3333740234375, 775.3333740234375, 773.3333740234375, 771.3333740234375, 769.3333740234375, 767.3333740234375, 764.666748046875, 762.666748046875, 761.3333740234375, 759.3333740234375, 756.666748046875, 754.666748046875, 747.3333740234375, 739.3333740234375, 733.3333740234375, 727.3333740234375, 719.3333740234375, 711.3333740234375, 709.3333740234375, 708.0, 706.0, 704.0, 702.0, 699.3333740234375, 687.3333740234375, 675.3333740234375, 667.3333740234375, 659.3333740234375, 653.3333740234375, 647.3333740234375, 641.3333740234375, 635.3333740234375, 621.3333740234375, 608.0, 606.0, 604.0, 602.0, 600.0, 598.0, 596.0, 603.3333740234375, 611.3333740234375, 617.3333740234375, 623.3333740234375, 626.0, 628.0, 629.3333740234375, 631.3333740234375, 633.3333740234375, 635.3333740234375, 638.0, 640.0, 643.3333740234375, 647.3333740234375, 650.0, 652.0, 650.0, 648.0, 646.0, 644.0, 642.0, 639.3333740234375, 635.3333740234375, 631.3333740234375, 629.3333740234375, 627.3333740234375, 624.6666870117188, 622.6666870117188, 621.3333740234375, 619.3333740234375, 617.3333740234375, 616.0, 614.0, 612.0, 614.0, 616.0, 614.0, 612.0, 610.0, 607.3333740234375, 605.3333740234375, 604.0, 605.3333740234375, 606.6666870117188, 608.6666870117188, 610.6666870117188, 612.6666870117188, 615.3333740234375, 617.3333740234375, 618.6666870117188, 620.6666870117188, 622.6666870117188, 624.6666870117188, 626.6666870117188, 628.6666870117188, 630.6666870117188, 632.6666870117188, 635.3333740234375, 639.3333740234375, 642.666748046875, 644.666748046875, 646.666748046875, 648.666748046875, 651.3333740234375, 653.3333740234375, 654.666748046875, 656.666748046875, 658.666748046875, 660.666748046875, 662.666748046875, 664.666748046875, 667.3333740234375, 673.3333740234375, 679.3333740234375, 683.3333740234375, 687.3333740234375, 691.3333740234375, 695.3333740234375, 699.3333740234375, 703.3333740234375, 707.3333740234375, 711.3333740234375, 717.3333740234375, 722.666748046875, 724.666748046875, 727.3333740234375, 731.3333740234375, 734.666748046875, 736.666748046875, 739.3333740234375, 743.3333740234375, 746.666748046875, 748.666748046875, 750.666748046875, 752.666748046875, 754.666748046875, 756.666748046875, 759.3333740234375, 761.3333740234375, 762.666748046875, 766.666748046875, 771.3333740234375, 777.3333740234375, 783.3333740234375, 789.3333740234375, 795.3333740234375, 801.3333740234375, 807.3333740234375, 813.3333740234375, 819.3333740234375, 825.3333740234375, 831.3333740234375, 837.3333740234375, 843.3333740234375, 846.0, 848.0, 846.0, 844.0, 842.0, 840.0, 838.0, 836.0, 834.0, 832.0, 830.0, 827.3333740234375, 825.3333740234375, 824.0, 822.0, 820.0, 818.0, 816.0, 814.0, 812.0, 810.0, 808.0, 806.0, 804.0, 802.0, 800.0, 798.0, 795.3333740234375, 791.3333740234375, 788.0, 786.0, 784.0, 787.3333740234375, 791.3333740234375, 795.3333740234375, 798.666748046875, 800.666748046875, 803.3333740234375, 805.3333740234375, 806.666748046875, 808.666748046875, 810.666748046875, 812.666748046875, 814.666748046875, 816.666748046875, 818.666748046875, 820.666748046875, 822.666748046875, 824.666748046875, 826.666748046875, 828.666748046875, 830.666748046875, 832.666748046875, 834.666748046875, 836.666748046875, 839.3333740234375, 843.3333740234375, 846.666748046875, 848.666748046875, 851.3333740234375, 855.3333740234375, 859.3333740234375, 865.3333740234375, 871.3333740234375, 875.3333740234375, 879.3333740234375, 883.3333740234375, 887.3333740234375, 893.3333740234375, 899.3333740234375, 905.3333740234375, 911.3333740234375, 913.3333740234375, 915.3333740234375, 918.0, 920.0, 923.3333740234375, 927.3333740234375, 939.3333740234375, 950.666748046875, 947.3333740234375, 943.3333740234375, 937.3333740234375, 931.3333740234375, 927.3333740234375, 923.3333740234375, 921.3333740234375, 919.3333740234375, 916.666748046875, 914.666748046875, 916.666748046875, 919.3333740234375, 921.3333740234375, 922.666748046875, 917.3333740234375, 911.3333740234375, 908.666748046875, 906.666748046875, 904.666748046875, 902.666748046875, 904.666748046875, 906.666748046875, 908.666748046875, 910.666748046875, 912.666748046875, 914.666748046875, 916.666748046875], [621.3333740234375, 612.0, 614.0, 616.0, 618.0, 620.0, 622.0, 624.0, 625.3333740234375, 627.3333740234375, 630.0, 632.0, 633.3333740234375, 635.3333740234375, 637.3333740234375, 639.3333740234375, 642.0, 644.0, 647.3333740234375, 651.3333740234375, 657.3333740234375, 662.666748046875, 664.666748046875, 666.666748046875, 668.666748046875, 670.666748046875, 672.666748046875, 675.3333740234375, 679.3333740234375, 682.666748046875, 684.666748046875, 686.666748046875, 688.666748046875, 690.666748046875, 692.666748046875, 695.3333740234375, 699.3333740234375, 702.666748046875, 704.666748046875, 706.666748046875, 701.3333740234375, 696.0, 694.0, 692.0, 690.0, 688.0, 686.0, 684.0, 682.0, 680.0, 678.0, 676.0, 674.0, 672.0, 670.0, 667.3333740234375, 665.3333740234375, 664.0, 660.0, 656.0, 654.0, 651.3333740234375, 648.666748046875, 646.666748046875, 644.666748046875, 642.666748046875, 640.666748046875, 638.6666870117188, 636.6666870117188, 634.6666870117188, 632.6666870117188, 630.6666870117188, 632.6666870117188, 635.3333740234375, 643.3333740234375, 651.3333740234375, 663.3333740234375, 675.3333740234375, 701.3333740234375, 727.3333740234375, 733.3333740234375, 739.3333740234375, 742.0, 744.0, 746.0, 748.0, 746.0, 744.0, 746.0, 748.0, 750.0, 752.0, 754.0, 756.0, 758.0, 760.0, 762.0, 764.0, 762.0, 760.0, 758.0, 756.0, 754.0, 752.0, 750.0, 747.3333740234375, 743.3333740234375, 740.0, 738.0, 735.3333740234375, 733.3333740234375, 732.0, 734.0, 736.0, 738.0, 740.0, 738.0, 736.0, 737.3333740234375, 739.3333740234375, 741.3333740234375, 743.3333740234375, 746.0, 748.0, 746.0, 744.0, 742.0, 740.0, 738.0, 736.0, 734.0, 732.0, 735.3333740234375, 739.3333740234375, 743.3333740234375, 747.3333740234375, 755.3333740234375, 763.3333740234375, 791.3333740234375, 818.666748046875, 816.666748046875, 814.666748046875, 822.666748046875, 831.3333740234375, 835.3333740234375, 838.666748046875, 840.666748046875, 842.666748046875, 844.666748046875, 847.3333740234375, 849.3333740234375, 850.666748046875, 852.666748046875, 855.3333740234375, 863.3333740234375, 871.3333740234375, 875.3333740234375, 879.3333740234375, 881.3333740234375, 883.3333740234375, 886.0, 888.0, 890.0, 892.0, 890.0, 887.3333740234375, 883.3333740234375, 880.0, 878.0, 876.0, 877.3333740234375, 879.3333740234375, 889.3333740234375, 899.3333740234375, 905.3333740234375, 911.3333740234375, 915.3333740234375, 919.3333740234375, 922.0, 924.0, 927.3333740234375, 930.666748046875, 929.3333740234375, 927.3333740234375, 925.3333740234375, 923.3333740234375, 920.666748046875, 918.666748046875, 915.3333740234375, 911.3333740234375, 909.3333740234375, 907.3333740234375, 905.3333740234375, 903.3333740234375, 901.3333740234375, 899.3333740234375, 896.666748046875, 894.666748046875, 893.3333740234375, 891.3333740234375, 889.3333740234375, 887.3333740234375, 884.666748046875, 882.666748046875, 879.3333740234375, 875.3333740234375, 865.3333740234375, 855.3333740234375, 853.3333740234375, 851.3333740234375, 848.666748046875, 846.666748046875, 844.666748046875, 842.666748046875, 840.666748046875, 838.666748046875, 836.666748046875, 834.666748046875, 832.666748046875, 830.666748046875, 828.666748046875, 826.666748046875, 825.3333740234375, 823.3333740234375, 820.666748046875, 818.666748046875, 817.3333740234375, 815.3333740234375, 812.666748046875, 810.666748046875, 807.3333740234375, 803.3333740234375, 801.3333740234375, 799.3333740234375, 797.3333740234375, 795.3333740234375, 792.666748046875, 790.666748046875, 787.3333740234375, 783.3333740234375, 780.666748046875, 778.666748046875, 776.666748046875, 774.666748046875, 772.666748046875, 770.666748046875, 768.666748046875, 766.666748046875, 764.666748046875, 762.666748046875, 760.666748046875, 758.666748046875, 760.666748046875, 763.3333740234375, 765.3333740234375, 767.3333740234375, 769.3333740234375, 771.3333740234375, 773.3333740234375, 775.3333740234375, 777.3333740234375, 779.3333740234375, 781.3333740234375, 783.3333740234375, 786.0, 788.0, 791.3333740234375, 795.3333740234375, 799.3333740234375, 803.3333740234375, 807.3333740234375, 811.3333740234375, 817.3333740234375, 823.3333740234375, 827.3333740234375, 831.3333740234375, 837.3333740234375, 843.3333740234375, 845.3333740234375, 847.3333740234375, 850.0, 852.0, 853.3333740234375, 855.3333740234375, 859.3333740234375, 862.666748046875, 860.666748046875, 858.666748046875, 857.3333740234375, 855.3333740234375, 852.666748046875, 850.666748046875, 847.3333740234375, 843.3333740234375, 839.3333740234375, 835.3333740234375, 831.3333740234375, 827.3333740234375, 821.3333740234375, 815.3333740234375, 811.3333740234375, 807.3333740234375, 803.3333740234375, 799.3333740234375, 795.3333740234375, 791.3333740234375, 789.3333740234375, 787.3333740234375, 784.666748046875, 782.666748046875, 781.3333740234375, 779.3333740234375, 776.666748046875, 774.666748046875, 772.666748046875, 770.666748046875, 768.666748046875, 766.666748046875, 764.666748046875, 762.666748046875, 760.666748046875, 758.666748046875, 756.666748046875, 754.666748046875, 758.666748046875, 762.666748046875, 764.666748046875, 767.3333740234375, 769.3333740234375, 770.666748046875, 772.666748046875, 774.666748046875, 771.3333740234375, 768.0, 764.0, 760.0, 762.0, 764.0, 762.0, 760.0, 758.0, 755.3333740234375, 753.3333740234375, 751.3333740234375, 748.666748046875, 746.666748046875, 743.3333740234375, 740.0, 738.0, 735.3333740234375, 725.3333740234375, 715.3333740234375, 703.3333740234375, 691.3333740234375, 677.3333740234375, 663.3333740234375, 655.3333740234375, 647.3333740234375, 639.3333740234375, 631.3333740234375]], [[1575.3333740234375, 1572.0, 1575.3333740234375, 1579.3333740234375, 1593.3333740234375, 1607.3333740234375, 1609.3333740234375, 1611.3333740234375, 1613.3333740234375, 1615.3333740234375, 1618.0, 1620.0, 1625.3333740234375, 1631.3333740234375, 1635.3333740234375, 1638.666748046875, 1640.666748046875, 1642.666748046875, 1644.666748046875, 1646.666748046875, 1648.666748046875, 1651.3333740234375, 1653.3333740234375, 1654.666748046875, 1652.666748046875, 1650.666748046875, 1649.3333740234375, 1647.3333740234375, 1643.3333740234375, 1639.3333740234375, 1637.3333740234375, 1635.3333740234375, 1633.3333740234375, 1631.3333740234375, 1628.666748046875, 1626.666748046875, 1621.3333740234375, 1616.0, 1614.0, 1611.3333740234375, 1595.3333740234375, 1579.3333740234375], [1360.0, 1365.3333740234375, 1370.666748046875, 1372.666748046875, 1374.666748046875, 1384.666748046875, 1394.666748046875, 1396.666748046875, 1398.666748046875, 1400.666748046875, 1403.3333740234375, 1405.3333740234375, 1406.666748046875, 1404.666748046875, 1402.666748046875, 1401.3333740234375, 1399.3333740234375, 1397.3333740234375, 1395.3333740234375, 1393.3333740234375, 1391.3333740234375, 1388.666748046875, 1386.666748046875, 1381.3333740234375, 1375.3333740234375, 1371.3333740234375, 1368.0, 1366.0, 1364.0, 1362.0, 1360.0, 1358.0, 1356.0, 1354.0, 1351.3333740234375, 1349.3333740234375, 1348.0, 1349.3333740234375, 1351.3333740234375, 1354.0, 1356.0, 1358.0]], [[497.42889404296875, 493.02178955078125, 487.73321533203125, 482.44464111328125, 478.03741455078125, 475.39312744140625, 471.86749267578125, 466.57891845703125, 461.29034423828125, 456.00177001953125, 451.5946044921875, 448.9503173828125, 445.42462158203125, 440.13604736328125, 434.84747314453125, 429.55889892578125, 425.1517333984375, 422.5074462890625, 418.98175048828125, 415.45599365234375, 412.81170654296875, 410.16741943359375, 407.52313232421875, 405.76031494140625, 403.11602783203125, 395.18316650390625, 387.25030517578125, 381.96173095703125, 377.5545654296875, 374.9102783203125, 371.38458251953125, 366.09600830078125, 361.6888427734375, 359.0445556640625, 356.4002685546875, 353.7559814453125, 351.1116943359375, 348.4674072265625, 344.94171142578125, 342.29742431640625, 340.5345458984375, 337.8902587890625, 335.2459716796875, 337.8902587890625, 340.5345458984375, 342.29742431640625, 344.94171142578125, 337.8902587890625, 329.07598876953125, 325.55029296875, 322.906005859375, 318.49884033203125, 313.2103271484375, 307.9217529296875, 303.5145263671875, 300.8702392578125, 298.2259521484375, 295.5816650390625, 292.9373779296875, 290.2930908203125, 286.76739501953125, 278.83453369140625, 270.90167236328125, 268.25738525390625, 265.61309814453125, 262.08740234375, 259.443115234375, 256.798828125, 254.154541015625, 256.798828125, 259.443115234375, 257.68023681640625, 255.03594970703125, 252.39166259765625, 250.6287841796875, 247.9844970703125, 245.3402099609375, 240.0516357421875, 234.7630615234375, 236.52593994140625, 239.17022705078125, 244.45880126953125, 249.74737548828125, 253.2730712890625, 255.9173583984375, 257.68023681640625, 260.32452392578125, 258.5616455078125, 255.9173583984375, 253.2730712890625, 249.74737548828125, 247.10308837890625, 245.3402099609375, 247.9844970703125, 250.6287841796875, 247.9844970703125, 244.45880126953125, 241.81451416015625, 240.0516357421875, 237.4073486328125, 233.88165283203125, 231.23736572265625, 229.4744873046875, 226.8302001953125, 224.1859130859375, 221.5416259765625, 218.8973388671875, 216.2530517578125, 212.72735595703125, 210.08306884765625, 208.3201904296875, 205.6759033203125, 202.15020751953125, 199.50592041015625, 197.7430419921875, 195.0987548828125, 192.4544677734375, 189.8101806640625, 187.1658935546875, 188.92877197265625, 191.57305908203125, 194.21734619140625, 196.86163330078125, 200.3873291015625, 203.0316162109375, 210.08306884765625, 217.134521484375, 215.37164306640625, 212.72735595703125, 207.43878173828125, 202.15020751953125, 198.62451171875, 195.980224609375, 198.62451171875, 201.268798828125, 203.9130859375, 207.43878173828125, 210.08306884765625, 211.845947265625, 214.490234375, 218.01593017578125, 228.59307861328125, 239.17022705078125, 244.45880126953125, 248.865966796875, 243.577392578125, 238.288818359375, 240.93310546875, 243.577392578125, 246.2216796875, 248.865966796875, 251.51025390625, 254.154541015625, 256.798828125, 259.443115234375, 262.08740234375, 265.61309814453125, 273.54595947265625, 281.47882080078125, 286.76739501953125, 292.05596923828125, 299.9888916015625, 307.9217529296875, 311.4473876953125, 314.0916748046875, 315.85455322265625, 318.49884033203125, 322.0245361328125, 324.6688232421875, 329.07598876953125, 334.36456298828125, 342.29742431640625, 350.23028564453125, 353.7559814453125, 356.4002685546875, 358.16314697265625, 359.926025390625, 362.5703125, 365.214599609375, 367.85888671875, 371.38458251953125, 376.67315673828125, 381.96173095703125, 384.60601806640625, 387.25030517578125, 390.7760009765625, 393.4202880859375, 396.0645751953125, 398.7088623046875, 400.47174072265625, 403.11602783203125, 405.76031494140625, 408.40460205078125, 411.9302978515625, 414.5745849609375, 418.98175048828125, 423.38885498046875, 426.03314208984375, 429.55889892578125, 432.20318603515625, 433.96600341796875, 436.61029052734375, 440.13604736328125, 442.78033447265625, 444.54315185546875, 447.18743896484375, 449.83172607421875, 452.47601318359375, 455.12030029296875, 457.76458740234375, 460.40887451171875, 456.00177001953125, 450.71319580078125, 444.54315185546875, 439.25457763671875, 436.61029052734375, 433.96600341796875, 431.32171630859375, 428.67742919921875, 426.91461181640625, 424.27032470703125, 426.03314208984375, 428.67742919921875, 431.32171630859375, 433.96600341796875, 436.61029052734375, 439.25457763671875, 441.89886474609375, 444.54315185546875, 447.18743896484375, 449.83172607421875, 452.47601318359375, 456.00177001953125, 458.64605712890625, 460.40887451171875, 463.05316162109375, 465.69744873046875, 468.34173583984375, 470.98602294921875, 473.63031005859375, 477.15606689453125, 479.80035400390625, 481.56317138671875, 484.20745849609375, 486.85174560546875, 489.49603271484375, 493.02178955078125, 495.66607666015625], [392.03790283203125, 387.633544921875, 390.276123046875, 392.918701171875, 394.68048095703125, 397.32305908203125, 400.846435546875, 403.489013671875, 406.131591796875, 408.774169921875, 410.53594970703125, 413.17852783203125, 416.70196533203125, 419.34454345703125, 421.98712158203125, 424.62969970703125, 426.3914794921875, 429.0340576171875, 421.98712158203125, 413.17852783203125, 394.68048095703125, 376.18243408203125, 370.89727783203125, 366.492919921875, 363.850341796875, 361.207763671875, 363.850341796875, 366.492919921875, 368.25469970703125, 370.89727783203125, 374.420654296875, 377.063232421875, 378.82501220703125, 381.46759033203125, 384.11016845703125, 386.75274658203125, 389.39532470703125, 392.03790283203125, 395.561279296875, 398.203857421875, 402.60821533203125, 407.89337158203125, 421.10626220703125, 434.3192138671875, 442.2469482421875, 449.29376220703125, 451.9364013671875, 455.4598388671875, 453.69805908203125, 450.1746826171875, 444.8895263671875, 440.48516845703125, 443.12774658203125, 445.77032470703125, 447.5321044921875, 450.1746826171875, 455.4598388671875, 460.7449951171875, 463.3875732421875, 466.0301513671875, 469.55352783203125, 472.19610595703125, 469.55352783203125, 466.91094970703125, 464.26837158203125, 460.7449951171875, 452.8172607421875, 444.8895263671875, 429.0340576171875, 413.17852783203125, 407.89337158203125, 403.489013671875, 400.846435546875, 398.203857421875, 402.60821533203125, 407.89337158203125, 431.6766357421875, 455.4598388671875, 460.7449951171875, 465.1492919921875, 467.7918701171875, 470.4344482421875, 473.0770263671875, 476.6004638671875, 481.8856201171875, 486.2899169921875, 488.9324951171875, 492.45599365234375, 495.09857177734375, 497.74114990234375, 501.2645263671875, 503.9071044921875, 510.95404052734375, 518.8817749023438, 526.8095092773438, 534.7372436523438, 538.2606201171875, 540.9031982421875, 545.3075561523438, 550.5927124023438, 554.1161499023438, 556.7587280273438, 561.1630859375, 566.4482421875, 571.7333984375, 577.0185546875, 582.3037109375, 587.5888671875, 591.1122436523438, 593.7548217773438, 598.1591796875, 603.4443359375, 606.9677124023438, 609.6103515625, 614.0146484375, 619.2998657226562, 624.5850219726562, 629.8701782226562, 648.3682861328125, 665.9853515625, 668.6279296875, 671.2705078125, 673.9130859375, 677.4366455078125, 680.0792236328125, 681.8409423828125, 680.0792236328125, 678.3173828125, 675.6748046875, 673.0322265625, 670.3896484375, 666.8663330078125, 648.3682861328125, 629.8701782226562, 624.5850219726562, 619.2998657226562, 615.7763671875, 613.1337890625, 608.7294921875, 603.4443359375, 599.9208984375, 597.2783203125, 594.6357421875, 591.9931640625, 587.5888671875, 582.3037109375, 577.0185546875, 571.7333984375, 563.8056640625, 555.8779296875, 553.2353515625, 550.5927124023438, 547.9501342773438, 545.3075561523438, 542.6649780273438, 540.0223999023438, 536.4989624023438, 533.8563842773438, 531.2138061523438, 528.5712280273438, 525.9286499023438, 523.2860717773438, 525.9286499023438, 529.4520874023438, 534.7372436523438, 539.1415405273438, 541.7841186523438, 545.3075561523438, 547.9501342773438, 549.7118530273438, 552.3544311523438, 554.9970092773438, 557.6395874023438, 561.1630859375, 566.4482421875, 570.8524780273438, 569.0908203125, 566.4482421875, 561.1630859375, 555.8779296875, 552.3544311523438, 549.7118530273438, 552.3544311523438, 554.9970092773438, 557.6395874023438, 561.1630859375, 566.4482421875, 571.7333984375, 577.0185546875, 581.4228515625, 584.0654296875, 586.7080078125, 589.3505859375, 592.8740234375, 595.5166015625, 597.2783203125, 595.5166015625, 592.8740234375, 589.3505859375, 586.7080078125, 582.3037109375, 577.0185546875, 573.4950561523438, 570.8524780273438, 566.4482421875, 561.1630859375, 555.8779296875, 550.5927124023438, 545.3075561523438, 540.0223999023438, 534.7372436523438, 530.3328857421875, 527.6903076171875, 524.1669311523438, 518.8817749023438, 513.5966186523438, 508.31146240234375, 503.02630615234375, 495.09857177734375, 488.0516357421875, 485.4090576171875, 481.8856201171875, 476.6004638671875, 471.3153076171875, 468.6727294921875, 466.0301513671875, 463.3875732421875, 460.7449951171875, 458.1024169921875, 455.4598388671875, 452.8172607421875, 450.1746826171875, 446.65118408203125, 444.00860595703125, 439.6043701171875, 434.3192138671875, 431.6766357421875, 429.0340576171875, 426.3914794921875, 423.7489013671875, 420.22540283203125, 417.58282470703125, 413.17852783203125, 407.89337158203125, 405.25079345703125, 402.60821533203125, 399.08477783203125, 396.44219970703125]], [[1349.7706298828125, 1333.9049072265625, 1318.0391845703125, 1288.9520263671875, 1260.746337890625, 1258.10205078125, 1254.5762939453125, 1251.9320068359375, 1250.169189453125, 1247.52490234375, 1244.880615234375, 1242.236328125, 1239.592041015625, 1229.014892578125, 1217.5562744140625, 1212.2677001953125, 1206.9791259765625, 1203.4534912109375, 1200.8092041015625, 1198.1649169921875, 1195.5206298828125, 1191.1134033203125, 1186.706298828125, 1184.06201171875, 1180.5362548828125, 1177.8919677734375, 1176.129150390625, 1173.48486328125, 1170.840576171875, 1168.1962890625, 1165.552001953125, 1162.90771484375, 1160.263427734375, 1162.0263671875, 1164.670654296875, 1167.31494140625, 1169.9591064453125, 1172.6033935546875, 1175.2476806640625, 1177.8919677734375, 1180.5362548828125, 1184.06201171875, 1186.706298828125, 1184.06201171875, 1181.417724609375, 1178.7734375, 1176.129150390625, 1173.48486328125, 1169.9591064453125, 1164.670654296875, 1160.263427734375, 1157.619140625, 1154.974853515625, 1152.33056640625, 1148.804931640625, 1143.516357421875, 1139.109130859375, 1136.46484375, 1133.820556640625, 1131.17626953125, 1128.531982421875, 1125.8876953125, 1123.243408203125, 1120.59912109375, 1117.073486328125, 1106.496337890625, 1096.800537109375, 1107.377685546875, 1117.954833984375, 1120.59912109375, 1123.243408203125, 1130.294921875, 1138.227783203125, 1143.516357421875, 1147.9234619140625, 1150.5677490234375, 1153.2120361328125, 1155.8563232421875, 1158.5006103515625, 1161.1448974609375, 1164.670654296875, 1169.9591064453125, 1174.3663330078125, 1177.0106201171875, 1179.6549072265625, 1182.2991943359375, 1185.8248291015625, 1191.1134033203125, 1195.5206298828125, 1198.1649169921875, 1200.8092041015625, 1203.4534912109375, 1206.0977783203125, 1208.7420654296875, 1211.3863525390625, 1214.0306396484375, 1216.6749267578125, 1219.3192138671875, 1221.9635009765625, 1224.6077880859375, 1227.2520751953125, 1229.8963623046875, 1233.4219970703125, 1236.0662841796875, 1237.8292236328125, 1240.4735107421875, 1243.1177978515625, 1245.7620849609375, 1248.4063720703125, 1251.0506591796875, 1253.6949462890625, 1256.3392333984375, 1258.9835205078125, 1261.6278076171875, 1265.1534423828125, 1270.4420166015625, 1274.8492431640625, 1277.4935302734375, 1280.1378173828125, 1282.7821044921875, 1286.3077392578125, 1296.8848876953125, 1307.4620361328125, 1310.1063232421875, 1312.7506103515625, 1316.2763671875, 1318.920654296875, 1320.6834716796875, 1322.4464111328125, 1325.0906982421875, 1327.7349853515625, 1325.0906982421875, 1322.4464111328125, 1320.6834716796875, 1318.0391845703125, 1311.8692626953125, 1306.5806884765625, 1309.2249755859375, 1312.7506103515625, 1318.0391845703125, 1322.4464111328125, 1325.0906982421875, 1327.7349853515625, 1330.3792724609375, 1333.0235595703125, 1335.6678466796875, 1338.3121337890625, 1340.9564208984375, 1343.6007080078125, 1346.2449951171875, 1349.7706298828125, 1352.4149169921875, 1354.1778564453125, 1352.4149169921875], [712.6710205078125, 710.0284423828125, 707.3858642578125, 704.7432861328125, 706.5050048828125, 709.1475830078125, 712.6710205078125, 715.3135986328125, 722.3604736328125, 730.2882080078125, 746.1436767578125, 761.9991455078125, 767.2843017578125, 772.5694580078125, 776.0928955078125, 778.7354736328125, 781.3780517578125, 783.139892578125, 756.7139892578125, 730.2882080078125, 725.0030517578125, 720.5987548828125, 722.3604736328125, 725.0030517578125, 728.5264892578125, 731.1690673828125, 735.5733642578125, 740.8585205078125, 748.7862548828125, 756.7139892578125, 764.6417236328125, 772.5694580078125, 783.139892578125, 792.8292236328125, 795.4718017578125, 798.1143798828125, 800.7569580078125, 803.3995361328125, 806.042236328125, 808.684814453125, 811.327392578125, 814.850830078125, 825.421142578125, 835.991455078125, 846.561767578125, 857.132080078125, 870.344970703125, 883.557861328125, 889.723876953125, 895.009033203125, 896.770751953125, 899.413330078125, 902.055908203125, 904.6986083984375, 908.221923828125, 910.864501953125, 912.6263427734375, 915.2689208984375, 920.5540771484375, 925.8392333984375, 939.0521240234375, 952.2650146484375, 962.8353271484375, 973.4056396484375, 976.9290771484375, 979.5716552734375, 981.3333740234375, 983.9759521484375, 991.9036865234375, 999.8314208984375, 1007.7591552734375, 1014.80615234375, 1012.16357421875, 1009.52099609375, 1007.7591552734375, 1005.1165771484375, 1002.4739990234375, 999.8314208984375, 997.1888427734375, 994.5462646484375, 991.02294921875, 988.38037109375, 986.6185302734375, 983.9759521484375, 981.3333740234375, 978.6907958984375, 975.1673583984375, 972.5247802734375, 970.7630615234375, 968.1204833984375, 965.4779052734375, 962.8353271484375, 960.1927490234375, 957.5501708984375, 954.9075927734375, 952.2650146484375, 949.6224365234375, 946.9798583984375, 944.3372802734375, 941.6947021484375, 939.0521240234375, 936.4095458984375, 932.8861083984375, 930.243408203125, 925.8392333984375, 920.5540771484375, 915.2689208984375, 909.9837646484375, 904.6986083984375, 899.413330078125, 878.272705078125, 857.132080078125, 851.846923828125, 846.561767578125, 843.038330078125, 840.395751953125, 835.991455078125, 830.706298828125, 825.421142578125, 820.135986328125, 800.7569580078125, 782.2589111328125, 784.9014892578125, 787.5440673828125, 790.1866455078125, 793.710205078125, 814.850830078125, 835.110595703125, 833.348876953125, 830.706298828125, 812.208251953125, 793.710205078125, 788.425048828125, 784.0206298828125, 781.3780517578125, 777.854736328125, 769.9268798828125, 761.9991455078125, 758.4757080078125, 755.8331298828125, 754.0714111328125, 751.4288330078125, 748.7862548828125, 746.1436767578125, 743.5010986328125, 740.8585205078125, 738.2159423828125, 735.5733642578125, 732.9307861328125, 730.2882080078125, 726.7647705078125, 724.1221923828125, 719.7178955078125, 715.3135986328125]], [[1926.22509765625, 1920.9365234375, 1916.529296875, 1913.885009765625, 1911.24072265625, 1908.596435546875, 1905.9521484375, 1903.307861328125, 1900.66357421875, 1898.019287109375, 1894.49365234375, 1890.968017578125, 1888.32373046875, 1885.679443359375, 1883.03515625, 1881.272216796875, 1878.6279296875, 1875.102294921875, 1872.4580078125, 1870.695068359375, 1868.05078125, 1865.406494140625, 1862.76220703125, 1859.236572265625, 1856.59228515625, 1849.540771484375, 1842.4892578125, 1839.844970703125, 1837.20068359375, 1839.844970703125, 1842.4892578125, 1845.133544921875, 1847.77783203125, 1850.422119140625, 1852.18505859375, 1841.60791015625, 1831.03076171875, 1825.7421875, 1820.45361328125, 1815.1650390625, 1809.87646484375, 1801.943603515625, 1794.0107421875, 1788.72216796875, 1783.43359375, 1775.500732421875, 1767.56787109375, 1762.279296875, 1756.99072265625, 1751.7021484375, 1747.294921875, 1749.057861328125, 1751.7021484375, 1755.227783203125, 1757.8720703125, 1762.279296875, 1767.56787109375, 1770.212158203125, 1772.8564453125, 1776.382080078125, 1779.0263671875, 1776.382080078125, 1772.8564453125, 1756.99072265625, 1742.00634765625, 1743.769287109375, 1746.41357421875, 1770.212158203125, 1794.0107421875, 1799.29931640625, 1804.587890625, 1802.824951171875, 1799.29931640625, 1796.655029296875, 1794.89208984375, 1796.655029296875, 1799.29931640625, 1802.824951171875, 1805.46923828125, 1809.87646484375, 1815.1650390625, 1818.690673828125, 1821.3349609375, 1818.690673828125, 1816.04638671875, 1813.402099609375, 1810.7578125, 1813.402099609375, 1816.04638671875, 1813.402099609375, 1810.7578125, 1808.113525390625, 1804.587890625, 1799.29931640625, 1794.89208984375, 1792.247802734375, 1789.603515625, 1786.959228515625, 1783.43359375, 1780.789306640625, 1779.0263671875, 1780.789306640625, 1783.43359375, 1786.959228515625, 1789.603515625, 1801.943603515625, 1815.1650390625, 1828.386474609375, 1841.60791015625, 1845.133544921875, 1847.77783203125, 1852.18505859375, 1857.4736328125, 1862.76220703125, 1868.05078125, 1878.6279296875, 1889.205078125, 1894.49365234375, 1898.90087890625, 1901.545166015625, 1904.189453125, 1906.833740234375, 1909.47802734375, 1906.833740234375, 1904.189453125, 1906.833740234375, 1910.359375, 1913.003662109375, 1915.64794921875, 1920.9365234375, 1926.22509765625, 1928.869384765625, 1930.63232421875, 1933.276611328125, 1935.9208984375, 1938.565185546875, 1941.20947265625, 1938.565185546875, 1935.9208984375, 1933.276611328125, 1930.63232421875, 1928.869384765625], [876.510986328125, 873.868408203125, 875.630126953125, 878.272705078125, 883.557861328125, 888.843017578125, 896.770751953125, 904.6986083984375, 912.6263427734375, 920.5540771484375, 926.719970703125, 931.1243896484375, 925.8392333984375, 920.5540771484375, 912.6263427734375, 905.579345703125, 902.936767578125, 899.413330078125, 894.128173828125, 889.723876953125, 887.081298828125, 884.438720703125, 881.796142578125, 878.272705078125, 875.630126953125, 873.868408203125, 878.272705078125, 883.557861328125, 886.200439453125, 888.843017578125, 902.055908203125, 915.2689208984375, 928.4818115234375, 941.6947021484375, 955.788330078125, 969.0013427734375, 966.3587646484375, 963.7161865234375, 961.0736083984375, 958.430908203125, 955.788330078125, 953.145751953125, 950.503173828125, 947.860595703125, 945.218017578125, 942.575439453125, 939.932861328125, 937.290283203125, 939.932861328125, 942.575439453125, 946.9798583984375, 951.3841552734375, 954.0267333984375, 957.5501708984375, 960.1927490234375, 961.9544677734375, 964.5970458984375, 967.2396240234375, 969.8822021484375, 973.4056396484375, 978.6907958984375, 983.9759521484375, 987.4993896484375, 990.1419677734375, 997.1888427734375, 1004.23583984375, 1006.87841796875, 1009.52099609375, 1006.87841796875, 1004.23583984375, 1009.52099609375, 1015.6868896484375, 1019.2103271484375, 1021.8529052734375, 1031.5423583984375, 1041.23193359375, 1043.87451171875, 1047.39794921875, 1050.04052734375, 1051.80224609375, 1054.44482421875, 1057.96826171875, 1060.61083984375, 1063.25341796875, 1068.53857421875, 1073.82373046875, 1079.10888671875, 1084.39404296875, 1089.67919921875, 1094.96435546875, 1097.60693359375, 1100.24951171875, 1103.77294921875, 1106.41552734375, 1108.17724609375, 1110.81982421875, 1113.46240234375, 1116.10498046875, 1119.62841796875, 1122.27099609375, 1126.67529296875, 1131.0797119140625, 1133.7222900390625, 1137.24560546875, 1139.88818359375, 1141.6500244140625, 1144.2926025390625, 1146.9351806640625, 1149.5777587890625, 1153.10107421875, 1158.38623046875, 1162.7906494140625, 1165.4332275390625, 1168.0758056640625, 1165.4332275390625, 1162.7906494140625, 1160.1480712890625, 1157.5054931640625, 1155.74365234375, 1153.10107421875, 1139.88818359375, 1126.67529296875, 1113.46240234375, 1100.24951171875, 1081.75146484375, 1063.25341796875, 1059.72998046875, 1057.08740234375, 1059.72998046875, 1062.37255859375, 1065.01513671875, 1067.65771484375, 1063.25341796875, 1057.96826171875, 1039.47021484375, 1020.9720458984375, 970.7630615234375, 920.5540771484375, 907.3411865234375, 894.128173828125, 886.200439453125, 879.153564453125]], [[555.6032104492188, 546.7888793945312, 550.3146362304688, 555.6032104492188, 566.1803588867188, 576.7575073242188, 583.8088989257812, 589.0974731445312, 597.9118041992188, 608.4889526367188, 619.0661010742188, 629.6432495117188, 636.6946411132812, 641.983154296875, 647.271728515625, 652.560302734375, 656.0860595703125, 661.3746337890625, 673.714599609375, 684.291748046875, 679.003173828125, 671.9517822265625, 661.3746337890625, 652.560302734375, 647.271728515625, 641.983154296875, 636.6946411132812, 631.4060668945312, 626.1174926757812, 620.8289184570312, 615.5403442382812, 608.4889526367188, 603.2003784179688, 599.6746215820312, 645.5089111328125, 693.1060791015625, 767.1461181640625, 841.1861572265625, 878.2061767578125, 915.2261962890625, 925.8033447265625, 936.3804931640625, 946.9576416015625, 955.77197265625, 961.060546875, 966.34912109375, 952.2462158203125, 938.143310546875, 948.720458984375, 959.297607421875, 954.009033203125, 948.720458984375, 943.431884765625, 936.3804931640625, 920.5147705078125, 904.6490478515625, 872.9176025390625, 841.1861572265625, 767.1461181640625, 693.1060791015625, 656.0860595703125, 619.0661010742188, 622.5918579101562, 627.8804321289062, 633.1690063476562, 638.4575805664062, 643.74609375, 649.03466796875, 654.3232421875, 659.61181640625, 664.900390625, 670.18896484375, 675.4775390625, 682.5289306640625, 693.1060791015625, 703.6832275390625, 714.2603759765625, 723.07470703125, 728.36328125, 733.65185546875, 738.9404296875, 744.22900390625, 749.517578125, 754.80615234375, 760.0947265625, 765.38330078125, 770.671875, 777.7232666015625, 788.3004150390625, 798.8775634765625, 814.7432861328125, 830.6090087890625, 846.4747314453125, 862.3404541015625, 895.834716796875, 927.566162109375, 922.277587890625, 916.989013671875, 911.700439453125, 904.6490478515625, 894.0718994140625, 883.4947509765625, 872.9176025390625, 864.103271484375, 858.814697265625, 853.526123046875, 848.237548828125, 842.948974609375, 837.660400390625, 830.6090087890625, 825.3204345703125, 821.794677734375, 825.3204345703125, 828.84619140625, 834.134765625, 839.42333984375, 844.7119140625, 851.7633056640625, 862.3404541015625, 872.9176025390625, 883.4947509765625, 894.0718994140625, 904.6490478515625, 913.46337890625, 918.751953125, 924.04052734375, 929.3291015625, 934.61767578125, 939.90625, 946.9576416015625, 957.5347900390625, 966.34912109375, 971.6376953125, 976.92626953125, 973.400634765625, 969.874755859375, 964.586181640625, 957.5347900390625, 936.3804931640625, 915.2261962890625, 878.2061767578125, 841.1861572265625, 809.4547119140625, 777.7232666015625, 772.4346923828125, 767.1461181640625, 760.0947265625, 754.80615234375, 735.4146728515625, 714.2603759765625, 708.9718017578125, 703.6832275390625, 696.6318359375, 691.34326171875, 687.8175048828125, 682.5289306640625, 656.0860595703125, 629.6432495117188, 613.7775268554688, 597.9118041992188, 587.3346557617188, 578.5203247070312, 587.3346557617188, 597.9118041992188, 604.9631958007812, 610.2517700195312, 619.0661010742188, 627.8804321289062, 613.7775268554688, 597.9118041992188, 582.0460815429688, 566.1803588867188], [235.56304931640625, 244.38873291015625, 253.21441650390625, 258.50982666015625, 263.80523681640625, 269.10064697265625, 276.16119384765625, 281.45660400390625, 284.98687744140625, 290.2823486328125, 295.5777587890625, 300.8731689453125, 307.93365478515625, 323.81988525390625, 339.70611572265625, 355.59234619140625, 369.7135009765625, 375.0089111328125, 382.06939697265625, 387.36480712890625, 392.66021728515625, 399.72076416015625, 405.01617431640625, 408.54644775390625, 413.84185791015625, 424.43267822265625, 435.02349853515625, 450.90972900390625, 466.79595947265625, 487.97760009765625, 509.15924072265625, 516.2197875976562, 521.5151977539062, 535.6362915039062, 549.7574462890625, 544.4620361328125, 539.1666259765625, 544.4620361328125, 549.7574462890625, 544.4620361328125, 539.1666259765625, 533.8712158203125, 528.5758056640625, 519.7500610351562, 509.15924072265625, 493.27301025390625, 479.15191650390625, 482.68218994140625, 487.97760009765625, 493.27301025390625, 498.56842041015625, 509.15924072265625, 519.7500610351562, 526.8106079101562, 532.1060180664062, 537.4014282226562, 542.6968383789062, 537.4014282226562, 532.1060180664062, 537.4014282226562, 542.6968383789062, 532.1060180664062, 519.7500610351562, 503.86383056640625, 487.97760009765625, 466.79595947265625, 445.61431884765625, 435.02349853515625, 424.43267822265625, 419.13726806640625, 413.84185791015625, 408.54644775390625, 403.25103759765625, 396.1905517578125, 390.8951416015625, 385.5997314453125, 380.3043212890625, 371.47857666015625, 360.88775634765625, 355.59234619140625, 350.29693603515625, 345.00152587890625, 339.70611572265625, 334.41070556640625, 329.11529541015625, 323.81988525390625, 318.52447509765625, 311.4639892578125, 306.1685791015625, 300.8731689453125, 295.5777587890625, 290.2823486328125, 284.98687744140625, 279.69146728515625, 276.16119384765625, 281.45660400390625, 286.75201416015625, 292.04742431640625, 297.34283447265625, 304.40338134765625, 309.69879150390625, 314.99420166015625, 320.28961181640625, 323.81988525390625, 329.11529541015625, 334.41070556640625, 339.70611572265625, 345.00152587890625, 350.29693603515625, 357.35748291015625, 362.65289306640625, 366.18316650390625, 369.7135009765625, 366.18316650390625, 360.88775634765625, 355.59234619140625, 350.29693603515625, 343.2364501953125, 337.9410400390625, 332.6456298828125, 327.3502197265625, 322.0548095703125, 316.7593994140625, 313.22906494140625, 307.93365478515625, 302.63824462890625, 297.34283447265625, 292.04742431640625, 286.75201416015625, 279.69146728515625, 274.39605712890625, 260.27496337890625, 244.38873291015625, 239.09332275390625, 235.56304931640625, 239.09332275390625, 244.38873291015625, 251.44927978515625, 256.74468994140625, 262.04010009765625, 267.33551025390625, 272.63092041015625, 277.92633056640625, 272.63092041015625, 267.33551025390625, 262.04010009765625, 254.97955322265625, 249.68414306640625, 246.15386962890625, 235.56304931640625, 224.97222900390625, 219.67681884765625, 212.61627197265625, 202.02545166015625, 193.19976806640625, 187.90435791015625, 182.60894775390625, 187.90435791015625, 193.19976806640625, 198.49517822265625, 203.79058837890625, 212.61627197265625, 221.44195556640625, 226.73736572265625, 233.79791259765625, 239.09332275390625, 242.62359619140625, 249.68414306640625, 256.74468994140625, 251.44927978515625, 246.15386962890625, 240.85845947265625]], [[476.27459716796875, 460.40887451171875, 449.83172607421875, 441.0174560546875, 435.7288818359375, 430.4403076171875, 425.1517333984375, 419.8631591796875, 414.5745849609375, 407.52313232421875, 402.23455810546875, 398.7088623046875, 393.4202880859375, 388.1317138671875, 382.8431396484375, 377.5545654296875, 372.2659912109375, 366.9774169921875, 361.6888427734375, 356.4002685546875, 351.1116943359375, 345.8231201171875, 340.5345458984375, 333.483154296875, 328.194580078125, 322.906005859375, 317.617431640625, 312.328857421875, 305.2774658203125, 299.9888916015625, 291.174560546875, 280.597412109375, 264.731689453125, 250.6287841796875, 245.3402099609375, 238.288818359375, 227.711669921875, 218.8973388671875, 213.6087646484375, 208.3201904296875, 203.0316162109375, 197.7430419921875, 201.268798828125, 206.557373046875, 222.423095703125, 238.288818359375, 243.577392578125, 248.865966796875, 255.9173583984375, 261.2059326171875, 264.731689453125, 270.020263671875, 240.0516357421875, 208.3201904296875, 203.0316162109375, 197.7430419921875, 203.0316162109375, 208.3201904296875, 211.845947265625, 217.134521484375, 224.1859130859375, 229.4744873046875, 233.000244140625, 238.288818359375, 245.3402099609375, 250.6287841796875, 264.731689453125, 278.83453369140625, 284.12310791015625, 289.41168212890625, 294.7003173828125, 299.9888916015625, 310.5660400390625, 321.14312744140625, 326.43170166015625, 333.483154296875, 349.348876953125, 363.45172119140625, 368.74029541015625, 375.791748046875, 386.368896484375, 396.94598388671875, 407.52313232421875, 418.10028076171875, 428.67742919921875, 439.25457763671875, 435.7288818359375, 430.4403076171875, 425.1517333984375, 419.8631591796875, 425.1517333984375, 430.4403076171875, 439.25457763671875, 448.06890869140625, 442.78033447265625, 437.49176025390625, 442.78033447265625, 448.06890869140625, 453.35748291015625, 460.40887451171875, 465.69744873046875, 470.98602294921875, 478.03741455078125, 483.32598876953125, 488.61456298828125, 493.90313720703125, 478.03741455078125, 462.1717529296875, 465.69744873046875, 470.98602294921875, 476.27459716796875, 481.56317138671875, 486.85174560546875, 492.14031982421875, 499.19171142578125, 504.48028564453125, 536.2117309570312, 567.9431762695312, 562.6546020507812, 557.3660278320312, 552.0774536132812, 545.0260620117188, 539.7374877929688, 536.2117309570312, 539.7374877929688, 545.0260620117188, 552.0774536132812, 557.3660278320312, 576.7575073242188, 597.9118041992188, 608.4889526367188, 617.3032836914062, 622.5918579101562, 627.8804321289062, 633.1690063476562, 638.4575805664062, 643.74609375, 650.7974853515625, 656.0860595703125, 659.61181640625, 664.900390625, 670.18896484375, 675.4775390625, 680.76611328125, 686.0546875, 691.34326171875, 687.8175048828125, 682.5289306640625, 677.2403564453125, 671.9517822265625, 664.900390625, 659.61181640625, 650.7974853515625, 640.2203369140625, 629.6432495117188, 619.0661010742188, 612.0147094726562, 606.7261352539062, 601.4375610351562, 596.1489868164062, 601.4375610351562, 606.7261352539062, 612.0147094726562, 619.0661010742188, 624.3546752929688, 627.8804321289062, 633.1690063476562, 638.4575805664062, 643.74609375, 650.7974853515625, 656.0860595703125, 659.61181640625, 654.3232421875, 649.03466796875, 680.76611328125, 714.2603759765625, 730.1260986328125, 745.9918212890625, 761.8575439453125, 777.7232666015625, 793.5889892578125, 809.4547119140625, 825.3204345703125, 841.1861572265625, 857.0518798828125, 872.9176025390625, 879.968994140625, 885.257568359375, 894.0718994140625, 904.6490478515625, 911.700439453125, 916.989013671875, 911.700439453125, 906.411865234375, 901.123291015625, 895.834716796875, 899.3604736328125, 904.6490478515625, 915.2261962890625, 924.04052734375, 920.5147705078125, 915.2261962890625, 909.9376220703125, 904.6490478515625, 899.3604736328125, 894.0718994140625, 887.0205078125, 881.73193359375, 872.9176025390625, 862.3404541015625, 846.4747314453125, 830.6090087890625, 814.7432861328125, 798.8775634765625, 783.0118408203125, 767.1461181640625, 751.2803955078125, 735.4146728515625, 714.2603759765625, 693.1060791015625, 671.9517822265625, 650.7974853515625, 624.3546752929688, 599.6746215820312, 603.2003784179688, 608.4889526367188, 604.9631958007812, 599.6746215820312, 594.3860473632812, 587.3346557617188, 582.0460815429688, 578.5203247070312, 573.2317504882812, 567.9431762695312, 562.6546020507812, 555.6032104492188, 545.0260620117188, 534.4489135742188, 527.3975219726562, 522.1089477539062, 513.2946166992188, 502.71746826171875, 486.85174560546875, 470.98602294921875, 455.12030029296875, 439.25457763671875, 442.78033447265625, 448.06890869140625, 453.35748291015625, 458.64605712890625, 463.93463134765625, 469.22320556640625, 474.51177978515625, 479.80035400390625, 485.08892822265625, 490.37750244140625, 495.66607666015625, 500.95465087890625, 506.24322509765625, 513.2946166992188, 518.5831909179688, 522.1089477539062, 518.5831909179688, 513.2946166992188, 508.00604248046875, 502.71746826171875, 495.66607666015625, 490.37750244140625], [1029.8746337890625, 1035.1700439453125, 1040.4654541015625, 1043.995849609375, 1049.291259765625, 1054.586669921875, 1059.882080078125, 1065.177490234375, 1070.472900390625, 1077.5333251953125, 1082.8287353515625, 1091.654541015625, 1102.245361328125, 1107.540771484375, 1112.836181640625, 1118.131591796875, 1123.427001953125, 1128.722412109375, 1134.017822265625, 1139.313232421875, 1144.608642578125, 1149.904052734375, 1155.199462890625, 1162.2598876953125, 1167.5552978515625, 1162.2598876953125, 1156.9644775390625, 1151.6690673828125, 1144.608642578125, 1134.017822265625, 1125.1920166015625, 1119.8966064453125, 1114.6011962890625, 1118.131591796875, 1123.427001953125, 1130.4874267578125, 1135.7828369140625, 1144.608642578125, 1155.199462890625, 1165.790283203125, 1176.381103515625, 1202.858154296875, 1227.5699462890625, 1232.8653564453125, 1238.1607666015625, 1243.4561767578125, 1248.7515869140625, 1254.0469970703125, 1261.107666015625, 1271.698486328125, 1280.5240478515625, 1285.8194580078125, 1292.880126953125, 1298.175537109375, 1303.470947265625, 1319.357177734375, 1335.243408203125, 1345.834228515625, 1354.6597900390625, 1359.9552001953125, 1367.015869140625, 1377.606689453125, 1386.4322509765625, 1391.7276611328125, 1398.788330078125, 1404.083740234375, 1407.6138916015625, 1398.788330078125, 1388.197509765625, 1372.311279296875, 1356.425048828125, 1340.538818359375, 1324.652587890625, 1319.357177734375, 1314.061767578125, 1307.0010986328125, 1301.7056884765625, 1298.175537109375, 1292.880126953125, 1296.4102783203125, 1301.7056884765625, 1307.0010986328125, 1312.2965087890625, 1317.5919189453125, 1322.8873291015625, 1338.7735595703125, 1356.425048828125, 1377.606689453125, 1398.788330078125, 1462.333251953125, 1525.878173828125, 1536.468994140625, 1545.2945556640625, 1541.764404296875, 1536.468994140625, 1488.810302734375, 1441.151611328125, 1430.560791015625, 1419.969970703125, 1412.9093017578125, 1407.6138916015625, 1423.5001220703125, 1441.151611328125, 1462.333251953125, 1483.514892578125, 1499.401123046875, 1515.287353515625, 1520.582763671875, 1524.1129150390625, 1529.4083251953125, 1534.7037353515625, 1539.9991455078125, 1545.2945556640625, 1550.5899658203125, 1557.650634765625, 1562.946044921875, 1568.241455078125, 1589.423095703125, 1610.604736328125, 1615.900146484375, 1621.195556640625, 1628.2559814453125, 1633.5513916015625, 1652.968017578125, 1672.384521484375, 1677.679931640625, 1684.740478515625, 1690.035888671875, 1693.566162109375, 1688.270751953125, 1682.975341796875, 1679.445068359375, 1674.149658203125, 1668.854248046875, 1663.558837890625, 1658.263427734375, 1652.968017578125, 1645.9073486328125, 1640.6119384765625, 1631.786376953125, 1621.195556640625, 1610.604736328125, 1600.013916015625, 1589.423095703125, 1578.832275390625, 1568.241455078125, 1559.4156494140625, 1554.1202392578125, 1548.8248291015625, 1543.5294189453125, 1536.468994140625, 1531.173583984375, 1527.6431884765625, 1522.3477783203125, 1517.0523681640625, 1511.7569580078125, 1504.696533203125, 1472.924072265625, 1441.151611328125, 1430.560791015625, 1419.969970703125, 1414.674560546875, 1409.379150390625, 1402.3184814453125, 1397.0230712890625, 1388.197509765625, 1377.606689453125, 1372.311279296875, 1367.015869140625, 1359.9552001953125, 1354.6597900390625, 1298.175537109375, 1239.926025390625, 1224.039794921875, 1208.153564453125, 1211.6837158203125, 1216.9791259765625, 1222.2745361328125, 1227.5699462890625, 1232.8653564453125, 1238.1607666015625, 1243.4561767578125, 1248.7515869140625, 1254.0469970703125, 1259.3424072265625, 1264.6378173828125, 1271.698486328125, 1276.993896484375, 1280.5240478515625, 1291.1148681640625, 1303.470947265625, 1324.652587890625, 1345.834228515625, 1351.129638671875, 1356.425048828125, 1361.720458984375, 1365.2506103515625, 1359.9552001953125, 1354.6597900390625, 1324.652587890625, 1294.6451416015625, 1289.3497314453125, 1284.0543212890625, 1278.7589111328125, 1273.4635009765625, 1268.1680908203125, 1261.107666015625, 1255.812255859375, 1252.2818603515625, 1246.9864501953125, 1241.6910400390625, 1236.3956298828125, 1231.1002197265625, 1225.8048095703125, 1220.5093994140625, 1215.2139892578125, 1209.9185791015625, 1204.6231689453125, 1199.3277587890625, 1194.0323486328125, 1188.7369384765625, 1183.4415283203125, 1178.1461181640625, 1192.267333984375, 1206.3883056640625, 1211.6837158203125, 1218.744384765625, 1229.335205078125, 1239.926025390625, 1246.9864501953125, 1252.2818603515625, 1261.107666015625, 1271.698486328125, 1276.993896484375, 1282.289306640625, 1289.3497314453125, 1294.6451416015625, 1289.3497314453125, 1282.289306640625, 1276.993896484375, 1273.4635009765625, 1268.1680908203125, 1262.8726806640625, 1257.5772705078125, 1252.2818603515625, 1246.9864501953125, 1239.926025390625, 1229.335205078125, 1218.744384765625, 1202.858154296875, 1186.971923828125, 1171.085693359375, 1155.199462890625, 1144.608642578125, 1134.017822265625, 1112.836181640625, 1091.654541015625, 1086.359130859375, 1081.063720703125, 1074.0030517578125, 1068.7076416015625, 1065.177490234375, 1061.6470947265625, 1056.3516845703125, 1051.0562744140625, 1045.7608642578125, 1038.700439453125, 1033.405029296875]], [[1019.23486328125, 1015.7091064453125, 1012.183349609375, 1017.471923828125, 1022.760498046875, 1028.049072265625, 1033.337646484375, 1028.049072265625, 1020.9976806640625, 1010.420654296875, 999.843505859375, 989.266357421875, 980.451904296875, 994.554931640625, 1010.420654296875, 1026.2862548828125, 1040.38916015625, 1045.677734375, 1052.7291259765625, 1073.8834228515625, 1095.0377197265625, 1102.089111328125, 1107.377685546875, 1116.1920166015625, 1126.7691650390625, 1132.0577392578125, 1137.3463134765625, 1142.6348876953125, 1147.9234619140625, 1153.2120361328125, 1158.5006103515625, 1163.7891845703125, 1169.0777587890625, 1174.3663330078125, 1179.6549072265625, 1184.9434814453125, 1190.2320556640625, 1195.5206298828125, 1200.8092041015625, 1207.860595703125, 1213.149169921875, 1207.860595703125, 1202.572021484375, 1197.283447265625, 1191.994873046875, 1186.706298828125, 1181.417724609375, 1176.129150390625, 1170.840576171875, 1160.263427734375, 1147.9234619140625, 1116.1920166015625, 1084.4605712890625, 1077.4091796875, 1072.12060546875, 1063.3062744140625, 1052.7291259765625, 1042.1519775390625, 1033.337646484375, 1028.049072265625, 1020.9976806640625, 999.843505859375, 978.689208984375, 962.8233642578125, 946.9576416015625, 936.3804931640625, 927.566162109375, 922.277587890625, 916.989013671875, 911.700439453125, 904.6490478515625, 899.3604736328125, 895.834716796875, 901.123291015625, 906.411865234375, 909.9376220703125, 915.2261962890625, 931.0919189453125, 946.9576416015625, 962.8233642578125, 978.689208984375, 983.977783203125, 989.266357421875, 999.843505859375, 1010.420654296875, 1020.9976806640625, 1031.5748291015625, 1028.049072265625, 1022.760498046875, 1017.471923828125, 1010.420654296875, 999.843505859375, 989.266357421875, 968.1119384765625, 946.9576416015625, 915.2261962890625, 883.4947509765625, 851.7633056640625, 820.0318603515625, 804.1661376953125, 788.3004150390625, 772.4346923828125, 756.5689697265625, 735.4146728515625, 716.023193359375, 724.8375244140625, 735.4146728515625, 751.2803955078125, 767.1461181640625, 788.3004150390625, 809.4547119140625, 830.6090087890625, 851.7633056640625, 878.2061767578125, 904.6490478515625, 925.8033447265625, 946.9576416015625, 973.400634765625, 999.843505859375, 1015.7091064453125, 1029.81201171875, 1035.1005859375, 1040.38916015625, 1035.1005859375, 1029.81201171875, 1035.1005859375, 1042.1519775390625, 1047.4405517578125, 1050.96630859375, 1056.2548828125, 1061.54345703125, 1056.2548828125, 1050.96630859375, 1045.677734375, 1040.38916015625, 1045.677734375, 1052.7291259765625, 1063.3062744140625, 1073.8834228515625, 1095.0377197265625, 1116.1920166015625, 1123.243408203125, 1128.531982421875, 1132.0577392578125, 1137.3463134765625, 1158.5006103515625, 1177.8919677734375, 1183.1805419921875, 1190.2320556640625, 1202.572021484375, 1213.149169921875, 1216.6749267578125, 1221.9635009765625, 1227.2520751953125, 1232.5406494140625, 1243.1177978515625, 1253.6949462890625, 1264.2720947265625, 1274.8492431640625, 1285.4263916015625, 1296.0035400390625, 1306.5806884765625, 1317.1578369140625, 1327.7349853515625, 1336.5491943359375, 1331.2606201171875, 1325.9720458984375, 1322.4464111328125, 1317.1578369140625, 1310.1063232421875, 1304.8177490234375, 1299.5291748046875, 1294.2406005859375, 1288.9520263671875, 1283.6634521484375, 1280.1378173828125, 1274.8492431640625, 1267.7977294921875, 1262.5091552734375, 1257.2205810546875, 1251.9320068359375, 1257.2205810546875, 1262.5091552734375, 1253.6949462890625, 1243.1177978515625, 1232.5406494140625, 1221.9635009765625, 1214.9119873046875, 1209.6234130859375, 1206.0977783203125, 1200.8092041015625, 1195.5206298828125, 1190.2320556640625, 1184.9434814453125, 1179.6549072265625, 1174.3663330078125, 1169.0777587890625, 1163.7891845703125, 1158.5006103515625, 1153.2120361328125, 1147.9234619140625, 1142.6348876953125, 1137.3463134765625, 1130.294921875, 1125.00634765625, 1116.1920166015625, 1105.6148681640625, 1095.0377197265625, 1084.4605712890625, 1068.5948486328125, 1052.7291259765625, 1047.4405517578125, 1042.1519775390625, 1036.8634033203125, 1031.5748291015625, 1024.5234375], [1128.722412109375, 1125.1920166015625, 1134.017822265625, 1144.608642578125, 1155.199462890625, 1165.790283203125, 1176.381103515625, 1186.971923828125, 1194.0323486328125, 1199.3277587890625, 1204.6231689453125, 1209.9185791015625, 1213.448974609375, 1216.9791259765625, 1211.6837158203125, 1206.3883056640625, 1202.858154296875, 1197.562744140625, 1190.5020751953125, 1185.2066650390625, 1190.5020751953125, 1197.562744140625, 1202.858154296875, 1206.3883056640625, 1211.6837158203125, 1216.9791259765625, 1222.2745361328125, 1227.5699462890625, 1232.8653564453125, 1238.1607666015625, 1243.4561767578125, 1248.7515869140625, 1254.0469970703125, 1259.3424072265625, 1264.6378173828125, 1269.9332275390625, 1275.2286376953125, 1280.5240478515625, 1285.8194580078125, 1292.880126953125, 1308.766357421875, 1324.652587890625, 1345.834228515625, 1367.015869140625, 1377.606689453125, 1388.197509765625, 1398.788330078125, 1409.379150390625, 1419.969970703125, 1430.560791015625, 1437.6212158203125, 1442.9166259765625, 1437.6212158203125, 1430.560791015625, 1425.265380859375, 1421.7349853515625, 1427.0303955078125, 1432.3258056640625, 1435.856201171875, 1441.151611328125, 1448.2120361328125, 1453.5074462890625, 1458.8028564453125, 1464.0982666015625, 1469.3936767578125, 1474.6890869140625, 1478.219482421875, 1483.514892578125, 1504.696533203125, 1525.878173828125, 1532.9385986328125, 1538.2340087890625, 1541.764404296875, 1547.059814453125, 1578.832275390625, 1608.8394775390625, 1614.1348876953125, 1619.4302978515625, 1624.7257080078125, 1630.0211181640625, 1624.7257080078125, 1619.4302978515625, 1624.7257080078125, 1630.0211181640625, 1624.7257080078125, 1619.4302978515625, 1624.7257080078125, 1631.786376953125, 1642.377197265625, 1652.968017578125, 1675.914794921875, 1697.096435546875, 1702.391845703125, 1707.687255859375, 1712.982666015625, 1718.278076171875, 1723.573486328125, 1728.868896484375, 1734.164306640625, 1739.459716796875, 1744.755126953125, 1750.050537109375, 1755.345947265625, 1760.641357421875, 1769.467041015625, 1778.292724609375, 1772.997314453125, 1767.701904296875, 1762.406494140625, 1757.111083984375, 1751.815673828125, 1746.520263671875, 1741.224853515625, 1735.929443359375, 1730.634033203125, 1725.338623046875, 1720.043212890625, 1714.747802734375, 1709.452392578125, 1704.156982421875, 1700.626708984375, 1695.331298828125, 1690.035888671875, 1684.740478515625, 1668.854248046875, 1652.968017578125, 1645.9073486328125, 1640.6119384765625, 1631.786376953125, 1621.195556640625, 1600.013916015625, 1578.832275390625, 1562.946044921875, 1547.059814453125, 1531.173583984375, 1515.287353515625, 1508.2266845703125, 1502.9312744140625, 1508.2266845703125, 1513.5220947265625, 1518.8175048828125, 1525.878173828125, 1531.173583984375, 1534.7037353515625, 1529.4083251953125, 1524.1129150390625, 1520.582763671875, 1515.287353515625, 1502.9312744140625, 1494.105712890625, 1509.991943359375, 1524.1129150390625, 1529.4083251953125, 1534.7037353515625, 1529.4083251953125, 1524.1129150390625, 1518.8175048828125, 1513.5220947265625, 1508.2266845703125, 1502.9312744140625, 1497.6358642578125, 1492.3404541015625, 1497.6358642578125, 1502.9312744140625, 1488.810302734375, 1472.924072265625, 1462.333251953125, 1453.5074462890625, 1448.2120361328125, 1441.151611328125, 1430.560791015625, 1419.969970703125, 1409.379150390625, 1398.788330078125, 1388.197509765625, 1379.3717041015625, 1374.0762939453125, 1367.015869140625, 1356.425048828125, 1345.834228515625, 1335.243408203125, 1324.652587890625, 1319.357177734375, 1315.8267822265625, 1310.5313720703125, 1305.2359619140625, 1299.9405517578125, 1292.880126953125, 1282.289306640625, 1273.4635009765625, 1268.1680908203125, 1262.8726806640625, 1257.5772705078125, 1252.2818603515625, 1246.9864501953125, 1241.6910400390625, 1236.3956298828125, 1231.1002197265625, 1225.8048095703125, 1220.5093994140625, 1215.2139892578125, 1209.9185791015625, 1204.6231689453125, 1197.562744140625, 1192.267333984375, 1188.7369384765625, 1183.4415283203125, 1178.1461181640625, 1172.8507080078125, 1167.5552978515625, 1162.2598876953125, 1156.9644775390625, 1151.6690673828125, 1146.3736572265625, 1141.0782470703125, 1134.017822265625]], [[1579.8236083984375, 1576.2979736328125, 1572.7720947265625, 1567.4835205078125, 1562.1949462890625, 1556.9063720703125, 1551.6177978515625, 1546.3292236328125, 1541.0406494140625, 1535.7520751953125, 1528.7008056640625, 1518.1236572265625, 1507.5465087890625, 1486.3922119140625, 1465.2379150390625, 1449.3721923828125, 1433.5064697265625, 1422.9293212890625, 1412.3521728515625, 1401.7750244140625, 1391.1978759765625, 1384.1463623046875, 1378.8577880859375, 1375.3321533203125, 1370.0435791015625, 1364.7550048828125, 1359.4664306640625, 1354.1778564453125, 1348.8892822265625, 1343.6007080078125, 1338.3121337890625, 1333.0235595703125, 1327.7349853515625, 1322.4464111328125, 1318.920654296875, 1313.632080078125, 1306.5806884765625, 1301.2921142578125, 1297.766357421875, 1292.477783203125, 1287.189208984375, 1290.7149658203125, 1296.0035400390625, 1301.2921142578125, 1306.5806884765625, 1313.632080078125, 1318.920654296875, 1327.7349853515625, 1338.3121337890625, 1343.6007080078125, 1348.8892822265625, 1355.9405517578125, 1361.2291259765625, 1366.5177001953125, 1371.8062744140625, 1366.5177001953125, 1359.4664306640625, 1348.8892822265625, 1338.3121337890625, 1327.7349853515625, 1317.1578369140625, 1301.2921142578125, 1285.4263916015625, 1269.5606689453125, 1253.6949462890625, 1237.8292236328125, 1221.9635009765625, 1216.6749267578125, 1213.149169921875, 1227.2520751953125, 1243.1177978515625, 1264.2720947265625, 1285.4263916015625, 1290.7149658203125, 1296.0035400390625, 1303.054931640625, 1308.343505859375, 1322.4464111328125, 1338.3121337890625, 1348.8892822265625, 1359.4664306640625, 1370.0435791015625, 1380.6207275390625, 1391.1978759765625, 1401.7750244140625, 1412.3521728515625, 1422.9293212890625, 1433.5064697265625, 1442.3206787109375, 1447.6092529296875, 1454.6607666015625, 1459.9493408203125, 1463.4749755859375, 1458.1864013671875, 1452.8978271484375, 1458.1864013671875, 1465.2379150390625, 1507.5465087890625, 1548.0921630859375, 1553.3807373046875, 1558.6693115234375, 1553.3807373046875, 1548.0921630859375, 1553.3807373046875, 1558.6693115234375, 1563.9578857421875, 1569.2464599609375, 1574.5350341796875], [1557.650634765625, 1506.4615478515625, 1536.468994140625, 1568.241455078125, 1594.718505859375, 1621.195556640625, 1637.081787109375, 1652.968017578125, 1668.854248046875, 1684.740478515625, 1697.096435546875, 1707.687255859375, 1712.982666015625, 1718.278076171875, 1723.573486328125, 1728.868896484375, 1734.164306640625, 1739.459716796875, 1744.755126953125, 1750.050537109375, 1723.573486328125, 1695.331298828125, 1668.854248046875, 1644.1422119140625, 1638.8468017578125, 1633.5513916015625, 1628.2559814453125, 1622.9605712890625, 1617.6651611328125, 1612.3697509765625, 1607.0743408203125, 1601.7789306640625, 1596.4835205078125, 1591.1881103515625, 1594.718505859375, 1600.013916015625, 1607.0743408203125, 1612.3697509765625, 1621.195556640625, 1631.786376953125, 1637.081787109375, 1640.6119384765625, 1645.9073486328125, 1651.2027587890625, 1656.4981689453125, 1663.558837890625, 1668.854248046875, 1672.384521484375, 1677.679931640625, 1682.975341796875, 1688.270751953125, 1695.331298828125, 1711.217529296875, 1727.103759765625, 1737.694580078125, 1748.285400390625, 1755.345947265625, 1760.641357421875, 1765.936767578125, 1771.232177734375, 1776.527587890625, 1781.822998046875, 1787.118408203125, 1792.413818359375, 1797.709228515625, 1803.004638671875, 1797.709228515625, 1792.413818359375, 1806.534912109375, 1820.656005859375, 1815.360595703125, 1810.065185546875, 1820.656005859375, 1831.246826171875, 1836.542236328125, 1843.602783203125, 1854.193603515625, 1863.019287109375, 1857.723876953125, 1852.428466796875, 1847.133056640625, 1841.837646484375, 1836.542236328125, 1831.246826171875, 1825.951416015625, 1820.656005859375, 1815.360595703125, 1810.065185546875, 1806.534912109375, 1801.239501953125, 1794.178955078125, 1788.883544921875, 1780.057861328125, 1769.467041015625, 1758.876220703125, 1748.285400390625, 1741.224853515625, 1735.929443359375, 1732.399169921875, 1727.103759765625, 1721.808349609375, 1716.512939453125, 1695.331298828125, 1674.149658203125, 1658.263427734375, 1642.377197265625, 1626.490966796875, 1610.604736328125]]], "confs": [0.88720703125, 0.9140625, 0.88916015625, 0.9365234375, 0.931640625, 0.9072265625, 0.88671875, 0.8076171875, 0.6875, 0.56396484375], "classes": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], "scales": [1.0, 1.0, 1.0, 0.7565172262365764, 0.7565172262365764, 0.7565172262365764, 0.3778883798713426, 0.3778883798713426, 0.3778883798713426, 0.3778883798713426], "identifier": "ChangeThisTEMPORARY", "image_path": "dev/reference/ALUS_Non-miteArachnids_Unknown_2020_11_03_4545.jpg", "image_width": 1940, "image_height": 1933, "mask_width": 1940, "mask_height": 1933} \ No newline at end of file diff --git a/src/flat_bug/tests/assets/metadata_ALUS_Non-miteArachnids_Unknown_2020_11_03_4545_UUID_XXXX.json b/src/flat_bug/tests/assets/metadata_ALUS_Non-miteArachnids_Unknown_2020_11_03_4545_UUID_XXXX.json deleted file mode 100644 index 6ab8846..0000000 --- a/src/flat_bug/tests/assets/metadata_ALUS_Non-miteArachnids_Unknown_2020_11_03_4545_UUID_XXXX.json +++ /dev/null @@ -1,2 +0,0 @@ -{"boxes": [[181, 355, 502, 687], [531, 173, 977, 559], [588, 605, 956, 939], [1057, 21, 1201, 183], [1092, 702, 1362, 1021], [1734, 867, 1939, 1171], [1565, 1340, 1661, 1413], [1583, 515, 1668, 581], [185, 1024, 797, 1599], [738, 1211, 1352, 1771], [1100, 1489, 1595, 1867]], - "contours": [[[96, 94, 93, 90, 89, 86, 86, 85, 85, 86, 86, 87, 87, 86, 83, 81, 81, 80, 77, 76, 76, 73, 73, 71, 69, 68, 67, 64, 64, 63, 63, 64, 64, 63, 62, 62, 61, 61, 62, 62, 61, 59, 59, 61, 62, 65, 65, 63, 62, 62, 61, 61, 63, 63, 61, 61, 58, 58, 57, 57, 56, 56, 55, 55, 54, 54, 52, 52, 50, 50, 49, 49, 48, 48, 47, 47, 50, 51, 52, 53, 53, 51, 50, 48, 48, 47, 48, 48, 49, 49, 51, 51, 53, 53, 55, 59, 60, 61, 61, 60, 59, 59, 60, 60, 61, 61, 62, 62, 64, 65, 66, 68, 69, 71, 72, 73, 74, 75, 76, 77, 78, 78, 82, 84, 85, 88, 89, 89, 91, 91, 93, 95, 96, 97, 98, 98, 99, 99, 100, 100, 101, 101, 103, 106, 107, 107, 109, 109, 110, 110, 111, 111, 112, 112, 114, 114, 113, 110, 109, 109, 108, 108, 107, 107, 106, 106, 108, 108, 116, 116, 120, 120, 123, 123, 122, 120, 119, 117, 116, 114, 113, 111, 110, 109, 108, 106, 105, 103, 103, 102, 102, 101, 101], [90, 92, 92, 95, 95, 98, 99, 100, 110, 111, 112, 113, 114, 115, 115, 113, 111, 110, 110, 111, 112, 115, 116, 118, 118, 117, 117, 114, 111, 110, 104, 103, 100, 99, 100, 104, 105, 110, 111, 113, 114, 114, 116, 118, 118, 121, 123, 125, 125, 126, 127, 129, 131, 132, 134, 135, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 149, 150, 152, 153, 154, 155, 156, 157, 158, 167, 170, 170, 171, 171, 169, 167, 167, 165, 163, 162, 161, 159, 158, 157, 155, 154, 152, 151, 149, 149, 148, 148, 146, 146, 145, 144, 143, 141, 140, 139, 138, 137, 135, 135, 134, 134, 133, 133, 132, 132, 131, 131, 130, 130, 131, 134, 138, 138, 139, 139, 140, 143, 141, 140, 138, 138, 139, 139, 140, 141, 142, 143, 144, 145, 146, 147, 149, 149, 148, 147, 145, 144, 143, 142, 141, 140, 139, 138, 136, 132, 131, 131, 130, 129, 128, 127, 126, 124, 123, 120, 118, 117, 109, 108, 104, 103, 100, 96, 96, 98, 98, 100, 100, 102, 102, 104, 104, 105, 105, 107, 107, 105, 97, 96, 93, 92, 90]], [[163, 162, 161, 160, 158, 157, 155, 154, 152, 151, 149, 148, 147, 145, 145, 146, 149, 150, 151, 152, 153, 154, 155, 156, 159, 158, 157, 156, 153, 152, 150, 149, 146, 145, 142, 141, 140, 139, 137, 136, 134, 134, 135, 136, 138, 139, 140, 141, 144, 145, 147, 148, 149, 150, 152, 153, 155, 156, 157, 158, 160, 161, 163, 163, 164, 164, 166, 170, 171, 170, 169, 160, 160, 159, 159, 158, 158, 157, 157, 156, 156, 155, 155, 154, 154, 153, 153, 152, 152, 151, 151, 150, 150, 166, 167, 177, 178, 190, 191, 198, 199, 207, 208, 219, 220, 224, 225, 230, 231, 232, 233, 234, 235, 236, 238, 238, 239, 239, 240, 240, 237, 237, 238, 238, 237, 237, 235, 234, 232, 230, 229, 208, 207, 196, 195, 193, 192, 178, 177, 166, 165, 155, 154, 154, 155, 155, 156, 156, 157, 157, 158, 158, 159, 159, 160, 160, 161, 161, 162, 162, 171, 172, 176, 180, 181, 181, 183, 183, 184, 184, 186, 188, 190, 190, 192, 192, 193, 193, 194, 195, 196, 199, 200, 203, 204, 207, 208, 210, 211, 213, 214, 217, 218, 220, 221, 221, 222, 223, 223, 222, 221, 219, 218, 217, 216, 213, 212, 208, 208, 205, 205, 204, 204, 200, 197, 196, 195, 194, 194, 196, 198, 199, 200, 201, 202, 204, 204, 205, 205, 207, 207, 213, 214, 217, 218, 219, 220, 221, 222, 223, 224, 226, 227, 228, 229, 231, 232, 237, 238, 241, 241, 240, 234, 233, 229, 228, 222, 221, 220, 216, 215, 213, 212, 194, 193, 192, 190, 190, 186, 184, 183, 181, 177, 177, 175, 175, 172, 172, 167], [44, 45, 45, 46, 46, 47, 47, 48, 48, 49, 49, 50, 50, 52, 55, 55, 58, 58, 59, 59, 60, 60, 61, 61, 64, 65, 65, 64, 64, 63, 63, 62, 62, 61, 61, 60, 60, 59, 59, 58, 58, 61, 62, 62, 64, 64, 65, 65, 68, 68, 70, 70, 71, 71, 73, 73, 75, 75, 76, 76, 78, 78, 80, 89, 90, 93, 95, 95, 96, 97, 97, 106, 108, 109, 110, 111, 113, 114, 120, 121, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 138, 138, 137, 137, 136, 136, 135, 135, 136, 136, 137, 137, 138, 138, 137, 137, 136, 136, 135, 135, 134, 134, 132, 131, 130, 129, 128, 120, 120, 123, 124, 127, 128, 129, 131, 131, 133, 133, 134, 134, 133, 133, 132, 132, 133, 133, 134, 134, 135, 135, 134, 133, 132, 131, 130, 129, 128, 126, 125, 121, 120, 116, 115, 114, 113, 111, 110, 109, 100, 100, 96, 96, 95, 93, 91, 90, 89, 88, 86, 86, 84, 83, 81, 80, 79, 78, 77, 77, 76, 76, 75, 75, 74, 74, 73, 73, 72, 72, 71, 71, 70, 70, 71, 72, 72, 73, 77, 78, 78, 80, 80, 81, 81, 84, 84, 88, 89, 92, 93, 94, 95, 99, 99, 98, 98, 97, 99, 101, 101, 102, 102, 101, 101, 99, 98, 97, 96, 94, 93, 87, 87, 84, 84, 83, 83, 82, 82, 81, 81, 79, 79, 78, 78, 76, 76, 71, 71, 68, 65, 64, 64, 65, 65, 66, 66, 65, 66, 66, 67, 67, 68, 68, 67, 67, 65, 64, 60, 60, 61, 61, 57, 56, 54, 53, 50, 49, 44]], [[222, 221, 219, 218, 217, 216, 215, 213, 212, 209, 209, 208, 208, 209, 209, 215, 216, 219, 219, 218, 216, 216, 215, 215, 212, 212, 214, 215, 216, 217, 219, 221, 222, 225, 226, 226, 225, 225, 224, 224, 223, 220, 219, 213, 212, 208, 207, 205, 204, 202, 201, 200, 199, 197, 196, 195, 190, 190, 188, 185, 184, 182, 181, 179, 178, 177, 174, 169, 168, 166, 165, 163, 162, 159, 158, 152, 152, 150, 150, 149, 149, 153, 154, 156, 156, 157, 157, 162, 162, 158, 157, 156, 155, 155, 154, 153, 153, 152, 152, 151, 151, 155, 155, 158, 159, 163, 163, 164, 164, 165, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 178, 179, 180, 182, 183, 185, 186, 187, 187, 192, 193, 194, 196, 197, 199, 200, 202, 203, 204, 205, 207, 208, 210, 211, 205, 205, 204, 204, 203, 203, 202, 202, 201, 201, 200, 200, 198, 197, 195, 195, 197, 198, 199, 201, 201, 202, 202, 203, 203, 204, 204, 205, 205, 206, 206, 212, 213, 214, 215, 216, 217, 218, 219, 220, 222, 223, 225, 226, 231, 237, 237, 235, 231, 230, 231, 231, 230, 228, 226, 226, 227, 227, 228, 228, 229, 229], [152, 153, 153, 154, 154, 155, 155, 157, 157, 160, 161, 162, 165, 166, 167, 173, 173, 176, 173, 173, 171, 170, 169, 168, 165, 164, 162, 162, 161, 161, 159, 159, 158, 158, 159, 162, 163, 166, 167, 184, 185, 185, 186, 186, 185, 185, 186, 186, 187, 187, 188, 188, 189, 189, 190, 190, 185, 184, 182, 182, 183, 183, 184, 184, 183, 183, 186, 186, 185, 185, 184, 184, 183, 183, 182, 182, 183, 185, 189, 190, 205, 205, 204, 206, 208, 209, 210, 215, 217, 221, 220, 220, 219, 218, 218, 219, 227, 228, 229, 230, 233, 229, 228, 225, 225, 221, 220, 219, 218, 217, 214, 213, 213, 212, 212, 211, 211, 210, 210, 209, 209, 208, 208, 207, 207, 205, 205, 203, 203, 202, 201, 196, 196, 195, 195, 194, 194, 193, 193, 192, 192, 191, 191, 190, 190, 191, 197, 198, 199, 201, 202, 204, 205, 206, 207, 208, 209, 210, 212, 212, 214, 216, 216, 215, 215, 213, 212, 211, 210, 209, 208, 207, 206, 205, 203, 202, 201, 195, 195, 194, 194, 193, 193, 192, 192, 191, 191, 190, 190, 189, 194, 194, 191, 189, 189, 188, 187, 184, 183, 183, 181, 168, 167, 163, 162, 159, 158, 152]], [[289, 280, 279, 278, 276, 276, 266, 266, 267, 267, 266, 266, 267, 267, 274, 275, 276, 276, 277, 277, 278, 278, 279, 279, 282, 283, 285, 285, 286, 286, 287, 288, 288, 289, 289, 290, 290, 291, 291, 292, 292, 293, 293, 298, 298, 297, 297, 296, 296, 294, 294, 292], [6, 15, 15, 16, 16, 18, 28, 37, 38, 35, 34, 33, 32, 31, 24, 24, 25, 29, 30, 32, 33, 34, 35, 36, 39, 39, 41, 42, 43, 44, 45, 44, 43, 42, 39, 38, 34, 33, 32, 31, 29, 28, 26, 21, 16, 15, 13, 12, 11, 9, 8, 6]], [[315, 314, 313, 313, 312, 312, 311, 311, 310, 310, 308, 304, 301, 300, 299, 299, 298, 298, 297, 295, 295, 293, 293, 292, 292, 291, 291, 290, 290, 292, 292, 296, 296, 295, 295, 294, 294, 290, 289, 286, 285, 283, 283, 282, 282, 281, 281, 278, 274, 274, 279, 280, 280, 281, 283, 284, 285, 287, 288, 292, 293, 296, 297, 303, 304, 304, 308, 308, 310, 310, 311, 311, 312, 312, 313, 313, 312, 313, 313, 317, 318, 318, 319, 319, 320, 320, 321, 321, 323, 324, 325, 326, 330, 330, 331, 331, 332, 332, 331, 331, 330, 330, 326, 330, 331, 338, 338, 337, 333, 332], [176, 177, 177, 179, 180, 184, 185, 189, 190, 191, 193, 193, 196, 196, 195, 182, 181, 180, 179, 181, 182, 184, 186, 187, 189, 190, 192, 193, 198, 200, 201, 205, 209, 210, 213, 214, 220, 224, 224, 227, 227, 229, 230, 231, 236, 237, 241, 244, 244, 246, 246, 247, 253, 254, 254, 253, 253, 251, 251, 247, 247, 244, 244, 238, 238, 237, 233, 232, 230, 229, 228, 226, 225, 222, 221, 218, 217, 216, 214, 210, 210, 209, 208, 206, 205, 204, 203, 197, 195, 195, 196, 196, 200, 209, 208, 205, 204, 200, 199, 198, 197, 196, 192, 188, 188, 181, 178, 177, 177, 176]], [[461, 459, 459, 460, 460, 461, 461, 462, 462, 463, 463, 462, 458, 457, 456, 454, 452, 451, 450, 449, 447, 446, 444, 443, 441, 440, 437, 436, 436, 439, 440, 444, 443, 442, 441, 440, 439, 436, 435, 434, 434, 435, 450, 451, 450, 449, 447, 447, 448, 448, 451, 453, 454, 453, 453, 452, 452, 453, 450, 449, 444, 444, 447, 454, 455, 459, 460, 460, 463, 466, 467, 470, 471, 473, 474, 474, 475, 475, 474, 474, 475, 475, 474, 474, 475, 475, 476, 478, 479, 480, 480, 481, 481, 482, 482, 481, 481, 480, 480, 478, 477, 477, 476, 476, 475, 475, 474, 474, 473, 473, 471, 469, 469, 467, 467, 466, 466, 462], [217, 219, 224, 225, 229, 230, 234, 235, 237, 238, 241, 242, 242, 241, 241, 239, 239, 238, 238, 237, 237, 236, 236, 235, 235, 234, 234, 235, 238, 241, 241, 245, 246, 246, 247, 247, 246, 246, 247, 247, 251, 252, 252, 253, 254, 254, 256, 259, 260, 261, 264, 264, 265, 266, 267, 268, 271, 272, 275, 275, 280, 283, 286, 286, 287, 287, 288, 289, 292, 292, 291, 291, 290, 290, 289, 288, 287, 284, 283, 282, 281, 273, 272, 268, 267, 266, 265, 265, 266, 266, 263, 262, 253, 252, 239, 238, 220, 219, 217, 217, 218, 220, 221, 224, 225, 227, 228, 230, 231, 232, 234, 232, 226, 224, 223, 222, 221, 217]], [[402, 401, 398, 397, 393, 392, 392, 393, 399, 400, 400, 401, 401, 402, 402, 403, 406, 407, 408, 409, 410, 411, 411, 412, 412, 406], [336, 337, 337, 338, 338, 339, 343, 344, 344, 345, 347, 348, 349, 350, 351, 352, 352, 351, 351, 350, 350, 349, 348, 347, 342, 336]], [[397, 396, 396, 397, 398, 401, 402, 403, 404, 406, 407, 411, 412, 413, 414, 414, 412, 411, 407, 407, 405], [130, 131, 140, 140, 141, 141, 142, 142, 143, 143, 144, 144, 143, 143, 142, 139, 137, 137, 133, 132, 130]], [[118, 116, 115, 114, 113, 110, 109, 104, 104, 101, 101, 99, 99, 98, 97, 94, 93, 93, 89, 89, 87, 87, 86, 86, 84, 82, 80, 80, 78, 78, 76, 76, 72, 71, 70, 69, 68, 67, 66, 64, 63, 62, 60, 59, 56, 56, 55, 55, 54, 54, 53, 51, 50, 49, 48, 48, 49, 49, 52, 52, 54, 54, 53, 53, 52, 52, 54, 57, 57, 60, 60, 62, 62, 63, 64, 66, 67, 68, 68, 69, 68, 63, 62, 57, 56, 53, 52, 51, 51, 53, 53, 55, 55, 57, 57, 59, 59, 66, 68, 69, 69, 70, 70, 71, 71, 72, 72, 73, 73, 74, 74, 75, 75, 76, 76, 78, 79, 81, 82, 82, 83, 90, 91, 91, 92, 93, 94, 95, 97, 98, 100, 101, 103, 104, 105, 106, 108, 110, 111, 112, 113, 113, 112, 112, 111, 111, 110, 110, 109, 109, 108, 108, 107, 107, 106, 106, 107, 107, 108, 108, 109, 109, 110, 110, 110, 109, 109, 108, 108, 107, 107, 108, 109, 111, 114, 115, 117, 119, 120, 123, 123, 124, 124, 123, 123, 122, 120, 119, 116, 116, 117, 119, 120, 121, 124, 124, 127, 128, 129, 144, 145, 145, 144, 144, 169, 170, 170, 171, 171, 172, 172, 171, 170, 167, 167, 166, 165, 163, 162, 160, 159, 158, 157, 156, 155, 154, 153, 152, 151, 151, 150, 150, 151, 151, 150, 150, 149, 149, 147, 156, 156, 159, 159, 162, 162, 163, 163, 164, 164, 165, 165, 164, 164, 163, 163, 162, 162, 161, 161, 160, 161, 163, 164, 168, 169, 172, 173, 177, 178, 180, 181, 183, 184, 186, 187, 190, 191, 192, 193, 195, 196, 197, 197, 196, 194, 193, 190, 189, 187, 186, 184, 183, 181, 180, 177, 176, 172, 171, 168, 167, 163, 162, 158, 157, 153, 152, 151, 150, 150, 153, 153, 154, 153, 153, 151, 151, 149, 149, 147, 147, 144, 144, 141, 141, 140, 140, 139, 139, 138, 137, 136, 132, 131, 129, 128, 126, 126, 124, 121, 120, 117, 116, 111, 110, 110, 111, 111, 112, 112, 113, 113, 114, 114, 115, 115, 116, 116, 117, 117, 118, 118, 119, 119, 122, 122, 123, 123, 125, 126, 127, 129, 129, 126, 125, 125, 124, 123, 121], [256, 258, 258, 259, 259, 262, 262, 267, 268, 271, 272, 274, 275, 276, 276, 279, 279, 280, 284, 285, 287, 288, 289, 290, 292, 292, 290, 289, 287, 286, 284, 283, 279, 279, 278, 278, 277, 277, 278, 278, 279, 279, 281, 281, 284, 285, 286, 287, 288, 292, 293, 293, 294, 294, 295, 301, 302, 303, 306, 307, 309, 306, 305, 304, 303, 302, 300, 303, 304, 307, 309, 311, 312, 313, 313, 315, 315, 316, 320, 321, 322, 322, 323, 323, 324, 324, 325, 325, 333, 335, 336, 338, 339, 341, 342, 344, 345, 352, 352, 351, 350, 349, 347, 346, 344, 343, 341, 340, 338, 337, 335, 334, 332, 331, 329, 327, 328, 328, 327, 326, 325, 325, 324, 322, 321, 321, 322, 322, 324, 324, 326, 326, 328, 328, 329, 329, 331, 331, 330, 330, 331, 333, 334, 335, 336, 337, 338, 339, 340, 342, 343, 345, 346, 353, 354, 375, 376, 380, 381, 383, 384, 385, 386, 387, 384, 383, 380, 379, 374, 373, 364, 363, 363, 361, 361, 360, 360, 362, 362, 365, 369, 370, 375, 376, 378, 379, 379, 378, 378, 381, 382, 382, 383, 383, 386, 387, 390, 390, 391, 391, 392, 394, 395, 398, 398, 397, 396, 395, 394, 393, 387, 386, 386, 383, 382, 382, 381, 381, 380, 380, 379, 379, 378, 378, 377, 377, 376, 376, 375, 373, 372, 370, 369, 362, 361, 360, 359, 358, 356, 347, 346, 343, 342, 339, 338, 337, 336, 335, 334, 333, 324, 323, 314, 313, 309, 308, 304, 303, 300, 299, 298, 298, 299, 299, 300, 300, 301, 301, 302, 302, 303, 303, 304, 304, 305, 305, 306, 306, 307, 307, 308, 308, 305, 304, 304, 303, 303, 302, 302, 301, 301, 300, 300, 299, 299, 298, 298, 297, 297, 296, 296, 295, 295, 294, 294, 293, 293, 294, 297, 300, 301, 302, 303, 304, 306, 307, 309, 310, 312, 313, 316, 317, 320, 321, 322, 323, 324, 325, 326, 325, 325, 321, 321, 319, 319, 317, 316, 314, 314, 313, 313, 312, 312, 311, 309, 308, 307, 306, 305, 304, 303, 302, 299, 298, 296, 295, 293, 292, 291, 290, 288, 287, 285, 282, 274, 273, 270, 268, 268, 267, 267, 263, 263, 262, 259, 258, 258, 256]], [[284, 284, 286, 287, 296, 296, 300, 300, 304, 304, 303, 303, 302, 302, 301, 301, 300, 300, 298, 298, 297, 297, 296, 296, 295, 295, 294, 294, 293, 293, 292, 292, 291, 287, 286, 281, 280, 274, 273, 271, 268, 268, 267, 263, 262, 261, 260, 259, 254, 253, 252, 245, 244, 240, 239, 238, 237, 235, 230, 230, 229, 229, 230, 230, 227, 227, 229, 229, 228, 228, 227, 227, 228, 234, 236, 249, 250, 251, 252, 253, 254, 256, 258, 259, 260, 261, 261, 260, 260, 257, 257, 253, 253, 252, 252, 254, 254, 253, 249, 248, 245, 244, 241, 240, 236, 235, 232, 231, 226, 225, 221, 220, 215, 214, 211, 210, 206, 205, 203, 202, 199, 198, 195, 194, 192, 191, 189, 188, 186, 186, 188, 189, 192, 193, 195, 196, 199, 200, 203, 204, 208, 209, 212, 213, 216, 217, 222, 223, 228, 229, 232, 233, 237, 238, 241, 242, 246, 247, 250, 251, 253, 254, 256, 257, 258, 258, 256, 256, 255, 258, 258, 263, 263, 264, 264, 265, 265, 264, 264, 263, 263, 262, 262, 261, 261, 260, 260, 259, 259, 260, 261, 263, 264, 264, 266, 267, 268, 269, 271, 272, 273, 274, 276, 277, 278, 279, 280, 280, 279, 279, 278, 278, 277, 277, 276, 276, 277, 277, 278, 278, 279, 279, 280, 280, 282, 282, 284, 284, 294, 295, 295, 284, 284, 282, 282, 281, 281, 280, 280, 279, 279, 278, 278, 277, 277, 276, 277, 277, 278, 278, 279, 279, 280, 280, 281, 281, 282, 282, 283, 288, 289, 292, 295, 295, 296, 296, 299, 299, 300, 301, 302, 302, 303, 303, 304, 304, 305, 305, 307, 308, 310, 311, 312, 313, 314, 315, 316, 318, 319, 321, 322, 323, 330, 331, 334, 334, 332, 332, 331, 331, 329, 329, 328, 328, 326, 326, 325, 325, 324, 324, 322, 322, 321, 321, 320, 320, 318, 318, 317, 317, 316, 316, 314, 314, 313, 313, 312, 313, 317, 317, 316, 315, 314, 313, 312, 311, 308, 303, 303, 300, 300, 296, 296, 289, 288, 286], [302, 304, 306, 306, 315, 316, 320, 321, 325, 327, 328, 330, 331, 334, 335, 338, 339, 341, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 357, 358, 358, 359, 359, 360, 360, 361, 361, 358, 356, 355, 355, 356, 356, 357, 357, 362, 362, 363, 363, 364, 364, 365, 365, 366, 366, 371, 373, 374, 377, 378, 380, 383, 385, 387, 389, 390, 394, 395, 402, 403, 403, 405, 405, 406, 406, 405, 405, 404, 404, 402, 402, 401, 402, 403, 404, 405, 408, 409, 413, 414, 415, 418, 420, 422, 423, 423, 424, 424, 425, 425, 426, 426, 427, 427, 428, 428, 429, 429, 430, 430, 431, 431, 432, 432, 433, 433, 434, 434, 435, 435, 436, 436, 437, 437, 438, 438, 441, 441, 440, 440, 439, 439, 438, 438, 437, 437, 436, 436, 435, 435, 434, 434, 433, 433, 432, 432, 431, 431, 430, 430, 429, 429, 428, 428, 427, 427, 426, 426, 425, 425, 424, 424, 420, 418, 417, 416, 413, 412, 407, 406, 405, 403, 402, 399, 398, 395, 394, 392, 391, 388, 387, 385, 384, 382, 381, 380, 379, 379, 377, 378, 383, 385, 385, 384, 385, 385, 384, 384, 383, 385, 384, 384, 383, 384, 385, 386, 388, 389, 392, 393, 399, 400, 414, 415, 418, 419, 420, 421, 423, 424, 425, 427, 428, 430, 431, 441, 441, 439, 428, 427, 425, 424, 423, 422, 421, 420, 419, 418, 417, 415, 414, 407, 406, 405, 397, 396, 394, 393, 390, 389, 387, 386, 384, 383, 381, 380, 380, 381, 381, 378, 377, 376, 374, 371, 370, 369, 369, 370, 371, 372, 374, 375, 378, 379, 380, 382, 381, 381, 380, 380, 379, 379, 378, 378, 376, 376, 374, 374, 373, 373, 374, 374, 370, 368, 367, 366, 365, 363, 362, 361, 360, 358, 357, 356, 355, 354, 353, 351, 350, 349, 348, 347, 346, 344, 343, 342, 341, 340, 339, 337, 336, 335, 333, 332, 331, 331, 329, 329, 328, 328, 327, 327, 326, 326, 321, 320, 317, 316, 312, 311, 304, 304, 302]], [[393, 393, 392, 392, 391, 391, 390, 390, 389, 389, 388, 388, 387, 387, 386, 386, 385, 385, 384, 384, 383, 383, 381, 381, 380, 374, 373, 371, 370, 368, 367, 365, 364, 363, 362, 360, 359, 357, 356, 355, 354, 351, 350, 346, 345, 345, 344, 344, 343, 343, 344, 345, 345, 344, 344, 343, 343, 342, 342, 341, 341, 335, 334, 332, 330, 329, 328, 326, 326, 325, 325, 324, 324, 323, 323, 327, 328, 331, 332, 334, 335, 337, 337, 338, 339, 339, 340, 340, 341, 341, 342, 342, 343, 343, 341, 339, 338, 337, 336, 334, 332, 331, 330, 329, 328, 326, 325, 323, 322, 319, 318, 316, 315, 312, 311, 305, 304, 304, 300, 299, 294, 293, 286, 286, 282, 282, 281, 281, 280, 280, 279, 279, 278, 278, 277, 277, 276, 276, 277, 277, 278, 278, 279, 279, 280, 280, 281, 281, 282, 282, 280, 280, 279, 279, 278, 278, 277, 277, 276, 276, 277, 277, 278, 278, 279, 279, 280, 280, 281, 281, 283, 283, 295, 296, 301, 302, 303, 303, 304, 305, 306, 309, 310, 313, 314, 317, 318, 320, 321, 322, 323, 321, 321, 322, 322, 325, 326, 327, 327, 328, 329, 332, 333, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 348, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 363, 363, 362, 361, 360, 359, 358, 359, 361, 362, 363, 364, 366, 367, 368, 369, 378, 379, 388, 388, 384, 383, 384, 384, 385, 385, 386, 386, 387, 387, 388, 388, 389, 389, 390, 390, 391, 391, 392, 392, 393, 393, 394, 394], [372, 389, 390, 395, 396, 400, 401, 404, 405, 407, 408, 410, 411, 413, 414, 415, 416, 418, 419, 420, 421, 423, 425, 426, 427, 427, 428, 428, 429, 429, 430, 430, 431, 431, 432, 432, 433, 433, 434, 434, 435, 435, 436, 436, 435, 433, 432, 429, 428, 427, 426, 427, 423, 422, 415, 414, 411, 410, 409, 408, 407, 401, 401, 399, 399, 398, 398, 400, 401, 402, 403, 404, 405, 406, 410, 414, 414, 417, 417, 419, 419, 421, 422, 422, 423, 424, 425, 426, 427, 429, 430, 432, 433, 436, 438, 438, 439, 439, 440, 440, 442, 442, 443, 443, 444, 444, 445, 445, 446, 446, 447, 447, 448, 448, 449, 449, 448, 447, 443, 443, 438, 438, 431, 430, 426, 425, 424, 423, 422, 421, 420, 419, 418, 416, 415, 413, 412, 399, 398, 395, 394, 390, 389, 388, 387, 385, 384, 382, 381, 379, 381, 384, 385, 386, 387, 389, 390, 394, 395, 417, 418, 420, 421, 422, 423, 424, 425, 426, 427, 428, 430, 431, 443, 443, 448, 448, 449, 453, 453, 454, 453, 453, 452, 452, 451, 451, 450, 450, 449, 449, 450, 452, 456, 457, 458, 461, 461, 462, 463, 463, 464, 464, 463, 463, 462, 462, 461, 461, 460, 460, 459, 459, 458, 458, 457, 457, 455, 455, 454, 454, 453, 453, 452, 452, 451, 451, 450, 450, 448, 440, 439, 439, 438, 438, 437, 436, 436, 435, 435, 434, 434, 433, 433, 432, 432, 431, 431, 428, 428, 427, 426, 424, 423, 421, 420, 419, 418, 416, 415, 414, 413, 411, 410, 407, 406, 404, 403, 399, 398, 393, 392, 372]]], "confs": [0.95751953125, 0.93994140625, 0.9287109375, 0.9248046875, 0.9599609375, 0.9619140625, 0.8984375, 0.83349609375, 0.705078125, 0.57080078125, 0.319580078125], "classes": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], "scales": [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], "identifier": "XXXX", "image_path": "ALUS_Non-miteArachnids_Unknown_2020_11_03_4545.jpg", "image_width": 1940, "image_height": 1933, "mask_width": 484, "mask_height": 482} \ No newline at end of file diff --git a/src/flat_bug/yolo_helpers.py b/src/flat_bug/yolo_helpers.py index cfc4cfc..e04efe1 100644 --- a/src/flat_bug/yolo_helpers.py +++ b/src/flat_bug/yolo_helpers.py @@ -124,11 +124,11 @@ def merge_tile_results( exclude_masks : bool=False ) : """Merge results from multiple images into a single Results object, possibly with a new image.""" - assert isinstance(results[0].boxes, torch.Tensor) + assert results[0].boxes is not None and isinstance(results[0].boxes.data, torch.Tensor) _device = results[0].boxes.data.device if orig_img is None: - orig_img = results[0].orig_img - assert isinstance(orig_img, torch.Tensor), f"orig_img must be a torch.Tensor, not {type(orig_img)}" + orig_img = torch.as_tensor(results[0].orig_img) + assert isinstance(orig_img, torch.Tensor), f"orig_img must be a torch.Tensor, not {type(orig_img).__name__}" if box_offsetters is None: box_offsetters = torch.zeros((len(results), 2), device=_device).int() if mask_offsetters is None: diff --git a/src/flat_bug/tests/README.txt b/tests/README.txt similarity index 62% rename from src/flat_bug/tests/README.txt rename to tests/README.txt index f9d6459..d49a530 100644 --- a/src/flat_bug/tests/README.txt +++ b/tests/README.txt @@ -1,3 +1,15 @@ +# Run unit tests + +From the repository root, run: + +```bash +uv run pytest tests +``` + +## Details + +Changes to the [test assets](./assets) should (usually) not be committed! + The files: - pyramid_tps_**.tps @@ -7,6 +19,6 @@ The files: Are stored with `git update-index --assume-unchanged`, and should not be updated in the repository, instead the files stored in the ERDA test file repository (https://anon.erda.au.dk/cgi-sid/ls.py?share_id=ecgKtuRWe5) should be swapped instead. -Use the script `restore_assets.py` to restore the files to links if necessary. +The script `restore_assets.py` is automatically run after all unit tests. Contact: asgersvenning@ecos.au.dk diff --git a/src/flat_bug/tests/__init__.py b/tests/__init__.py similarity index 100% rename from src/flat_bug/tests/__init__.py rename to tests/__init__.py diff --git a/tests/assets/.gitignore b/tests/assets/.gitignore new file mode 100644 index 0000000..abf0d4b --- /dev/null +++ b/tests/assets/.gitignore @@ -0,0 +1,2 @@ +* +!ALUS_Non-miteArachnids_Unknown_2020_11_03_4545.jpg \ No newline at end of file diff --git a/src/flat_bug/tests/assets/ALUS_Non-miteArachnids_Unknown_2020_11_03_4545.jpg b/tests/assets/ALUS_Non-miteArachnids_Unknown_2020_11_03_4545.jpg similarity index 100% rename from src/flat_bug/tests/assets/ALUS_Non-miteArachnids_Unknown_2020_11_03_4545.jpg rename to tests/assets/ALUS_Non-miteArachnids_Unknown_2020_11_03_4545.jpg diff --git a/src/flat_bug/tests/assets/pyramid_output_length.txt b/tests/assets/pyramid_output_length.txt similarity index 100% rename from src/flat_bug/tests/assets/pyramid_output_length.txt rename to tests/assets/pyramid_output_length.txt diff --git a/src/flat_bug/tests/assets/single_scale_output_length.txt b/tests/assets/single_scale_output_length.txt similarity index 100% rename from src/flat_bug/tests/assets/single_scale_output_length.txt rename to tests/assets/single_scale_output_length.txt diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..6c61548 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,15 @@ +# tests/conftest.py +import subprocess +import os + +def pytest_sessionfinish(session, exitstatus): + """ + This hook runs after all tests have completed. + """ + script_path = os.path.join(os.path.dirname(__file__), "restore_assets.py") + print(f"\n\nRunning Post-Test Cleanup:\n\t{script_path}\n") + + try: + subprocess.run(["python3", script_path], check=False) + except Exception as e: + print(f"Post-test script failed to execute: {e}") \ No newline at end of file diff --git a/src/flat_bug/tests/generate_model_outputs.py b/tests/generate_model_outputs.py similarity index 94% rename from src/flat_bug/tests/generate_model_outputs.py rename to tests/generate_model_outputs.py index 8436f89..23c86f7 100644 --- a/src/flat_bug/tests/generate_model_outputs.py +++ b/tests/generate_model_outputs.py @@ -5,7 +5,7 @@ import torch from torchvision.io import read_image -from flat_bug.tests.test_predictor import ASSET_DIR, ASSET_NAME, TEST_MODEL_NAME, DummyModel +from tests.test_predictor import ASSET_DIR, ASSET_NAME, TEST_MODEL_NAME, DummyModel # ruff: disable[E501] # Command I used: diff --git a/src/flat_bug/tests/remote_lfs_fallback.py b/tests/remote_lfs_fallback.py similarity index 100% rename from src/flat_bug/tests/remote_lfs_fallback.py rename to tests/remote_lfs_fallback.py diff --git a/src/flat_bug/tests/restore_assets.py b/tests/restore_assets.py similarity index 67% rename from src/flat_bug/tests/restore_assets.py rename to tests/restore_assets.py index 55b4c62..080e085 100644 --- a/src/flat_bug/tests/restore_assets.py +++ b/tests/restore_assets.py @@ -9,8 +9,10 @@ if f.startswith("pyramid_") or f.startswith("single_scale_") ] + print("-------------------------- Restoring test assets --------------------------") for asset in prediction_assets: - print(asset) + print("\t", asset) with open(asset, "w") as f: f.write("ERDA Pointer") + print("-------------------------- Test assets restored! --------------------------") \ No newline at end of file diff --git a/src/flat_bug/tests/test_augmentations.py b/tests/test_augmentations.py similarity index 79% rename from src/flat_bug/tests/test_augmentations.py rename to tests/test_augmentations.py index de19def..f404daf 100644 --- a/src/flat_bug/tests/test_augmentations.py +++ b/tests/test_augmentations.py @@ -1,7 +1,6 @@ """Tests for flatbug augmentations (including their integration into the dataloader).""" import math import os -import unittest from copy import deepcopy from typing import Any @@ -16,7 +15,7 @@ from ultralytics.utils.ops import resample_segments from flat_bug.datasets import train_augmentation_pipeline, validation_augmentation_pipeline -from flat_bug.tests.remote_lfs_fallback import check_file_with_remote_fallback +from tests.remote_lfs_fallback import check_file_with_remote_fallback TEST_HYP = { "hsv_h": 0.5, @@ -132,19 +131,18 @@ def make_empty(obj : Any) -> Any: # noqa: D103 obj = [] return obj -class TestMockYOLOHelpers(unittest.TestCase): # noqa: D101 +class TestMockYOLOHelpers: # noqa: D101 def test_mock_yolo_base_dataset_load_image(self): # noqa: D102 loaded_img, _, _ = mock_yolo_base_dataset_load_image(TEST_IMG, TEST_HYP["imgsz"]) - self.assertIsInstance(loaded_img, np.ndarray, msg=f"Expected {np.ndarray} object, got {type(loaded_img)}") - self.assertEqual( - loaded_img.shape, (TEST_HYP["imgsz"], TEST_HYP["imgsz"], 3), - msg=f"Expected image shape ({TEST_HYP['imgsz']}, {TEST_HYP['imgsz']}, 3), got {loaded_img.shape}" + assert isinstance(loaded_img, np.ndarray), f"Expected np.ndarray object, got {type(loaded_img).__name__}" + assert loaded_img.shape == (TEST_HYP["imgsz"], TEST_HYP["imgsz"], 3), ( + f"Expected image shape ({TEST_HYP['imgsz']}, {TEST_HYP['imgsz']}, 3), got {loaded_img.shape}" ) def test_mock_verify_image_label(self): # noqa: D102 result = mock_verify_image_label(TEST_IMG, TEST_LABEL) - self.assertIsInstance(result, dict, msg=f"Expected {dict} object, got {type(result)}") - correct = { + assert isinstance(result, dict), f"Expected dict, got {type(result).__name__}" + correct : dict[str, type | None] = { "im_file": str, "shape": tuple, "cls": np.ndarray, @@ -156,19 +154,21 @@ def test_mock_verify_image_label(self): # noqa: D102 "instances": Instances } for k, v in correct.items(): - self.assertTrue(k in result, msg=f"Missing key '{k}' in result") + assert k in result, f"Missing key '{k}' in result" if v is None: continue - self.assertIsInstance(result[k], v, msg=f"Invalid type for key '{k}'. Expected {v}, got {type(result[k])}") + assert isinstance(result[k], v), ( + f"Invalid type for key '{k}'. Expected {v.__name__}, got {type(result[k]).__name__}" + ) -class TestAugmentations(unittest.TestCase): # noqa: D101 +class TestAugmentations: # noqa: D101 def test_generate_train_augmentation_pipeline(self): # noqa: D102 pipeline = generate_train_augmentation_pipeline(TEST_HYP) - self.assertTrue(isinstance(pipeline, Compose), msg=f"Expected {Compose} object, got {type(pipeline)}") + assert isinstance(pipeline, Compose), f"Expected Compose object, got {type(pipeline).__name__}" def test_generate_validation_augmentation_pipeline(self): # noqa: D102 pipeline = generate_validation_augmentation_pipeline(TEST_HYP) - self.assertTrue(isinstance(pipeline, Compose), msg=f"Expected {Compose} object, got {type(pipeline)}") + assert isinstance(pipeline, Compose), f"Expected Compose object, got {type(pipeline).__name__}" def test_train_augmentation_pipeline(self): # noqa: D102 pipeline = generate_train_augmentation_pipeline(TEST_HYP) @@ -186,8 +186,9 @@ def test_train_augmentation_pipeline(self): # noqa: D102 # cv2.drawContours(out_img, polys, -1, (0, 255, 0), 2) # cv2.imwrite(ASSET_DIR + "/test_train_augmentation_pipeline.jpg", out_img) except Exception as e: - raise type(e)("Failed to execute training augmentation pipeline on image with labels due to:\n\t" + str(e)) - self.assertIsInstance(out, dict, msg="Invalid output of training augmentation pipeline on image with labels") + e.add_note("Failed to execute training augmentation pipeline on image with labels.") + raise + assert isinstance(out, dict), "Invalid output of training augmentation pipeline on image with labels" # Simulate empty labels empty_pipeline_input = deepcopy(pipeline_input) for k, v in empty_pipeline_input.items(): @@ -206,8 +207,9 @@ def test_train_augmentation_pipeline(self): # noqa: D102 try: out = pipeline(empty_pipeline_input) except Exception as e: - raise type(e)("Failed to execute training augmentation pipeline on image without labels due to:\n\t" + str(e)) - self.assertIsInstance(out, dict, msg="Invalid output of training augmentation pipeline on image without labels") + e.add_note("Failed to execute training augmentation pipeline on image without labels.") + raise + assert isinstance(out, dict), "Invalid output of training augmentation pipeline on image without labels" def test_validation_augmentation_pipeline(self): # noqa: D102 pipeline = generate_validation_augmentation_pipeline(TEST_HYP) @@ -218,8 +220,9 @@ def test_validation_augmentation_pipeline(self): # noqa: D102 try: out = pipeline(deepcopy(pipeline_input)) except Exception as e: - raise type(e)("Failed to execute validation augmentation pipeline on image with labels due to:\n\t" + str(e)) - self.assertIsInstance(out, dict, msg="Invalid output of validation augmentation pipeline on image with labels") + e.add_note("Failed to execute validation augmentation pipeline on image with labels.") + raise + assert isinstance(out, dict), "Invalid output of validation augmentation pipeline on image with labels" # Simulate empty labels empty_pipeline_input = deepcopy(pipeline_input) for k, v in empty_pipeline_input.items(): @@ -238,9 +241,6 @@ def test_validation_augmentation_pipeline(self): # noqa: D102 try: out = pipeline(empty_pipeline_input) except Exception as e: - raise type(e)("Failed to execute validation augmentation pipeline on image without labels due to:\n\t" + str(e)) - self.assertIsInstance(out, dict, msg="Invalid output of validation augmentation pipeline on image without labels") - - -if __name__ == "__main__": - unittest.main() \ No newline at end of file + e.add_note("Failed to execute validation augmentation pipeline on image without labels.") + raise + assert isinstance(out, dict), "Invalid output of validation augmentation pipeline on image without labels." \ No newline at end of file diff --git a/src/flat_bug/tests/test_config.py b/tests/test_config.py similarity index 86% rename from src/flat_bug/tests/test_config.py rename to tests/test_config.py index 5a08f0b..700df90 100644 --- a/src/flat_bug/tests/test_config.py +++ b/tests/test_config.py @@ -2,7 +2,7 @@ import copy import os import tempfile -import unittest +import pytest from flat_bug.config import DEFAULT_CFG, check_cfg_types, check_types, get_type_def, read_cfg, write_cfg @@ -52,7 +52,7 @@ def check_equals_recursive(obj1, obj2): # noqa: D103 return True return obj1 == obj2 -class TestConfig(unittest.TestCase): # noqa: D101 +class TestConfig: # noqa: D101 def test_check_types(self): # noqa: D102 for i, (key, obj) in enumerate(TEST_OBJECTS.items()): expected_type = get_type_def(obj) @@ -67,7 +67,7 @@ def test_check_cfg_types(self): # noqa: D102 type(e)("Error raised when checking the types of the default config:\n" + str(e)) altered_cfg = copy.deepcopy(DEFAULT_CFG) altered_cfg["UNKNOWN_KEY"] = "value" - with self.assertRaises(KeyError, msg="Failed to raise an error when checking config with unknown key and strict=True"): + with pytest.raises(KeyError): check_cfg_types(altered_cfg, strict=True) try: check_cfg_types(altered_cfg, strict=False) @@ -91,8 +91,7 @@ def test_get_type_def(self): # noqa: D102 obj = TEST_OBJECTS[key] type_def = get_type_def(obj, tuple_list_interchangeable=False) # Check that the generated type definition is the same as the expected type definition - self.assertTrue( - check_equals_recursive(type_def, expected_type), + assert check_equals_recursive(type_def, expected_type), ( f"\nFailed on object:\n'{key}' => {obj}\n" f"with generated type definition:\n{type_def}\n" f"and expected type definition:\n{expected_type}" @@ -107,8 +106,7 @@ def test_get_type_def(self): # noqa: D102 obj = TEST_OBJECTS[key] type_def = get_type_def(obj, tuple_list_interchangeable=True) # Check that the generated type definition is the same as the expected type definition - self.assertTrue( - check_equals_recursive(type_def, expected_type), + assert check_equals_recursive(type_def, expected_type), ( f"\nFailed on object:\n'{key}' => {obj}\n" f"with generated type definition:\n{type_def}" f"\nand expected type definition:\n{expected_type}" @@ -117,7 +115,8 @@ def test_get_type_def(self): # noqa: D102 # (no need to assertTrue, since check_types will raise an error if it fails) check_types(obj, type_def, f"Object {key}") except Exception as e: - raise type(e)(str(e) + error_msg.format(True)) + e.add_note(error_msg.format(True)) + raise def test_default_cfg(self): # noqa: D102 try: @@ -129,7 +128,7 @@ def test_write_read_cfg(self): # noqa: D102 orig_cfg = copy.deepcopy(DEFAULT_CFG) with tempfile.TemporaryDirectory() as tmpdir: invalid_cfg_file = os.path.join(tmpdir, "test.cfg") - with self.assertRaises(ValueError, msg="Failed to raise an error when writing a config file with an invalid extension"): + with pytest.raises(ValueError): write_cfg(orig_cfg, invalid_cfg_file) cfg_file = os.path.join(tmpdir, "test.yaml") write_cfg(orig_cfg, cfg_file) @@ -138,20 +137,15 @@ def test_write_read_cfg(self): # noqa: D102 alter_cfg["UNKNOWN_KEY"] = "value" alter_file = os.path.join(tmpdir, "alter_test.yaml") write_cfg(alter_cfg, alter_file) - with self.assertRaises(KeyError, msg="Failed to raise an error when reading a config file with unknown key and strict=True"): + with pytest.raises(KeyError): read_cfg(alter_file, strict=True) try: read_cfg(alter_file) except Exception as e: type(e)("Error raised when reading a config file with unknown key and strict=False:\n" + str(e)) - self.assertTrue( - check_types(new_cfg, get_type_def(orig_cfg), "Reconstructed Config", strict=False), + assert check_types(new_cfg, get_type_def(orig_cfg), "Reconstructed Config", strict=False), ( "Failed to reconstruct the original config with comparable types after writing and reading.") - self.assertTrue( - check_equals_recursive(orig_cfg, new_cfg), + assert check_equals_recursive(orig_cfg, new_cfg), ( "Failed to reconstruct the values of the original config after writing and reading. " "Although the types are comparable, the values are not equal." - ) - -if __name__ == '__main__': - unittest.main() \ No newline at end of file + ) \ No newline at end of file diff --git a/src/flat_bug/tests/test_dataset.py b/tests/test_dataset.py similarity index 80% rename from src/flat_bug/tests/test_dataset.py rename to tests/test_dataset.py index f9f03af..99243d6 100644 --- a/src/flat_bug/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -2,8 +2,8 @@ import glob import os import tempfile -import unittest from copy import deepcopy +import pytest import numpy as np from ultralytics.data import build_dataloader @@ -14,7 +14,8 @@ from ultralytics.utils.plotting import plot_images from flat_bug.datasets import FlatBugYOLODataset, FlatBugYOLOValidationDataset -from flat_bug.tests.remote_lfs_fallback import check_file_with_remote_fallback +from tests.remote_lfs_fallback import check_file_with_remote_fallback + TEST_DIR = os.path.dirname(__file__) ASSET_DIR = os.path.join(TEST_DIR, "assets") @@ -100,7 +101,7 @@ def create_validation_dataset(args : IterableSimpleNamespace) -> FlatBugYOLOVali subset_args={"n" : 1, "pattern" : ASSET_NAME} ) -def test_plot_batch(batch, ni): # noqa: D103 +def _test_plot_batch(batch, ni): # noqa: D103 with tempfile.NamedTemporaryFile(delete=False, suffix=".jpg") as f: plot_images( batch["img"], @@ -113,7 +114,8 @@ def test_plot_batch(batch, ni): # noqa: D103 on_plot=os.remove, ) -class TestDataset(unittest.TestCase): # noqa: D101 +@pytest.mark.filterwarnings("ignore:.*argument is set as true but no accelerator is found.*:UserWarning") +class TestDataset: # noqa: D101 def test_train_dataset(self): # noqa: D102 args = IterableSimpleNamespace(**TEST_CFG) if not isinstance(TEST_CFG, IterableSimpleNamespace) else TEST_CFG dataset = create_train_dataset(args) @@ -121,12 +123,9 @@ def test_train_dataset(self): # noqa: D102 dataloader_iter = dataloader.iterator for batch, _ in zip(dataloader_iter, range(1)): - self.assertEqual(batch["img"].shape[0], BATCH_SIZE) + assert batch["img"].shape[0] == BATCH_SIZE - try: - test_plot_batch(batch, 0) - except Exception as e: - self.fail(f"Failed to plot training batch: {e}") + _test_plot_batch(batch, 0) def test_validation_dataset(self): # noqa: D102 args = IterableSimpleNamespace(**TEST_CFG) if not isinstance(TEST_CFG, IterableSimpleNamespace) else TEST_CFG @@ -135,33 +134,26 @@ def test_validation_dataset(self): # noqa: D102 dataloader_iter = dataloader.iterator for batch, _ in zip(dataloader_iter, range(1)): - self.assertEqual(batch["img"].shape[0], BATCH_SIZE) + assert batch["img"].shape[0] == BATCH_SIZE - try: - test_plot_batch(batch, 0) - except Exception as e: - self.fail(f"Failed to plot validation batch: {e}") + _test_plot_batch(batch, 0) def test_verify_image_label(self): # noqa: D102 label = mock_verify_image_label(IMAGE_ASSET, LABEL_ASSET) - self.assertIsInstance(label, dict) - self.assertIn("im_file", label) - self.assertIn("shape", label) - self.assertIn("cls", label) - self.assertIn("bboxes", label) - self.assertIn("segments", label) - self.assertIn("keypoints", label) - self.assertIn("normalized", label) - self.assertIn("bbox_format", label) - self.assertIn("instances", label) + assert isinstance(label, dict) + assert "im_file" in label + assert "shape" in label + assert "cls" in label + assert "bboxes" in label + assert "segments" in label + assert "keypoints" in label + assert "normalized" in label + assert "bbox_format" in label + assert "instances" in label @classmethod - def tearDownClass(cls): # noqa: D102 + def teardown_class(cls): # noqa: D102 # Clean caches i.e. files ending with .cache or .cache.lock in the directory of this script cache_files = glob.glob(os.path.join(TEST_DIR, "*.cache*")) for cache_file in cache_files: - os.remove(cache_file) - -if __name__ == "__main__": - unittest.main() - + os.remove(cache_file) \ No newline at end of file diff --git a/src/flat_bug/tests/test_predictor.py b/tests/test_predictor.py similarity index 84% rename from src/flat_bug/tests/test_predictor.py rename to tests/test_predictor.py index 334eafd..e094b62 100644 --- a/src/flat_bug/tests/test_predictor.py +++ b/tests/test_predictor.py @@ -3,7 +3,6 @@ import re import shutil import tempfile -import unittest from collections import defaultdict from glob import glob @@ -13,7 +12,7 @@ from flat_bug import logger from flat_bug.predictor import Predictor, TensorPredictions -from flat_bug.tests.remote_lfs_fallback import check_file_with_remote_fallback +from tests.remote_lfs_fallback import check_file_with_remote_fallback TEST_MODEL_NAME = "flat_bug_M.pt" PYRAMID_SCALE_BEFORE = 0.6 @@ -44,18 +43,15 @@ "BATCH_SIZE": 1 } -class TestTensorPredictions(unittest.TestCase): # noqa: D101 +class TestTensorPredictions: # noqa: D101 def test_load(self): # noqa: D102 - tp = TensorPredictions() - tp.load(check_file_with_remote_fallback(SERIALISED_TENSOR_PREDS)) - self.assertEqual( - len(tp), N_PREDICTIONS, - msg=f"Number of predictions ({len(tp)}) does not match the expected number of predictions ({N_PREDICTIONS})" + tp = TensorPredictions.load(check_file_with_remote_fallback(SERIALISED_TENSOR_PREDS)) + assert len(tp) == N_PREDICTIONS, ( + f"Number of predictions ({len(tp)}) does not match the expected number of predictions ({N_PREDICTIONS})" ) def test_save(self): # noqa: D102 - tp = TensorPredictions() - tp = tp.load(check_file_with_remote_fallback(SERIALISED_TENSOR_PREDS)) + tp = TensorPredictions.load(check_file_with_remote_fallback(SERIALISED_TENSOR_PREDS)) image_path = os.path.join(os.path.dirname(__file__), "assets", f"{ASSET_NAME}.jpg") check_file_with_remote_fallback(image_path) tp.image = read_image(image_path) * 255 @@ -63,7 +59,7 @@ def test_save(self): # noqa: D102 with tempfile.TemporaryDirectory() as tmp_directory: save_dir = tp.save(tmp_directory, mask_crops=True, wait=True) assert save_dir is not None - self.assertTrue(os.path.exists(os.path.join(save_dir, "crops"))) + assert os.path.exists(os.path.join(save_dir, "crops")) crops = glob(os.path.join(save_dir, "crops", "*")) n_crops = len(crops) # ###### DEBUG ###### @@ -71,21 +67,19 @@ def test_save(self): # noqa: D102 # overview = glob(os.path.join(save_dir, "overview*"))[0] # shutil.move(overview, os.path.join(os.path.dirname(__file__), "assets", os.path.basename(overview))) # ################### - self.assertEqual( - n_crops, N_PREDICTIONS, - msg=f"Number of crops ({n_crops}) saved does not match the expected number of predictions ({N_PREDICTIONS})" + assert n_crops == N_PREDICTIONS, ( + f"Number of crops ({n_crops}) saved does not match the expected number of predictions ({N_PREDICTIONS})" ) centroid_initial = [i.float().mean(dim=0).numpy() for i in tp.contours] centroid_reloaded = [ i.float().mean(dim=0).numpy() - for i in TensorPredictions().load(glob(os.path.join(save_dir, "metadata*.json"))[0]).contours + for i in TensorPredictions.load(glob(os.path.join(save_dir, "metadata*.json"))[0]).contours ] centroid_initial = np.stack(centroid_initial) centroid_reloaded = np.stack(centroid_reloaded) abs_diff = np.abs(centroid_initial - centroid_reloaded).max() - self.assertTrue( - abs_diff < 0.01, - msg=f"Centroid difference between initial and reloaded contours ({abs_diff}) is too large" + assert abs_diff < 0.01, ( + f"Centroid difference between initial and reloaded contours ({abs_diff}) is too large" ) def cast_nested(obj, new_dtype): # noqa: D103 @@ -191,7 +185,7 @@ def generate_pyramid_files(self, weights, image, image_path): # noqa: D102 with open(os.path.join(self.asset_dir, "pyramid_output_length.txt"), "w") as f: f.write(str(len(output))) -class TestPredictor(unittest.TestCase): # noqa: D101 +class TestPredictor: # noqa: D101 TOLERANCE = 0.1 def test_single_scale(self): # noqa: D102 @@ -209,9 +203,8 @@ def test_single_scale(self): # noqa: D102 with open(check_file_with_remote_fallback(os.path.join(ASSET_DIR, "single_scale_output_length.txt"))) as f: reference_length = int(f.read()) # Check that the output length is within tolerance of the reference length - self.assertTrue( - abs(1 - output_length/reference_length) < self.TOLERANCE, - msg=f"Output length ({output_length}) does not match the reference length ({reference_length})" + assert abs(1 - output_length/reference_length) < self.TOLERANCE, ( + f"Output length ({output_length}) does not match the reference length ({reference_length})" ) def test_pyramid(self): # noqa: D102 @@ -228,10 +221,6 @@ def test_pyramid(self): # noqa: D102 with open(check_file_with_remote_fallback(os.path.join(ASSET_DIR, "pyramid_output_length.txt"))) as f: reference_length = int(f.read()) # Check that the output length is within tolerance of the reference length - self.assertTrue( - abs(1 - output_length/reference_length) < self.TOLERANCE, - msg=f"Output length ({output_length}) does not match the reference length ({reference_length})" - ) - -if __name__ == '__main__': - unittest.main() \ No newline at end of file + assert abs(1 - output_length/reference_length) < self.TOLERANCE, ( + f"Output length ({output_length}) does not match the reference length ({reference_length})" + ) \ No newline at end of file diff --git a/uv.lock b/uv.lock index d5e179a..9992e21 100644 --- a/uv.lock +++ b/uv.lock @@ -619,7 +619,7 @@ wheels = [ [[package]] name = "flat-bug" -version = "1.0.0" +version = "1.1.0" source = { editable = "." } dependencies = [ { name = "scikit-optimize" }, @@ -638,20 +638,21 @@ cloud-datasets = [ erda = [ { name = "pyremotedata" }, ] -jupyter = [ - { name = "jupyter" }, -] [package.dev-dependencies] -dev = [ - { name = "flat-bug", extra = ["cloud-datasets", "erda"] }, +notebook = [ + { name = "ipykernel" }, + { name = "ipywidgets" }, + { name = "jupyter" }, +] +tests = [ + { name = "pytest" }, ] [package.metadata] requires-dist = [ { name = "boto3", marker = "extra == 'cloud-datasets'", specifier = ">=1.40" }, { name = "cvat-sdk", marker = "extra == 'cloud-datasets'", specifier = ">=2.47" }, - { name = "jupyter", marker = "extra == 'jupyter'", specifier = ">=1.0.0" }, { name = "pyremotedata", marker = "extra == 'erda'", specifier = ">=0.0.16" }, { name = "scikit-optimize", specifier = ">=0.10.1" }, { name = "scipy", specifier = ">=1.14.1" }, @@ -660,10 +661,15 @@ requires-dist = [ { name = "torchvision", specifier = ">=0.17.0" }, { name = "ultralytics", specifier = ">=8.2.16,<=8.3.124" }, ] -provides-extras = ["erda", "cloud-datasets", "jupyter"] +provides-extras = ["erda", "cloud-datasets"] [package.metadata.requires-dev] -dev = [{ name = "flat-bug", extras = ["notebook", "erda", "cloud-datasets"] }] +notebook = [ + { name = "ipykernel", specifier = ">=7.2.0" }, + { name = "ipywidgets", specifier = ">=8.1.8" }, + { name = "jupyter", specifier = ">=1.0.0" }, +] +tests = [{ name = "pytest", specifier = ">=9.0.3" }] [[package]] name = "fonttools" @@ -778,6 +784,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "ipykernel" version = "7.2.0" @@ -1998,6 +2013,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +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 = "prometheus-client" version = "0.25.0" @@ -2126,6 +2150,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/2a/917b538610294ab56afca0fb9111930c0d1ffb6ee2b0d40cbc33db0193fc/pyremotedata-0.1.13-py3-none-any.whl", hash = "sha256:488b2e33c6c28155e91cb98e27c87627a87c44d89ab945684d59b8d308fe8f33", size = 29055, upload-time = "2026-04-16T13:34:07.673Z" }, ] +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" From 8a2c8379c176d791f35ff7aabea651a121f859de Mon Sep 17 00:00:00 2001 From: asgersvenning Date: Tue, 28 Apr 2026 15:42:26 +0200 Subject: [PATCH 11/41] Ensure all test files are kept as-is --- tests/assets/.gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/assets/.gitignore b/tests/assets/.gitignore index abf0d4b..f59ec20 100644 --- a/tests/assets/.gitignore +++ b/tests/assets/.gitignore @@ -1,2 +1 @@ -* -!ALUS_Non-miteArachnids_Unknown_2020_11_03_4545.jpg \ No newline at end of file +* \ No newline at end of file From 8989a30bac9ad86151d2f9e222667c6b250b0367 Mon Sep 17 00:00:00 2001 From: asgersvenning Date: Tue, 28 Apr 2026 15:57:46 +0200 Subject: [PATCH 12/41] Migrate GitHub workflows to uv --- .github/workflows/build_docs.yml | 15 +- .github/workflows/run-tests.yml | 36 ++-- docs/README.md | 11 +- pyproject.toml | 5 + uv.lock | 275 ++++++++++++++++++++++++++++++- 5 files changed, 311 insertions(+), 31 deletions(-) diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index 07cdbae..08918e7 100644 --- a/.github/workflows/build_docs.yml +++ b/.github/workflows/build_docs.yml @@ -12,16 +12,21 @@ jobs: docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v6 + - uses: astral-sh/setup-uv@v8.1.0 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" + python-version: 3.14 + - name: Install dependencies run: | - pip install sphinx sphinx_rtd_theme myst_parser furo - pip install -e . + uv sync --all-extras --group docs + uv pip install sphinx sphinx_rtd_theme myst_parser furo - name: Sphinx build run: | cd ./docs - make html + uv run make html - name: Copy built docs run: cp -r docs/build/html ./public_docs - name: Deploy to GitHub Pages diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index f127124..579afce 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -16,7 +16,7 @@ on: concurrency: group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }} jobs: Continous-Testing: @@ -30,23 +30,21 @@ jobs: - run: echo "The job was automatically triggered by a ${{ github.event_name }} event." - run: echo "This job is now running on a ${{ runner.os }} server hosted by GitHub!" - run: echo "The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." - - uses: actions/checkout@v4 - - run: echo "The ${{ github.repository }} repository has been cloned to the runner." - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v8.1.0 with: + enable-cache: true + cache-dependency-glob: "uv.lock" python-version: ${{ matrix.python-version }} - - name: Display Python version - run: python -c "import sys; print(sys.version)" - - run: echo "The workflow is now ready to test your code on the runner." - - name: List files in the repository - run: | - ls ${{ github.workspace }} - - name: Install package - run: | - python -m pip install --upgrade pip - python -m pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu - python -m pip install -e . - - name: Run unit tests - run: | - python -m unittest discover -s src/flat_bug/tests \ No newline at end of file + + - name: Install the project + run: uv sync --locked --all-extras --group tests + + - name: Lint with Ruff + run: uv run ruff check . --ignore E501 + + - name: Run tests + run: uv run pytest tests \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 2048570..f5061e2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,13 +1,14 @@ -To build the docs you need to install `sphinx`, `furo` and `myst-parser`: +# Build docs + +Simply install with the `docs` dependency group: ```sh -micromamba install sphinx -micromamba install furo -micromamba install myst-parser +uv sync --all-extras --group docs ``` And then run: ```sh -make html +# cd ./docs +uv run make html ``` diff --git a/pyproject.toml b/pyproject.toml index 5c636ff..127baf6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,11 @@ fb_prepare_data = "bin.fb_prepare_data:main" fb_clone_data = "bin.fb_clone_data:main" [dependency-groups] +docs = [ + "furo>=2025.12.19", + "myst-parser>=5.0.0", + "sphinx>=9.0.4", +] notebook = [ "ipykernel>=7.2.0", "ipywidgets>=8.1.8", diff --git a/uv.lock b/uv.lock index 9992e21..5184090 100644 --- a/uv.lock +++ b/uv.lock @@ -3,9 +3,32 @@ revision = 3 requires-python = ">=3.11" resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform == 'win32'", "python_full_version >= '3.14' and sys_platform != 'win32'", - "python_full_version < '3.14' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'win32'", + "python_full_version < '3.12' and sys_platform != 'win32'", +] + +[[package]] +name = "accessible-pygments" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" }, +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, ] [[package]] @@ -590,6 +613,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, ] +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + [[package]] name = "executing" version = "2.2.1" @@ -640,6 +672,12 @@ erda = [ ] [package.dev-dependencies] +docs = [ + { name = "furo" }, + { name = "myst-parser" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] notebook = [ { name = "ipykernel" }, { name = "ipywidgets" }, @@ -664,6 +702,11 @@ requires-dist = [ provides-extras = ["erda", "cloud-datasets"] [package.metadata.requires-dev] +docs = [ + { name = "furo", specifier = ">=2025.12.19" }, + { name = "myst-parser", specifier = ">=5.0.0" }, + { name = "sphinx", specifier = ">=9.0.4" }, +] notebook = [ { name = "ipykernel", specifier = ">=7.2.0" }, { name = "ipywidgets", specifier = ">=8.1.8" }, @@ -738,6 +781,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d5/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl", hash = "sha256:d2ceafaad1b3457968ed14efa28798162f1638dbb5d2a6868a2db002a5ee39a4", size = 202595, upload-time = "2026-03-27T19:11:13.595Z" }, ] +[[package]] +name = "furo" +version = "2025.12.19" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "accessible-pygments" }, + { name = "beautifulsoup4" }, + { name = "pygments" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sphinx-basic-ng" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/20/5f5ad4da6a5a27c80f2ed2ee9aee3f9e36c66e56e21c00fde467b2f8f88f/furo-2025.12.19.tar.gz", hash = "sha256:188d1f942037d8b37cd3985b955839fea62baa1730087dc29d157677c857e2a7", size = 1661473, upload-time = "2025-12-19T17:34:40.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/b2/50e9b292b5cac13e9e81272c7171301abc753a60460d21505b606e15cf21/furo-2025.12.19-py3-none-any.whl", hash = "sha256:bb0ead5309f9500130665a26bee87693c41ce4dbdff864dbfb6b0dae4673d24f", size = 339262, upload-time = "2025-12-19T17:34:38.905Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -784,6 +844,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, ] +[[package]] +name = "imagesize" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -1294,6 +1363,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -1444,6 +1525,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, ] +[[package]] +name = "mdit-py-plugins" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + [[package]] name = "mistune" version = "3.2.0" @@ -1462,6 +1564,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, ] +[[package]] +name = "myst-parser" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "jinja2" }, + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "pyyaml" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/fa/7b45eef11b7971f0beb29d27b7bfe0d747d063aa29e170d9edd004733c8a/myst_parser-5.0.0.tar.gz", hash = "sha256:f6f231452c56e8baa662cc352c548158f6a16fcbd6e3800fc594978002b94f3a", size = 98535, upload-time = "2026-01-15T09:08:18.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/ac/686789b9145413f1a61878c407210e41bfdb097976864e0913078b24098c/myst_parser-5.0.0-py3-none-any.whl", hash = "sha256:ab31e516024918296e169139072b81592336f2fef55b8986aa31c9f04b5f7211", size = 84533, upload-time = "2026-01-15T09:08:16.788Z" }, +] + [[package]] name = "nbclient" version = "0.10.4" @@ -2382,6 +2502,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" }, ] +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, +] + [[package]] name = "rpds-py" version = "0.30.0" @@ -2740,6 +2869,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, +] + [[package]] name = "soupsieve" version = "2.8.3" @@ -2749,6 +2887,139 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, ] +[[package]] +name = "sphinx" +version = "9.0.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version < '3.12' and sys_platform != 'win32'", +] +dependencies = [ + { name = "alabaster", marker = "python_full_version < '3.12'" }, + { name = "babel", marker = "python_full_version < '3.12'" }, + { name = "colorama", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version < '3.12'" }, + { name = "imagesize", marker = "python_full_version < '3.12'" }, + { name = "jinja2", marker = "python_full_version < '3.12'" }, + { name = "packaging", marker = "python_full_version < '3.12'" }, + { name = "pygments", marker = "python_full_version < '3.12'" }, + { name = "requests", marker = "python_full_version < '3.12'" }, + { name = "roman-numerals", marker = "python_full_version < '3.12'" }, + { name = "snowballstemmer", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.12'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" }, +] + +[[package]] +name = "sphinx" +version = "9.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'win32'", +] +dependencies = [ + { name = "alabaster", marker = "python_full_version >= '3.12'" }, + { name = "babel", marker = "python_full_version >= '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "docutils", marker = "python_full_version >= '3.12'" }, + { name = "imagesize", marker = "python_full_version >= '3.12'" }, + { name = "jinja2", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "requests", marker = "python_full_version >= '3.12'" }, + { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, +] + +[[package]] +name = "sphinx-basic-ng" +version = "1.0.0b2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/0b/a866924ded68efec7a1759587a4e478aec7559d8165fac8b2ad1c0e774d6/sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", size = 20736, upload-time = "2023-07-08T18:40:54.166Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/dd/018ce05c532a22007ac58d4f45232514cd9d6dd0ee1dc374e309db830983/sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b", size = 22496, upload-time = "2023-07-08T18:40:52.659Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + [[package]] name = "stack-data" version = "0.6.3" From 5442f46812c5fb23fad0f0bc7d69a8d82184f4b4 Mon Sep 17 00:00:00 2001 From: asgersvenning Date: Tue, 28 Apr 2026 16:07:52 +0200 Subject: [PATCH 13/41] Update lockfile and pypi publish action --- .github/workflows/publish.yml | 27 +++++++++++++++++++ uv.lock | 50 +++++++++++++++++------------------ 2 files changed, 52 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..356cce5 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,27 @@ +name: "Publish to PyPi" + +on: + push: + tags: + # Publish on any tag starting with a `v`, e.g., v0.1.0 + - v* + +jobs: + run: + runs-on: ubuntu-latest + environment: + name: pypi + permissions: + id-token: write + contents: read + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Install uv + uses: astral-sh/setup-uv@v8.1.0 + - name: Install Python 3.14 + run: uv python install 3.14 + - name: Build + run: uv build + - name: Publish + run: uv publish \ No newline at end of file diff --git a/uv.lock b/uv.lock index 5184090..00d1c5c 100644 --- a/uv.lock +++ b/uv.lock @@ -177,30 +177,30 @@ css = [ [[package]] name = "boto3" -version = "1.42.96" +version = "1.42.97" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/2d/69fb3acd50bab83fb295c167d33c4b653faeb5fb0f42bfca4d9b69d6fb68/boto3-1.42.96.tar.gz", hash = "sha256:b38a9e4a3fbbee9017252576f1379780d0a5814768676c08df2f539d31fcdd68", size = 113203, upload-time = "2026-04-24T19:47:18.677Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/7d/5c6fa0bb9fd5caf865b9356411793900304328bcd0bc1eda96a32a1368a6/boto3-1.42.97.tar.gz", hash = "sha256:2833dbeda3670ea610ad48dff7d27cdc829dbbfcdfbc6b750b673948e949b6f0", size = 113217, upload-time = "2026-04-27T20:39:17.646Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/9d/b3f617d011c42eb804d993103b8fa9acdce153e181a3042f58bfe33d7cb4/boto3-1.42.96-py3-none-any.whl", hash = "sha256:2f4566da2c209a98bdbfc874d813ef231c84ad24e4f815e9bc91de5f63351a24", size = 140557, upload-time = "2026-04-24T19:47:15.824Z" }, + { url = "https://files.pythonhosted.org/packages/38/43/84c1888139aa1aaf1dc53f8f914e6ec629e5a571fbafdd42fb2d98ac361f/boto3-1.42.97-py3-none-any.whl", hash = "sha256:966e49f0510af9a64057a902b7df53d4348c447de0d3df4cc855dfd85e058fcd", size = 140556, upload-time = "2026-04-27T20:39:15.509Z" }, ] [[package]] name = "botocore" -version = "1.42.96" +version = "1.42.97" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/77/2c333622a1d47cf5bf73cdcab0cb6c92addafbef2ec05f81b9f75687d9e5/botocore-1.42.96.tar.gz", hash = "sha256:75b3b841ffacaa944f645196655a21ca777591dd8911e732bfb6614545af0250", size = 15263344, upload-time = "2026-04-24T19:47:05.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/95/c37edb602948fad2253ffd1bb3dba5b938645bd1845ee4160350136a0f41/botocore-1.42.97.tar.gz", hash = "sha256:5c0bb00e32d16ff6d278cc8c9e10dc3672d9c1d569031635ac3c908a60de8310", size = 15269348, upload-time = "2026-04-27T20:39:05.625Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/56/152c3a859ca1b9d77ed16deac3cf81682013677c68cf5715698781fc81bd/botocore-1.42.96-py3-none-any.whl", hash = "sha256:db2c3e2006628be6fde81a24124a6563c363d6982fb92728837cf174bad9d98a", size = 14945920, upload-time = "2026-04-24T19:47:00.323Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d2/8e025ba1a4e257879af72d06913272311af79673d82fa2581a351b924317/botocore-1.42.97-py3-none-any.whl", hash = "sha256:77d2c8ce1bc592d3fbd7c01c35836f4a5b0cac2ca03ccdf6ffc60faa16b5fadc", size = 14950367, upload-time = "2026-04-27T20:39:01.261Z" }, ] [[package]] @@ -476,7 +476,7 @@ name = "cuda-bindings" version = "13.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder", marker = "sys_platform != 'win32'" }, + { name = "cuda-pathfinder" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/e0/a9/3a8241c6e19483ac1f1dcf5c10238205dcb8a6e9d0d4d4709240dff28ff4/cuda_bindings-13.2.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:721104c603f059780d287969be3d194a18d0cc3b713ed9049065a1107706759d", size = 5730273, upload-time = "2026-03-11T00:12:37.18Z" }, @@ -493,10 +493,10 @@ wheels = [ [[package]] name = "cuda-pathfinder" -version = "1.5.3" +version = "1.5.4" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/d6/ac63065d33dd700fee7ebd7d287332401b54e31b9346e142f871e1f0b116/cuda_pathfinder-1.5.3-py3-none-any.whl", hash = "sha256:dff021123aedbb4117cc7ec81717bbfe198fb4e8b5f1ee57e0e084fec5c8577d", size = 49991, upload-time = "2026-04-14T20:09:27.037Z" }, + { url = "https://files.pythonhosted.org/packages/11/d0/c177e29701cf1d3008d7d2b16b5fc626592ce13bd535f8795c5f57187e0e/cuda_pathfinder-1.5.4-py3-none-any.whl", hash = "sha256:9563d3175ce1828531acf4b94e1c1c7d67208c347ca002493e2654878b26f4b7", size = 51657, upload-time = "2026-04-27T22:42:07.712Z" }, ] [[package]] @@ -509,37 +509,37 @@ wheels = [ [package.optional-dependencies] cublas = [ - { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cublas", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] cudart = [ - { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] cufft = [ - { name = "nvidia-cufft", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cufft", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] cufile = [ { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, ] cupti = [ - { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] curand = [ - { name = "nvidia-curand", marker = "sys_platform == 'linux'" }, + { name = "nvidia-curand", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] cusolver = [ - { name = "nvidia-cusolver", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusolver", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] cusparse = [ - { name = "nvidia-cusparse", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparse", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] nvtx = [ - { name = "nvidia-nvtx", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvtx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" }, ] [[package]] @@ -1803,7 +1803,7 @@ name = "nvidia-cudnn-cu13" version = "9.19.0.56" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas", marker = "sys_platform != 'win32'" }, + { name = "nvidia-cublas" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/f1/84/26025437c1e6b61a707442184fa0c03d083b661adf3a3eecfd6d21677740/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:6ed29ffaee1176c612daf442e4dd6cfeb6a0caa43ddcbeb59da94953030b1be4", size = 433781201, upload-time = "2026-02-03T20:40:53.805Z" }, @@ -1815,7 +1815,7 @@ name = "nvidia-cufft" version = "12.0.0.61" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink", marker = "sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, @@ -1845,9 +1845,9 @@ name = "nvidia-cusolver" version = "12.0.4.66" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas", marker = "sys_platform != 'win32'" }, - { name = "nvidia-cusparse", marker = "sys_platform != 'win32'" }, - { name = "nvidia-nvjitlink", marker = "sys_platform != 'win32'" }, + { name = "nvidia-cublas" }, + { name = "nvidia-cusparse" }, + { name = "nvidia-nvjitlink" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, @@ -1859,7 +1859,7 @@ name = "nvidia-cusparse" version = "12.6.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink", marker = "sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, From 8b2f9eaf8100655536d7d77c9110a5ca9605c6ef Mon Sep 17 00:00:00 2001 From: asgersvenning Date: Tue, 28 Apr 2026 17:06:36 +0200 Subject: [PATCH 14/41] Add ruff to tests dependency group and update action file name + fix README badges --- .github/workflows/{run-tests.yml => ci.yml} | 0 README.md | 92 ++++++++++++++------- pyproject.toml | 1 + uv.lock | 31 ++++++- 4 files changed, 91 insertions(+), 33 deletions(-) rename .github/workflows/{run-tests.yml => ci.yml} (100%) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/ci.yml similarity index 100% rename from .github/workflows/run-tests.yml rename to .github/workflows/ci.yml diff --git a/README.md b/README.md index 2ae1a8a..d768961 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,12 @@ [![](https://img.shields.io/badge/Methods%20in%20Ecology%20and%20Evolution-10.1111/2041%2D%2D210x.70249-B52838?style=flat&logo=data:image/webp;base64,UklGRvgFAABXRUJQVlA4TOsFAAAvt8AtELchEEhi3d9hDYFAkj/nHgsJEib7v8iwbRtHt/+u970aihJ/VwNCFID4AW4IAD4EYgI0frz4EDQOfAA/CmBFIAoCCCBEAEQgxA0BCkIIUQraAoAmkqRm3N3dZ3rcuuf/f+22O0gI9wohov+QIElu2/RBWbROdEkAE6D3n7YYfb4+XJwc7GysLM5BeF/Z2MmTi4fXz9GfWRHvfro9C4zI7VN/N2fS7+HpBgiF57Cb8f99PVsD4Uh/HTXv9+kAKkV2b3n650OoGv7c6OQf5zNQPfq5N2c83AUl4dmb+jTdroai6LfezLXLhVAWPZu44lfzoDDmU/2V8f0yKA2/76q9bIHi6C96fR6D8uifSqe6mwP14dk1Vm/70UR013dvNBPZVfk6gobiqGv6+lqHpiJf1DztGpoL7yr8nESD0VPD99ceNBl7Xt3bJjQam72y4RI0Gzms6hGajseK7qDxyGpuoPm4qXeyudyBCZF1/5qtf2wIZoT0i/YlO6RsxeebYEhsumQTbg9Mib2UazCfgDHRx1Kuw5xwqaUqMCheZJZh1y2SXeJJR2BSHHW5ZsWgEKw8CumUs0DShCBko7Sh8REEgechopSkGuHc+/ZJAs+DC01SjT7mNj5pAs+DCc0xKBTe9TmnCkTo4p8cx4AovEVyHoGliJlISZXorJqOIlAhZip9EoW13njLOn1MdR+qhEbh1C10y4og80UT/6W5CppBRaFQJO1yvoBQv0lPVigKzTvFJdAh9YiQIKdcXyTlcoGO1N7gKhd1uuNuQxCwIUWb6HidsRpyN0BIaNXHlPVLjMkqiSC3Q5Wtf9r9R6TekBU6ObqYIin40KJNeNm5MoMIzc7Le8TNcKGfM0GhUh+VPIOlwksOpQVbaf7o5KU6OjgG+IvImM5NqfBpT2QREUQhIiLU6tMOxIQVcnpqIGOt7KLVG3LrbRTE1MSvbCCGup5MpddJZ1wgDkkbdPJJa2Fu5OTBBWFw9MzMocXyn1OL5T8bFvOJWsPkGGW+2yz/8sFmPvFMo5+4jeFvVyrmhda/tKzxRIBUs6NJo++NKv9yVgLe3EDyICBlGaSs0QSl5X9TSuBu1aMY8DdeltKnzlkBMfoUgTfk0TxwA+a2tGmEBIV9vlrt1VkggtaeJ+VBzRIDETTELwgBfqEbYb7QReETKKafCIuSyfSKSClr1g+sljtCkMyrClY+wnzDGIXqhc9XDAIyVhYl6o3WQISIPteO6beihYdVgi2MMZDkVvPe1mSa9BWrrWzYIyJE+I6gwvxRaDIFdNsKu32R6iCzR7M8EYZ/rw3a1S/aMJ0uA5aXiP5QZbmWm0fF5VrC+hTWXif+ag6Q8vqpZL1NOct6622EfY6EBL6aAp197CyDlDWevrTR36yO9ZL8LIMy+9D0peVfbofJkXavv7Z7e4Hd22fs3h5m9vZHu7f32r193e79GRj7j9CqZnrdS69aSXsSo3V1vf1HOPvrkJpCjLZOg5yzfxSz6QloHu15FdsfLaLm8WaMJnJVI4H9/yDCIkna35K56iCA/QLE2UD4Tql/u8vs3xoRTNEiL+1PzFd8ozUV0q59coH9t8Mozt1fHnnnOHTGiIu6z+4jgeMTIkKrqOocOx6EXvSgFB3Hg+Qup1kkrfh/VMrxfm04RZuofZn48WWsYoo+JhzPRyoW6aTjJzlXDNGddLwqp9SaTKWkHR/MKrpEzdO8yxyPLWE6j3Yk9fh3orCEU3udvucovlzTXKx/BxmDlvSxWH8aAZWXD/V54fRfQloeN0Tn9RdjGf/kddJYa71NG5LbH1Kd9WRt6GNu/1NV1ks2wgW68zQq0u7+1czuz87s/gPN7q/R7v4xze6P1Oz+X83ub9fs/o3N7k/a7P67ze4v3ez+6c0eD8Ds8RfMHu/C7PFFzB7Pxezxc8wer8jq8aHMHo/L7PHPrB5vzuzx/aweT9Hq8SvNHi/U6vFZrR4P1+rxh60e79no8bWtHs/c6PHjjR6v3+bfR/A//3sUzP79DwkA&logoColor=white&labelColor=gray)](https://doi.org/10.1111/2041-210x.70249) [![](https://img.shields.io/badge/Zenodo-10.5281/zenodo.14761446-0377cd?style=flat&logo=doi&logoColor=white&labelColor=gray)](https://doi.org/10.5281/zenodo.14761446) +[![PyPI version](https://img.shields.io/pypi/v/flat-bug.svg)](https://pypi.org/project/flat-bug/) +[![Python Versions](https://img.shields.io/pypi/pyversions/flat-bug.svg)](https://pypi.org/project/flat-bug/) +[![CI Status](https://github.com/darsa-group/flat-bug/actions/workflows/ci.yaml/badge.svg)](https://github.com/darsa-group/flat-bug/actions) +[![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +
@@ -16,58 +22,80 @@
Find and cite the flatbug paper in Method in Ecology and Evolution
+--- + `flatbug` is partly a high-performance pyramid tiling inference wrapper for [`YOLOv8`](https://github.com/ultralytics/ultralytics) and partly a hybrid instance segmentation dataset of terrestrial arthropods accompanied by an appropriate training schedule for `YOLOv8` segmentation models, built on top of the original [`YOLOv8` training schedule](https://docs.ultralytics.com/modes/train/#why-choose-ultralytics-yolo-for-training). The goal of `flatbug` is to provide a single unified model for detection and segmentation of all terrestrial arthropods on arbitrarily large images, especially fine-tuned for the case of top-down images/scans - thus the name `"flat"bug`. -### Installation -Installation via package managers coming later. - -#### Source/development + +### Source/development + Or a development version can be installed from source by cloning this repository: -```sh + +```bash +# Clone repository git clone https://github.com/darsa-group/flat-bug.git cd flat-bug +# Install +uv sync --all-extras --all-groups --upgrade +# or (not recommended) pip install -e . ``` -However, as with other packages built with `PyTorch` it is best to ensure that `torch` is installed separately. See [https://pytorch.org/](https://pytorch.org/) for details. We recommend using `torch>=2.3`. +However, as with other packages built with `PyTorch` it is best to ensure that `torch` is installed separately. See [https://pytorch.org/](https://pytorch.org/get-started/locally) for details. We recommend using `torch>=2.3`. + +--- + +## CLI Usage -### CLI Usage We provide a number of [CLI scripts](https://darsa.info/flat-bug/cli.html) with `flatbug`. The main one of interest is `fb_predict`, which can be used to run inference on images or videos: -```sh -fb_predict -i -o [-w ] ... + +```bash +[uv] fb_predict -i -o [-w ] ... ``` -### Tutorials +## Tutorials + We provide a number of tutorials on general and advanced usage, training, deployment and hyperparameters of `flatbug` in [examples/tutorials](examples/tutorials) or with Google Colab [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/darsa-group/flat-bug/blob/master/docs/flat-bug.ipynb). -### Documentation +## Documentation + Find our documentation at [https://darsa.info/flat-bug/](https://darsa.info/flat-bug/). - - +--> \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 127baf6..13548e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,6 +60,7 @@ notebook = [ ] tests = [ "pytest>=9.0.3", + "ruff>=0.15.12", ] [tool.ruff] diff --git a/uv.lock b/uv.lock index 00d1c5c..741123c 100644 --- a/uv.lock +++ b/uv.lock @@ -685,6 +685,7 @@ notebook = [ ] tests = [ { name = "pytest" }, + { name = "ruff" }, ] [package.metadata] @@ -712,7 +713,10 @@ notebook = [ { name = "ipywidgets", specifier = ">=8.1.8" }, { name = "jupyter", specifier = ">=1.0.0" }, ] -tests = [{ name = "pytest", specifier = ">=9.0.3" }] +tests = [ + { name = "pytest", specifier = ">=9.0.3" }, + { name = "ruff", specifier = ">=0.15.12" }, +] [[package]] name = "fonttools" @@ -2619,6 +2623,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, ] +[[package]] +name = "ruff" +version = "0.15.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" }, + { url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" }, + { url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" }, + { url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" }, + { url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" }, + { url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" }, + { url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" }, + { url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" }, + { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" }, +] + [[package]] name = "s3transfer" version = "0.16.1" From 60c8431a79392e3ab128f0fbce163fb727eb09cf Mon Sep 17 00:00:00 2001 From: asgersvenning Date: Tue, 28 Apr 2026 17:18:41 +0200 Subject: [PATCH 15/41] Fix autofixable problems and update ruff rules --- pyproject.toml | 9 +++++++++ src/flat_bug/geometric.py | 9 +++++++-- tests/conftest.py | 7 +++---- tests/test_config.py | 1 + tests/test_dataset.py | 3 +-- 5 files changed, 21 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 13548e2..64a0fbe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,6 +80,15 @@ extend-exclude = [ select = ["E", "F", "UP", "I", "D"] ignore = ["D105"] +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.lint.pydoclint] +ignore-one-line-docstrings = true + +[tool.ruff.lint.extend-per-file-ignores] +"tests/*" = ["D100"] + [tool.pytest.ini_options] filterwarnings = [ "ignore:.*The NVIDIA driver on your system is too old.*:UserWarning", diff --git a/src/flat_bug/geometric.py b/src/flat_bug/geometric.py index 1ac1011..0eb6d83 100644 --- a/src/flat_bug/geometric.py +++ b/src/flat_bug/geometric.py @@ -2,7 +2,7 @@ import math from collections.abc import Sequence from itertools import accumulate -from typing import Literal, TypeVar, overload, Never +from typing import Literal, Never, TypeVar, overload import cv2 import numpy as np @@ -218,6 +218,7 @@ def contours_to_masks( Returns: NxHxW tensor or array of boolean masks with the contours filled in. + """ N = len(contours) @@ -276,10 +277,13 @@ def poly_area(poly : torch.Tensor | np.ndarray) -> float: See https://en.wikipedia.org/wiki/Shoelace_formula#Shoelace_formula for details. Args: - poly: A tensor or array of shape (n, 2), where n is the number of vertices and the 2 columns are the x and y coordinates of the vertices. + poly: A tensor or array of shape (n, 2), + where n is the number of vertices + and the 2 columns are the x and y coordinates of the vertices. Returns: The area of the polygon + """ if isinstance(poly, torch.Tensor): return float(_poly_area_tensor(poly)) @@ -308,6 +312,7 @@ def poly_normals(polygon : V) -> V: Returns: A tensor of shape (n, 2), where n is the number of vertices and the 2 columns are the x and y coordinates of the normals. + """ if isinstance(polygon, torch.Tensor): return _poly_normals_tensor(polygon) diff --git a/tests/conftest.py b/tests/conftest.py index 6c61548..5ccb4b6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,11 +1,10 @@ # tests/conftest.py -import subprocess import os +import subprocess + def pytest_sessionfinish(session, exitstatus): - """ - This hook runs after all tests have completed. - """ + """This hook runs after all tests have completed.""" # noqa: D401, D404 script_path = os.path.join(os.path.dirname(__file__), "restore_assets.py") print(f"\n\nRunning Post-Test Cleanup:\n\t{script_path}\n") diff --git a/tests/test_config.py b/tests/test_config.py index 700df90..d44e356 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -2,6 +2,7 @@ import copy import os import tempfile + import pytest from flat_bug.config import DEFAULT_CFG, check_cfg_types, check_types, get_type_def, read_cfg, write_cfg diff --git a/tests/test_dataset.py b/tests/test_dataset.py index 99243d6..455eff6 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -3,9 +3,9 @@ import os import tempfile from copy import deepcopy -import pytest import numpy as np +import pytest from ultralytics.data import build_dataloader from ultralytics.data.utils import verify_image_label from ultralytics.utils import DEFAULT_CFG, IterableSimpleNamespace @@ -16,7 +16,6 @@ from flat_bug.datasets import FlatBugYOLODataset, FlatBugYOLOValidationDataset from tests.remote_lfs_fallback import check_file_with_remote_fallback - TEST_DIR = os.path.dirname(__file__) ASSET_DIR = os.path.join(TEST_DIR, "assets") ASSET_NAME = "ALUS_Non-miteArachnids_Unknown_2020_11_03_4545" From 0d71bd71dd3ae715b83bb964e1f018c539ba8cab Mon Sep 17 00:00:00 2001 From: asgersvenning Date: Tue, 28 Apr 2026 17:34:07 +0200 Subject: [PATCH 16/41] Rename actions and fix README badge --- .github/workflows/ci.yml | 2 +- .github/workflows/{build_docs.yml => docs.yml} | 0 .github/workflows/publish.yml | 2 +- README.md | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename .github/workflows/{build_docs.yml => docs.yml} (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 579afce..f2e6025 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Flat-bug Unit Tests +name: Tests run-name: ${{ github.ref }} is running unit tests on: push: diff --git a/.github/workflows/build_docs.yml b/.github/workflows/docs.yml similarity index 100% rename from .github/workflows/build_docs.yml rename to .github/workflows/docs.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 356cce5..6242acc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,4 +1,4 @@ -name: "Publish to PyPi" +name: Publish to PyPi on: push: diff --git a/README.md b/README.md index d768961..bc56ea5 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ [![PyPI version](https://img.shields.io/pypi/v/flat-bug.svg)](https://pypi.org/project/flat-bug/) [![Python Versions](https://img.shields.io/pypi/pyversions/flat-bug.svg)](https://pypi.org/project/flat-bug/) -[![CI Status](https://github.com/darsa-group/flat-bug/actions/workflows/ci.yaml/badge.svg)](https://github.com/darsa-group/flat-bug/actions) +[![CI Status](https://github.com/darsa-group/flat-bug/actions/workflows/ci.yml/badge.svg)](https://github.com/darsa-group/flat-bug/actions/workflows/ci.yml) [![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) From 1c0bb06cff13dd22d0517f1c486f1718c4b49641 Mon Sep 17 00:00:00 2001 From: asgersvenning Date: Tue, 28 Apr 2026 17:49:11 +0200 Subject: [PATCH 17/41] Clean up publication action --- .github/workflows/publish.yml | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6242acc..771e141 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,27 +1,29 @@ -name: Publish to PyPi +name: Publish to PyPI on: push: tags: - # Publish on any tag starting with a `v`, e.g., v0.1.0 - - v* + - "v*" jobs: - run: + publish: runs-on: ubuntu-latest - environment: - name: pypi + environment: pypi permissions: id-token: write contents: read + steps: - name: Checkout uses: actions/checkout@v6 - - name: Install uv + + - name: Install uv and set Python version uses: astral-sh/setup-uv@v8.1.0 - - name: Install Python 3.14 - run: uv python install 3.14 - - name: Build + with: + python-version: "3.14" + + - name: Build distributions run: uv build - - name: Publish + + - name: Publish to PyPI run: uv publish \ No newline at end of file From d1f498b3917fa2f5516b2db17afc431f83c014ba Mon Sep 17 00:00:00 2001 From: asgersvenning Date: Tue, 28 Apr 2026 18:03:05 +0200 Subject: [PATCH 18/41] Fix wrong command in README - CLI Usage --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bc56ea5..f308364 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ However, as with other packages built with `PyTorch` it is best to ensure that ` We provide a number of [CLI scripts](https://darsa.info/flat-bug/cli.html) with `flatbug`. The main one of interest is `fb_predict`, which can be used to run inference on images or videos: ```bash -[uv] fb_predict -i -o [-w ] ... +[uv run] fb_predict -i -o [-w ] ... ``` ## Tutorials From 780c0b57339c08023127ae486ae47424b2ffeed7 Mon Sep 17 00:00:00 2001 From: asgersvenning Date: Tue, 28 Apr 2026 19:35:07 +0200 Subject: [PATCH 19/41] Bumb minor version 1.1.1: Move CLI tools to submodule and improve README for CPU/CUDA compatible installation via uv/pip/source --- .github/workflows/ci.yml | 2 + README.md | 90 ++++++++++++++++++-- pyproject.toml | 16 ++-- src/{bin => flat_bug/cli}/fb_clone_data.py | 0 src/{bin => flat_bug/cli}/fb_eval.py | 0 src/{bin => flat_bug/cli}/fb_predict.py | 2 + src/{bin => flat_bug/cli}/fb_prepare_data.py | 0 src/{bin => flat_bug/cli}/fb_train.py | 0 src/{bin => flat_bug/cli}/fb_tune.py | 0 src/flat_bug/geometric.py | 6 +- src/flat_bug/nms.py | 6 +- uv.lock | 2 +- 12 files changed, 101 insertions(+), 23 deletions(-) rename src/{bin => flat_bug/cli}/fb_clone_data.py (100%) rename src/{bin => flat_bug/cli}/fb_eval.py (100%) rename src/{bin => flat_bug/cli}/fb_predict.py (99%) rename src/{bin => flat_bug/cli}/fb_prepare_data.py (100%) rename src/{bin => flat_bug/cli}/fb_train.py (100%) rename src/{bin => flat_bug/cli}/fb_tune.py (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2e6025..b2b6416 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,8 @@ concurrency: jobs: Continous-Testing: runs-on: ubuntu-latest + env: + UV_TORCH_BACKEND: cpu strategy: matrix: diff --git a/README.md b/README.md index f308364..7b920ca 100644 --- a/README.md +++ b/README.md @@ -35,12 +35,20 @@ The goal of `flatbug` is to provide a single unified model for detection and seg We recommend using `uv` ([*installation*](https://docs.astral.sh/uv/getting-started/installation/)): ```bash +# Easy-install +uv pip install flat-bug --torch-backend=auto # Add to a project permanently (recommended) uv add flat-bug -# install temporarily in a venv/project -uv pip install flat-bug ``` +> [!TIP] +> If you have problems with PyTorch not being installed with CUDA enabled try: +> ```bash +> uv pip install torch torchvision --torch-backend=auto --reinstall +> ``` +> More details: +> https://docs.astral.sh/uv/guides/integration/pytorch/#the-uv-pip-interface + or *(not recommended)*: ```bash @@ -51,17 +59,29 @@ pip install flat-bug Or a development version can be installed from source by cloning this repository: +#### Clone the repository + ```bash -# Clone repository git clone https://github.com/darsa-group/flat-bug.git cd flat-bug -# Install -uv sync --all-extras --all-groups --upgrade -# or (not recommended) +``` + +#### Install `flatbug` + +```bash +uv sync --all-extras --all-groups --upgrade +# (optional but recommended) +uv pip install torch torchvision --torch-backend=auto --reinstall +``` + +or *(not recommended)*: + +```bash pip install -e . ``` -However, as with other packages built with `PyTorch` it is best to ensure that `torch` is installed separately. See [https://pytorch.org/](https://pytorch.org/get-started/locally) for details. We recommend using `torch>=2.3`. +> [!WARNING] +> If you do decide to install with `pip`, as with other packages built with `PyTorch` it is best to ensure that `torch` is installed separately. See [https://pytorch.org/](https://pytorch.org/get-started/locally) for details. We recommend using `torch>=2.3`. --- @@ -83,6 +103,62 @@ Find our documentation at [https://darsa.info/flat-bug/](https://darsa.info/flat --- +## CUDA Issues + +Working with cross-platform PyTorch code can be a bit confusing, so if you ever get stuck with some CUDA errors, here are some possible paths to resolve the issues. + +### `uv` and `pip` + +If you installed `flat-bug` via a package manager but find that GPU acceleration is not working, your environment likely downloaded the default PyPI wheels which may not match your system's NVIDIA drivers. + +**If you are using `uv`**, the easiest fix is to force a re-resolution of the PyTorch backend: + +```bash +# Automatically detect hardware and reinstall PyTorch +uv pip install torch torchvision --torch-backend=auto --reinstall + +# OR manually force a specific CUDA version (e.g., CUDA 11.8) +uv pip install torch torchvision --torch-backend=cu118 --reinstall +``` + +**If you are using standard `pip`**, you must manually point to the PyTorch index that matches your system: + +```bash +# Uninstall the broken versions +pip uninstall torch torchvision + +# Reinstall pointing explicitly to the CUDA 11.8 or 12.1 (cu121) index +pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 +``` + +**Verification:** + +```bash +[uv run] python -c "import torch; print(f'CUDA Available: {torch.cuda.is_available()}')" +``` + + +### Source + +Rebuild the environment and lockfile from scratch: + +```bash +# cd ~/flat-bug + +# 1. Purge old state +rm uv.lock +rm -rf .venv + +# 2. Generate the pure, cross-platform lockfile +uv lock + +# 3. Create your local environment +uv sync --all-extras --all-groups + +# 4. Patch your local environment with your specific hardware backend +uv pip install torch torchvision --torch-backend=auto --reinstall +``` +