From f8489d2f8bbf7ea433cc0dc886242bf76a283b4d Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Tue, 30 Jun 2026 14:59:54 +0300 Subject: [PATCH 01/53] refactor(exchange-oracle): split job_creation into per-task package Break the 3k-line src/handlers/job_creation.py into a package with one file per task builder: - builders/vision/{base,basic,boxes_from_points,skeletons_from_boxes}.py - builders/audio/transcription.py (stub, raises NotImplementedError) - factory.py (create_task), exceptions.py, utils.py __init__.py exports only create_task. Move DM_DATASET_FORMAT_MAPPING / DM_GT_DATASET_FORMAT_MAPPING to a new src/core/tasks/cvat_formats.py and repoint job_export.py and the webhook test's mock-patch paths to the new module locations. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/core/tasks/cvat_formats.py | 22 + .../src/handlers/job_creation.py | 3060 ----------------- .../src/handlers/job_creation/__init__.py | 3 + .../job_creation/builders/__init__.py | 0 .../job_creation/builders/audio/__init__.py | 0 .../builders/audio/transcription.py | 14 + .../job_creation/builders/vision/__init__.py | 0 .../job_creation/builders/vision/base.py | 155 + .../job_creation/builders/vision/basic.py | 327 ++ .../builders/vision/boxes_from_points.py | 1142 ++++++ .../builders/vision/skeletons_from_boxes.py | 1404 ++++++++ .../src/handlers/job_creation/exceptions.py | 29 + .../src/handlers/job_creation/factory.py | 44 + .../src/handlers/job_creation/utils.py | 102 + .../src/handlers/job_export.py | 2 +- .../test_process_job_launcher_webhooks.py | 18 +- 16 files changed, 3254 insertions(+), 3068 deletions(-) create mode 100644 packages/examples/cvat/exchange-oracle/src/core/tasks/cvat_formats.py delete mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_creation.py create mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_creation/__init__.py create mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/__init__.py create mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/__init__.py create mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py create mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/__init__.py create mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/base.py create mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/basic.py create mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/boxes_from_points.py create mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/skeletons_from_boxes.py create mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_creation/exceptions.py create mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_creation/factory.py create mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py diff --git a/packages/examples/cvat/exchange-oracle/src/core/tasks/cvat_formats.py b/packages/examples/cvat/exchange-oracle/src/core/tasks/cvat_formats.py new file mode 100644 index 0000000000..2c102a0e5d --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/core/tasks/cvat_formats.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from src.core.types import TaskTypes + +DM_DATASET_FORMAT_MAPPING = { + TaskTypes.image_label_binary: "cvat_images", + TaskTypes.image_points: "coco_person_keypoints", + TaskTypes.image_boxes: "coco_instances", + TaskTypes.image_polygons: "coco_instances", + TaskTypes.image_boxes_from_points: "coco_instances", + TaskTypes.image_skeletons_from_boxes: "coco_person_keypoints", +} + +DM_GT_DATASET_FORMAT_MAPPING = { + # GT uses the same format both for boxes and points + TaskTypes.image_label_binary: "cvat_images", + TaskTypes.image_points: "coco_instances", + TaskTypes.image_boxes: "coco_instances", + TaskTypes.image_polygons: "coco_instances", + TaskTypes.image_boxes_from_points: "coco_instances", + TaskTypes.image_skeletons_from_boxes: "coco_person_keypoints", +} diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation.py deleted file mode 100644 index 666f71e2a8..0000000000 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation.py +++ /dev/null @@ -1,3060 +0,0 @@ -from __future__ import annotations - -import math -import os -import random -import uuid -from abc import ABCMeta, abstractmethod -from concurrent.futures import Future, ThreadPoolExecutor -from contextlib import ExitStack -from dataclasses import dataclass, field -from itertools import chain, groupby -from math import ceil -from pathlib import Path -from queue import Queue -from tempfile import TemporaryDirectory -from time import sleep -from typing import TYPE_CHECKING, TypeVar, cast - -import cv2 -import datumaro as dm -import numpy as np -from datumaro.util import filter_dict, take_by -from datumaro.util.annotation_util import BboxCoords, bbox_iou, find_instances -from datumaro.util.image import IMAGE_EXTENSIONS, decode_image, encode_image - -import src.core.tasks.boxes_from_points as boxes_from_points_task -import src.core.tasks.points as points_task -import src.core.tasks.simple as simple_task -import src.core.tasks.skeletons_from_boxes as skeletons_from_boxes_task -import src.cvat.api_calls as cvat_api -import src.services.cloud as cloud_service -import src.services.cvat as db_service -from src.chain.escrow import get_escrow_manifest -from src.core.config import Config -from src.core.storage import compose_data_bucket_filename, compose_data_bucket_prefix -from src.core.types import TaskStatuses, TaskTypes -from src.db import SessionLocal -from src.log import ROOT_LOGGER_NAME -from src.models.cvat import Project -from src.services.cloud import CloudProviders, StorageClient -from src.services.cloud.utils import BucketAccessInfo -from src.utils.annotations import InstanceSegmentsToBbox, ProjectLabels, is_point_in_bbox -from src.utils.assignments import parse_manifest -from src.utils.logging import NullLogger, format_sequence, get_function_logger -from src.utils.roi_uploader import BufferedRoiImageUploader -from src.utils.zip_archive import write_dir_to_zip_archive - -if TYPE_CHECKING: - from collections.abc import Generator, Sequence - from logging import Logger - - from src.core.manifest import TaskManifest - -module_logger = f"{ROOT_LOGGER_NAME}.cron.cvat" - -LABEL_TYPE_MAPPING = { - TaskTypes.image_label_binary: cvat_api.LabelType.tag, - TaskTypes.image_points: cvat_api.LabelType.points, - TaskTypes.image_boxes: cvat_api.LabelType.rectangle, - TaskTypes.image_polygons: cvat_api.LabelType.polygon, - TaskTypes.image_boxes_from_points: cvat_api.LabelType.rectangle, - TaskTypes.image_skeletons_from_boxes: cvat_api.LabelType.points, -} - -DM_DATASET_FORMAT_MAPPING = { - TaskTypes.image_label_binary: "cvat_images", - TaskTypes.image_points: "coco_person_keypoints", - TaskTypes.image_boxes: "coco_instances", - TaskTypes.image_polygons: "coco_instances", - TaskTypes.image_boxes_from_points: "coco_instances", - TaskTypes.image_skeletons_from_boxes: "coco_person_keypoints", -} - -DM_GT_DATASET_FORMAT_MAPPING = { - # GT uses the same format both for boxes and points - TaskTypes.image_label_binary: "cvat_images", - TaskTypes.image_points: "coco_instances", - TaskTypes.image_boxes: "coco_instances", - TaskTypes.image_polygons: "coco_instances", - TaskTypes.image_boxes_from_points: "coco_instances", - TaskTypes.image_skeletons_from_boxes: "coco_person_keypoints", -} - - -class DatasetValidationError(Exception): - pass - - -class MismatchingAnnotations(DatasetValidationError): - pass - - -class TooFewSamples(DatasetValidationError): - pass - - -class InvalidCategories(DatasetValidationError): - pass - - -class InvalidImageInfo(DatasetValidationError): - pass - - -class InvalidCoordinates(DatasetValidationError): - pass - - -class InvisibleSkeletonError(DatasetValidationError): - pass - - -T = TypeVar("T") - - -class _Undefined: - def __bool__(self) -> bool: - return False - - -_unset = _Undefined() - -_MaybeUnset = T | _Undefined - - -@dataclass -class _ExcludedAnnotationInfo: - message: str - sample_id: str = field(kw_only=True) - sample_subset: str = field(kw_only=True) - - -@dataclass -class _ExcludedAnnotationsInfo: - messages: list[_ExcludedAnnotationInfo] = field(default_factory=list) - - excluded_count: int = 0 - "The number of excluded annotations. Can be different from len(messages)" - - total_count: int = 0 - - def add_message(self, message: str, *, sample_id: str, sample_subset: str): - self.messages.append( - _ExcludedAnnotationInfo( - message=message, sample_id=sample_id, sample_subset=sample_subset - ) - ) - - -class _TaskBuilderBase(metaclass=ABCMeta): - def __init__(self, manifest: TaskManifest, escrow_address: str, chain_id: int) -> None: - self.exit_stack = ExitStack() - self.manifest = manifest - self.escrow_address = escrow_address - self.chain_id = chain_id - - self.logger: Logger = NullLogger() - - self._oracle_data_bucket = BucketAccessInfo.parse_obj(Config.storage_config) - - @property - def _task_segment_size(self) -> int: - return self.manifest.annotation.job_size - - @property - def _job_val_frames_count(self) -> int: - return self.manifest.validation.val_size - - def __enter__(self): - return self - - def __exit__(self, *args, **kwargs): - self.close() - - def close(self): - self.exit_stack.close() - - def set_logger(self, logger: Logger): - # TODO: add escrow info into messages - self.logger = logger - return self - - @classmethod - def _make_cloud_storage_client(cls, bucket_info: BucketAccessInfo) -> StorageClient: - extra_args = {} - - if bucket_info.provider == CloudProviders.aws: - import boto3.session - - extra_args["config"] = boto3.session.Config( - max_pool_connections=Config.features.max_data_storage_connections - ) - elif bucket_info.provider == CloudProviders.gcs: - pass # TODO: test and add connections if needed - - return cloud_service.make_client(bucket_info, **extra_args) - - def _save_cvat_gt_dataset_to_oracle_bucket( # noqa: B027 - self, - gt_dataset_path: Path, - *, - file_suffix: str = "", - ) -> None: - # method saves gt annotations that will be uploaded to CVAT - # into oracle bucket - pass - - def _wait_task_creation(self, task_id: int) -> cvat_api.RequestStatus: - # TODO: add a timeout or - # save gt datasets in the oracle bucket and upload in track_task_creation() - while True: - task_status, _ = cvat_api.get_task_upload_status(task_id) - if task_status not in [cvat_api.RequestStatus.STARTED, cvat_api.RequestStatus.QUEUED]: - return task_status - - sleep(Config.cvat_config.task_creation_check_interval) - - def _setup_gt_job_for_cvat_task( - self, task_id: int, gt_dataset: dm.Dataset, *, dm_export_format: str = "coco" - ) -> None: - task_status = self._wait_task_creation(task_id) - if task_status != cvat_api.RequestStatus.FINISHED: - return # will be handled in state_trackers.py::track_task_creation - - dm_format_to_cvat_format = { - "coco": "COCO 1.0", - "cvat": "CVAT 1.1", - "datumaro": "Datumaro 1.0", - } - - with TemporaryDirectory() as tmp_dir: - export_dir = Path(tmp_dir) / "export" - gt_dataset.export(save_dir=str(export_dir), save_media=False, format=dm_export_format) - - annotations_archive_path = Path(tmp_dir) / "annotations.zip" - with annotations_archive_path.open("wb") as annotations_archive: - write_dir_to_zip_archive(export_dir, annotations_archive) - - if Config.is_development_mode(): - self._save_cvat_gt_dataset_to_oracle_bucket( - gt_dataset_path=annotations_archive_path, file_suffix=f"for_task_{task_id}" - ) - - self._setup_gt_job( - task_id, - annotations_archive_path, - format_name=dm_format_to_cvat_format[dm_export_format], - ) - - def _setup_gt_job(self, task_id: int, dataset_path: Path, format_name: str) -> None: - gt_job = cvat_api.get_gt_job(task_id) - cvat_api.upload_gt_annotations(gt_job.id, dataset_path, format_name=format_name) - cvat_api.finish_gt_job(gt_job.id) - - def _setup_quality_settings(self, task_id: int, **overrides) -> None: - settings = cvat_api.get_quality_control_settings(task_id) - - values = { - "target_metric_threshold": self.manifest.validation.min_quality, - "empty_is_annotated": True, - } - values.update(**overrides) - cvat_api.update_quality_control_settings(settings.id, **values) - - def _split_dataset_per_task( - self, - data_filenames: list[str], - *, - subset_size: int, - ) -> Generator[str]: - random.shuffle(data_filenames) - yield from take_by(data_filenames, subset_size) - - @abstractmethod - def build(self) -> None: ... - - -class SimpleTaskBuilder(_TaskBuilderBase): - """ - Handles task creation for the IMAGE_BOXES task type - """ - - def _upload_task_meta(self, gt_dataset: dm.Dataset): - layout = simple_task.TaskMetaLayout() - serializer = simple_task.TaskMetaSerializer() - - file_list = [] - file_list.append( - ( - serializer.serialize_gt_annotations(gt_dataset), - layout.GT_FILENAME, - ) - ) - - storage_client = self._make_cloud_storage_client(self._oracle_data_bucket) - for file_data, filename in file_list: - storage_client.create_file( - compose_data_bucket_filename(self.escrow_address, self.chain_id, filename), - file_data, - ) - - def _parse_gt_dataset( - self, gt_file_data: bytes, *, add_prefix: str | None = None - ) -> dm.Dataset: - with TemporaryDirectory() as gt_temp_dir: - gt_filename = os.path.join(gt_temp_dir, "gt_annotations.json") - with open(gt_filename, "wb") as f: - f.write(gt_file_data) - - gt_dataset = dm.Dataset.import_from( - gt_filename, - format=DM_GT_DATASET_FORMAT_MAPPING[self.manifest.annotation.type], - ) - - if add_prefix: - gt_dataset = dm.Dataset.from_iterable( - [s.wrap(id=os.path.join(add_prefix, s.id)) for s in gt_dataset], - categories=gt_dataset.categories(), - media_type=gt_dataset.media_type(), - ) - - gt_dataset.init_cache() - - return gt_dataset - - def _get_gt_filenames( - self, gt_dataset: dm.Dataset, data_filenames: list[str], *, manifest: TaskManifest - ) -> list[str]: - gt_filenames = set(s.id + s.media.ext for s in gt_dataset) - known_data_filenames = set(data_filenames) - matched_gt_filenames = gt_filenames.intersection(known_data_filenames) - - if len(gt_filenames) != len(matched_gt_filenames): - missing_gt = gt_filenames - matched_gt_filenames - raise DatasetValidationError( - "Failed to find several validation samples in the dataset files: {}".format( - format_sequence(list(missing_gt)) - ) - ) - - if len(gt_filenames) < manifest.validation.val_size: - raise TooFewSamples( - f"Too few validation samples provided ({len(gt_filenames)}), " - f"at least {manifest.validation.val_size} required." - ) - - return list(matched_gt_filenames) - - def build(self): - manifest = self.manifest - escrow_address = self.escrow_address - chain_id = self.chain_id - - data_bucket = BucketAccessInfo.parse_obj(manifest.data.data_url) - gt_bucket = BucketAccessInfo.parse_obj(manifest.validation.gt_url) - - data_bucket_client = cloud_service.make_client(data_bucket) - gt_bucket_client = cloud_service.make_client(gt_bucket) - - # Task configuration creation - data_filenames = data_bucket_client.list_files(prefix=data_bucket.path) - data_filenames = filter_image_files(data_filenames) - - gt_file_data = gt_bucket_client.download_file(gt_bucket.path) - - # Validate and parse GT - gt_dataset = self._parse_gt_dataset(gt_file_data, add_prefix=data_bucket.path) - - # Create task configuration - gt_filenames = self._get_gt_filenames(gt_dataset, data_filenames, manifest=manifest) - data_to_be_annotated = [f for f in data_filenames if f not in set(gt_filenames)] - label_configuration = make_label_configuration(manifest) - - self._upload_task_meta(gt_dataset) - - # Register cloud storage on CVAT to pass user dataset - cloud_storage = cvat_api.create_cloudstorage(**_make_cvat_cloud_storage_params(data_bucket)) - - # Create a project - cvat_project = cvat_api.create_project( - escrow_address, - labels=label_configuration, - user_guide=manifest.annotation.user_guide, - ) - - # Setup webhooks for the project - cvat_webhook = cvat_api.create_cvat_webhook(cvat_project.id) - - with SessionLocal.begin() as session: - segment_size = self._task_segment_size - total_jobs = math.ceil(len(data_to_be_annotated) / segment_size) - - self.logger.info( - "Task creation for escrow '%s': will create %s assignments", - escrow_address, - total_jobs, - ) - db_service.create_escrow_creation( - session, escrow_address=escrow_address, chain_id=chain_id, total_jobs=total_jobs - ) - - project_id = db_service.create_project( - session, - cvat_project.id, - cloud_storage.id, - manifest.annotation.type, - escrow_address, - chain_id, - data_bucket.to_url(), - cvat_webhook_id=cvat_webhook.id, - ) - - db_service.get_project_by_id(session, project_id, for_update=True) # lock the row - db_service.add_project_images(session, cvat_project.id, data_filenames) - - for data_subset in self._split_dataset_per_task( - data_to_be_annotated, - subset_size=Config.cvat_config.max_jobs_per_task * segment_size, - ): - cvat_task = cvat_api.create_task( - cvat_project.id, escrow_address, segment_size=segment_size - ) - task_id = db_service.create_task( - session, cvat_task.id, cvat_project.id, TaskStatuses[cvat_task.status] - ) - db_service.get_task_by_id(session, task_id, for_update=True) # lock the row - - # The task is fully created once 'update:task' webhook is received. - cvat_api.put_task_data( - cvat_task.id, - cloud_storage.id, - filenames=data_subset, - validation_params={ - "gt_filenames": gt_filenames, # include whole GT dataset into each task - "gt_frames_per_job_count": self._job_val_frames_count, - }, - ) - - self._setup_gt_job_for_cvat_task(cvat_task.id, gt_dataset) - self._setup_quality_settings(cvat_task.id) - - db_service.create_data_upload(session, cvat_task.id) - db_service.touch(session, Project, [project_id]) - - -class PointsTaskBuilder(SimpleTaskBuilder): - def __init__(self, manifest: TaskManifest, escrow_address: str, chain_id: int) -> None: - super().__init__(manifest=manifest, escrow_address=escrow_address, chain_id=chain_id) - - self._mean_gt_bbox_radius_estimation: _MaybeUnset[float] = _unset - - def _parse_gt_dataset(self, gt_file_data, *, add_prefix=None): - gt_dataset = super()._parse_gt_dataset(gt_file_data, add_prefix=add_prefix) - - assert len(gt_dataset.categories()[dm.AnnotationType.label]) == 1 - label_id = 0 - - updated_gt_dataset = dm.Dataset(categories=gt_dataset.categories(), media_type=dm.Image) - - # Replace boxes with their center points - # Collect radius statistics - radiuses = [] - for gt_sample in gt_dataset: - image_size = gt_sample.media_as(dm.Image).size - image_half_diag = ((image_size[0] ** 2 + image_size[1] ** 2) ** 0.5) / 2 - - sample_points = [] - for gt_bbox in gt_sample.annotations: - if not isinstance(gt_bbox, dm.Bbox): - continue - - x, y, w, h = gt_bbox.get_bbox() - bbox_center = dm.Points([x + w / 2, y + h / 2], label=gt_bbox.label, id=gt_bbox.id) - - rel_int_bbox_radius = min(w, h) / 2 / image_half_diag - radiuses.append(rel_int_bbox_radius) - - sample_points.extend(bbox_center.points) - - # Join points into a single annotation for compatibility with CVAT capabilities - updated_anns = [] - if sample_points: - updated_anns.append(dm.Points(sample_points, label=label_id)) - - updated_gt_dataset.put(gt_sample.wrap(annotations=updated_anns)) - - self._mean_gt_bbox_radius_estimation = min(0.5, np.mean(radiuses).item()) - - return updated_gt_dataset - - def _upload_task_meta(self, gt_dataset: dm.Dataset): - layout = points_task.TaskMetaLayout() - serializer = points_task.TaskMetaSerializer() - - file_list = [] - file_list.append( - ( - serializer.serialize_gt_annotations(gt_dataset), - layout.GT_FILENAME, - ) - ) - - storage_client = self._make_cloud_storage_client(self._oracle_data_bucket) - for file_data, filename in file_list: - storage_client.create_file( - compose_data_bucket_filename(self.escrow_address, self.chain_id, filename), - file_data, - ) - - def _setup_gt_job_for_cvat_task( - self, task_id: int, gt_dataset: dm.Dataset, *, dm_export_format: str = "datumaro" - ) -> None: - super()._setup_gt_job_for_cvat_task( - task_id=task_id, gt_dataset=gt_dataset, dm_export_format=dm_export_format - ) - - def _setup_quality_settings(self, task_id: int, **overrides) -> None: - assert self._mean_gt_bbox_radius_estimation is not _unset - - values = { - # We have at most 1 annotation per frame, so accuracy on each frame is either 0 or 1, - # regardless of the points count. For job accuracy we'll have: - # quality = mean frame accuracy = count of correct frames / validation frame count. - # If we set quality threshold from the manifest on this value, it can break quality - # requirements. - # Example: target quality is 80%, 6 frames, 1 has 20 points, 5 others have 1. - # Suppose the 5 frames with 1 point are annotated correctly and - # the one with 20 - is not annotated. The mean frame accuracy will be 5 / 6 ~ 83%, - # which is higher than the target quality, so the job will be accepted. - # The per-point mean (aka micro) accuracy will be 5 / (20 + 5) = 20%. - # - # Instead, we require that each frame matches with the required quality threshold, - # so the job accuracy is 1. This is a more strict requirement, from which - # follows that the job quality >= required. - # For each frame, as we have just 1 annotation, quality is computed as mean - # point quality on the frame with frame matching threshold. - # Point set accuracy is controlled by iou_threshold, - # so configure it with the requested quality value. - # - # TODO: consider adding a quality option to count points as separate, - # or implement and use the mean IoU as the target metric in CVAT. - "target_metric_threshold": 0.95, # some big number close to 1 - "iou_threshold": self.manifest.validation.min_quality, - "oks_sigma": self._mean_gt_bbox_radius_estimation, - "point_size_base": "image_size", - } - values.update(overrides) - super()._setup_quality_settings(task_id, **values) - - def build(self): - if len(self.manifest.annotation.labels) != 1: - # TODO: implement support for multiple labels - # Probably, need to split the whole task into projects per label - # for efficient annotation - raise NotImplementedError("Point annotation tasks can have only 1 label") - - return super().build() - - -class PolygonTaskBuilder(SimpleTaskBuilder): - def _setup_quality_settings(self, task_id: int, **overrides) -> None: - values = {"iou_threshold": Config.cvat_config.iou_threshold, **overrides} - super()._setup_quality_settings(task_id, **values) - - -class BoxesFromPointsTaskBuilder(_TaskBuilderBase): - def __init__(self, manifest: TaskManifest, escrow_address: str, chain_id: int) -> None: - super().__init__(manifest=manifest, escrow_address=escrow_address, chain_id=chain_id) - - self._input_gt_data: _MaybeUnset[bytes] = _unset - self._input_points_data: _MaybeUnset[bytes] = _unset - - self._data_filenames: _MaybeUnset[Sequence[str]] = _unset - self._input_gt_dataset: _MaybeUnset[dm.Dataset] = _unset - self._gt_dataset: _MaybeUnset[dm.Dataset] = _unset - self._gt_roi_dataset: _MaybeUnset[dm.Dataset] = _unset - self._points_dataset: _MaybeUnset[dm.Dataset] = _unset - - self._bbox_point_mapping: _MaybeUnset[boxes_from_points_task.BboxPointMapping] = _unset - "bbox_id -> point_id" - - self._roi_size_estimations: _MaybeUnset[dict[int, tuple[float, float]]] = _unset - "label_id -> (rel. w, rel. h)" - - self._rois: _MaybeUnset[boxes_from_points_task.RoiInfos] = _unset - self._roi_filenames: _MaybeUnset[boxes_from_points_task.RoiFilenames] = _unset - self._roi_filenames_to_be_annotated: _MaybeUnset[Sequence[str]] = _unset - self._gt_roi_filenames: _MaybeUnset[Sequence[str]] = _unset - - self._job_layout: _MaybeUnset[Sequence[Sequence[str]]] = _unset - "File lists per CVAT job" - - self._label_configuration: _MaybeUnset[Sequence[dict]] = _unset - - self._excluded_points_info: _MaybeUnset[_ExcludedAnnotationsInfo] = _unset - self._excluded_gt_info: _MaybeUnset[_ExcludedAnnotationsInfo] = _unset - - # Configuration / constants - # TODO: consider WebP if produced files are too big - self.roi_file_ext = ".png" # supposed to be lossless and reasonably compressing - "File extension for RoI images, with leading dot (.) included" - - self.list_display_threshold = 5 - "The maximum number of rendered list items in a message" - - self.roi_size_mult = 1.1 - "Additional point ROI size multiplier" - - self.min_roi_size = ( - Config.core_config.min_roi_size_w, - Config.core_config.min_roi_size_h, - ) - "Minimum absolute ROI size, (w, h)" - - self.points_format = "coco_person_keypoints" - - self.embed_point_in_roi_image = True - "Put a small point into the extracted RoI images for the original point" - - self.embedded_point_radius = 15 - self.min_embedded_point_radius_percent = 0.005 - self.max_embedded_point_radius_percent = 0.01 - self.embedded_point_color = (0, 255, 255) - self.roi_background_color = (245, 240, 242) # BGR - CVAT background color - - self.oracle_data_bucket = BucketAccessInfo.parse_obj(Config.storage_config) - - self.min_class_samples_for_roi_estimation = 25 - - self.max_class_roi_image_side_threshold = 0.5 - """ - The maximum allowed percent of the image for the estimated class RoI, - before the default RoI is used. Too big RoI estimations reduce the overall - prediction quality, making them unreliable. - """ - - self.max_discarded_threshold = 0.5 - """ - The maximum allowed percent of discarded - GT boxes, points, or samples for successful job launch - """ - - # TODO: probably, need to also add an absolute number of minimum GT RoIs - - def _download_input_data(self): - data_bucket = BucketAccessInfo.parse_obj(self.manifest.data.data_url) - gt_bucket = BucketAccessInfo.parse_obj(self.manifest.validation.gt_url) - points_bucket = BucketAccessInfo.parse_obj(self.manifest.data.points_url) - - data_storage_client = self._make_cloud_storage_client(data_bucket) - gt_storage_client = self._make_cloud_storage_client(gt_bucket) - points_storage_client = self._make_cloud_storage_client(points_bucket) - - data_filenames = data_storage_client.list_files(prefix=data_bucket.path) - data_filenames = strip_bucket_prefix(data_filenames, prefix=data_bucket.path) - self._data_filenames = filter_image_files(data_filenames) - - self._input_gt_data = gt_storage_client.download_file(gt_bucket.path) - self._input_gt_filename = Path(gt_bucket.path).name - - self._input_points_data = points_storage_client.download_file(points_bucket.path) - - def _parse_dataset(self, annotation_file_data: bytes, dataset_format: str) -> dm.Dataset: - temp_dir = self.exit_stack.enter_context(TemporaryDirectory()) - - annotation_filename = os.path.join(temp_dir, "annotations.json") - with open(annotation_filename, "wb") as f: - f.write(annotation_file_data) - - return dm.Dataset.import_from(annotation_filename, format=dataset_format) - - def _parse_gt(self): - assert self._input_gt_data is not _unset - - self._input_gt_dataset = self._parse_dataset( - self._input_gt_data, - dataset_format=DM_GT_DATASET_FORMAT_MAPPING[self.manifest.annotation.type], - ) - - def _parse_points(self): - assert self._input_points_data is not _unset - - self._points_dataset = self._parse_dataset( - self._input_points_data, dataset_format=self.points_format - ) - - def _validate_gt_labels(self): - gt_labels = set( - label.name - for label in self._input_gt_dataset.categories()[dm.AnnotationType.label] - if not label.parent - ) - manifest_labels = set(label.name for label in self.manifest.annotation.labels) - if gt_labels - manifest_labels: - raise DatasetValidationError( - "GT labels do not match job labels. Unknown labels: {}".format( - format_sequence(list(gt_labels - manifest_labels)), - ) - ) - - self._input_gt_dataset.transform( - ProjectLabels, dst_labels=[label.name for label in self.manifest.annotation.labels] - ) - self._input_gt_dataset.init_cache() - - def _validate_gt_filenames(self): - gt_filenames = set(s.id + s.media.ext for s in self._input_gt_dataset) - - known_data_filenames = set(self._data_filenames) - matched_gt_filenames = gt_filenames.intersection(known_data_filenames) - - if len(gt_filenames) != len(matched_gt_filenames): - extra_gt = list(map(os.path.basename, gt_filenames - matched_gt_filenames)) - - raise MismatchingAnnotations( - "Failed to find several validation samples in the dataset files: {}".format( - format_sequence(extra_gt) - ) - ) - - if len(gt_filenames) < self._job_val_frames_count: - raise TooFewSamples( - f"Too few validation samples provided ({len(gt_filenames)}), " - f"at least {self._job_val_frames_count} required." - ) - - def _validate_gt_annotations(self): - label_cat: dm.LabelCategories = self._input_gt_dataset.categories()[dm.AnnotationType.label] - - excluded_gt_info = _ExcludedAnnotationsInfo() - excluded_samples = set() - visited_ids = set() - for gt_sample in self._input_gt_dataset: - # Could fail on this as well - img_h, img_w = gt_sample.media_as(dm.Image).size - - sample_boxes = [a for a in gt_sample.annotations if isinstance(a, dm.Bbox)] - valid_boxes = [] - for bbox in sample_boxes: - if not ( - (0 <= int(bbox.x) < int(bbox.x + bbox.w) <= img_w) - and (0 <= int(bbox.y) < int(bbox.y + bbox.h) <= img_h) - ): - excluded_gt_info.add_message( - "Sample '{}': GT bbox #{} ({}) - invalid coordinates".format( - gt_sample.id, bbox.id, label_cat[bbox.label].name - ), - sample_id=gt_sample.id, - sample_subset=gt_sample.subset, - ) - continue - - if bbox.id in visited_ids: - excluded_gt_info.add_message( - "Sample '{}': GT bbox #{} ({}) skipped - repeated annotation id {}".format( - gt_sample.id, bbox.id, label_cat[bbox.label].name, bbox.id - ), - sample_id=gt_sample.id, - sample_subset=gt_sample.subset, - ) - continue - - valid_boxes.append(bbox) - - excluded_gt_info.excluded_count += len(sample_boxes) - len(valid_boxes) - excluded_gt_info.total_count += len(sample_boxes) - - if len(valid_boxes) != len(sample_boxes): - if not valid_boxes: - excluded_samples.add((gt_sample.id, gt_sample.subset)) - else: - self._input_gt_dataset.put(gt_sample.wrap(annotations=valid_boxes)) - - for excluded_sample in excluded_samples: - self._input_gt_dataset.remove(*excluded_sample) - - if excluded_gt_info.excluded_count: - self.logger.warning( - "Some GT boxes were excluded due to the errors found: \n{}".format( - format_sequence([e.message for e in excluded_gt_info.messages], separator="\n") - ) - ) - - if excluded_gt_info.excluded_count > ceil( - excluded_gt_info.total_count * self.max_discarded_threshold - ): - raise TooFewSamples( - "Too many GT boxes discarded, canceling job creation. Errors: {}".format( - format_sequence( - [error_info.message for error_info in excluded_gt_info.messages] - ) - ) - ) - - self._excluded_gt_info = excluded_gt_info - - def _validate_gt(self): - assert self._data_filenames is not _unset - assert self._input_gt_dataset is not _unset - - self._validate_gt_filenames() - self._validate_gt_labels() - self._validate_gt_annotations() - - def _validate_points_categories(self): - invalid_point_categories_messages = [] - points_dataset_categories = self._points_dataset.categories() - points_dataset_label_cat: dm.LabelCategories = points_dataset_categories[ - dm.AnnotationType.label - ] - for category_id, category in points_dataset_categories[ - dm.AnnotationType.points - ].items.items(): - if len(category.labels) != 1: - invalid_point_categories_messages.append( - "Category '{}' (#{}): {}".format( - points_dataset_label_cat[category_id].name, - category_id, - f"too many skeleton points ({len(category.labels)}), only 1 expected", - ) - ) - - if invalid_point_categories_messages: - raise InvalidCategories( - "Invalid categories in the input point annotations: {}".format( - format_sequence(invalid_point_categories_messages, separator="; ") - ) - ) - - points_labels = set(label.name for label in points_dataset_label_cat if not label.parent) - manifest_labels = set(label.name for label in self.manifest.annotation.labels) - if manifest_labels != points_labels: - raise DatasetValidationError("Point labels do not match job labels") - - self._points_dataset.transform( - ProjectLabels, dst_labels=[label.name for label in self.manifest.annotation.labels] - ) - self._points_dataset.init_cache() - - def _validate_points_filenames(self): - points_filenames = set(sample.id + sample.media.ext for sample in self._points_dataset) - - known_data_filenames = set(self._data_filenames) - matched_points_filenames = points_filenames.intersection(known_data_filenames) - - if len(matched_points_filenames) != len(points_filenames): - extra_point_samples = list( - map(os.path.basename, points_filenames - matched_points_filenames) - ) - - raise MismatchingAnnotations( - "Failed to find several samples in the dataset files: {}".format( - format_sequence(extra_point_samples), - ) - ) - - def _validate_points_annotations(self): - def _validate_skeleton(skeleton: dm.Skeleton, *, sample_bbox: dm.Bbox): - if skeleton.id in visited_ids: - raise DatasetValidationError(f"repeated annotation id ({skeleton.id})") - - if len(skeleton.elements) != 1: - raise DatasetValidationError( - f"invalid points count ({len(skeleton.elements)}), expected 1" - ) - - point = skeleton.elements[0] - px, py = point.points[:2] - if not is_point_in_bbox(px, py, sample_bbox): - raise InvalidCoordinates("coordinates are outside image") - - label_cat: dm.LabelCategories = self._points_dataset.categories()[dm.AnnotationType.label] - - excluded_points_info = _ExcludedAnnotationsInfo() - excluded_samples = set() - visited_ids = set() - for sample in self._points_dataset: - # Could fail on this as well - image_h, image_w = sample.image.size - sample_bbox = dm.Bbox(0, 0, w=image_w, h=image_h) - - sample_skeletons = [a for a in sample.annotations if isinstance(a, dm.Skeleton)] - valid_skeletons = [] - for skeleton in sample_skeletons: - # Here 1 skeleton describes 1 point - try: - _validate_skeleton(skeleton, sample_bbox=sample_bbox) - except InvalidCoordinates as error: - excluded_points_info.add_message( - "Sample '{}': point #{} ({}) skipped - {}".format( - sample.id, skeleton.id, label_cat[skeleton.label].name, error - ), - sample_id=sample.id, - sample_subset=sample.subset, - ) - continue - except DatasetValidationError as error: - excluded_points_info.add_message( - "Sample '{}': point #{} ({}) - {}".format( - sample.id, skeleton.id, label_cat[skeleton.label].name, error - ), - sample_id=sample.id, - sample_subset=sample.subset, - ) - continue - - valid_skeletons.append(skeleton) - - excluded_points_info.excluded_count += len(sample_skeletons) - len(valid_skeletons) - excluded_points_info.total_count += len(sample_skeletons) - - if len(valid_skeletons) != len(sample_skeletons): - if not valid_skeletons: - excluded_samples.add((sample.id, sample.subset)) - else: - self._points_dataset.put(sample.wrap(annotations=valid_skeletons)) - - for excluded_sample in excluded_samples: - self._points_dataset.remove(*excluded_sample) - - if excluded_points_info.excluded_count: - self.logger.warning( - "Some points were excluded due to the errors found: \n{}".format( - format_sequence( - [e.message for e in excluded_points_info.messages], separator="\n" - ) - ) - ) - - if excluded_points_info.excluded_count > ceil( - excluded_points_info.total_count * self.max_discarded_threshold - ): - raise TooFewSamples( - "Too many points discarded, canceling job creation. Errors: {}".format( - format_sequence( - [error_info.message for error_info in excluded_points_info.messages] - ) - ) - ) - - self._excluded_points_info = excluded_points_info - - def _validate_points(self): - assert self._data_filenames is not _unset - assert self._points_dataset is not _unset - - self._validate_points_categories() - self._validate_points_filenames() - self._validate_points_annotations() - - @staticmethod - def _is_point_in_bbox(px: float, py: float, bbox: dm.Bbox) -> bool: - return is_point_in_bbox(px, py, bbox) - - def _prepare_gt(self): - def _find_unambiguous_matches( - input_skeletons: list[dm.Skeleton], - gt_boxes: list[dm.Bbox], - ) -> list[tuple[dm.Skeleton, dm.Bbox]]: - matches = [ - [ - (input_skeleton.label == gt_bbox.label) - and ( - self._is_point_in_bbox( - *input_skeleton.elements[0].points[0:2], bbox=gt_bbox - ) - ) - for gt_bbox in gt_boxes - ] - for input_skeleton in input_skeletons - ] - - ambiguous_boxes: list[int] = set() - ambiguous_skeletons: list[int] = set() - for skeleton_idx, input_skeleton in enumerate(input_skeletons): - matched_boxes: list[dm.Bbox] = [ - gt_boxes[j] for j in range(len(gt_boxes)) if matches[skeleton_idx][j] - ] - - if len(matched_boxes) > 1: - # Handle ambiguous matches - excluded_points_info.add_message( - "Sample '{}': point #{} ({}) and overlapping boxes skipped - " - "too many matching boxes ({}) found".format( - points_sample.id, - input_skeleton.id, - points_label_cat[input_skeleton.label].name, - format_sequence([f"#{a.id}" for a in matched_boxes]), - ), - sample_id=points_sample.id, - sample_subset=points_sample.subset, - ) - # not an error, should not be counted as excluded for an error - ambiguous_skeletons.add(input_skeleton.id) - ambiguous_boxes.update(a.id for a in matched_boxes) - continue - - for gt_idx, gt_bbox in enumerate(gt_boxes): - matched_skeletons: list[dm.Skeleton] = [ - input_skeletons[i] for i in range(len(input_skeletons)) if matches[i][gt_idx] - ] - - if len(matched_skeletons) > 1: - # Handle ambiguous matches - excluded_gt_info.add_message( - "Sample '{}': GT bbox #{} ({}) and overlapping points skipped - " - "too many matching points ({}) found".format( - gt_sample.id, - gt_bbox.id, - gt_label_cat[gt_bbox.label].name, - format_sequence([f"#{a.id}" for a in matched_skeletons]), - ), - sample_id=gt_sample.id, - sample_subset=gt_sample.subset, - ) - # not an error, should not be counted as excluded for an error - ambiguous_boxes.add(gt_bbox.id) - ambiguous_skeletons.update(a.id for a in matched_skeletons) - continue - if not matched_skeletons: - # Handle unmatched skeletons - excluded_gt_info.add_message( - "Sample '{}': GT bbox #{} ({}) skipped - " - "no matching points found".format( - gt_sample.id, - gt_bbox.id, - gt_label_cat[gt_bbox.label].name, - ), - sample_id=gt_sample.id, - sample_subset=gt_sample.subset, - ) - excluded_gt_info.excluded_count += 1 # an error - continue - - unambiguous_matches: list[tuple[dm.Bbox, dm.Skeleton]] = [] - for skeleton_idx, input_skeleton in enumerate(input_skeletons): - if input_skeleton.id in ambiguous_skeletons: - continue - - matched_bbox = None - for gt_idx, gt_bbox in enumerate(gt_boxes): - if gt_bbox.id in ambiguous_boxes: - continue - - if matches[skeleton_idx][gt_idx]: - matched_bbox = gt_bbox - break - - if matched_bbox: - unambiguous_matches.append((input_skeleton, matched_bbox)) - - return unambiguous_matches - - def _find_good_gt_boxes( - input_skeletons: list[dm.Skeleton], - gt_boxes: list[dm.Bbox], - ) -> list[dm.Bbox]: - matches = _find_unambiguous_matches(input_skeletons, gt_boxes) - - matched_boxes = [] - for input_skeleton, gt_bbox in matches: - gt_count_per_class[gt_bbox.label] = gt_count_per_class.get(gt_bbox.label, 0) + 1 - - matched_boxes.append(gt_bbox) - bbox_point_mapping[gt_bbox.id] = input_skeleton.id - - return matched_boxes - - assert self._data_filenames is not _unset - assert self._points_dataset is not _unset - assert self._input_gt_dataset is not _unset - assert [ - label.name for label in self._input_gt_dataset.categories()[dm.AnnotationType.label] - ] == [label.name for label in self.manifest.annotation.labels] - assert [ - label.name - for label in self._points_dataset.categories()[dm.AnnotationType.label] - if not label.parent - ] == [label.name for label in self.manifest.annotation.labels] - - points_label_cat: dm.LabelCategories = self._points_dataset.categories()[ - dm.AnnotationType.label - ] - gt_label_cat: dm.LabelCategories = self._input_gt_dataset.categories()[ - dm.AnnotationType.label - ] - - updated_gt_dataset = dm.Dataset( - categories=self._input_gt_dataset.categories(), media_type=dm.Image - ) - - excluded_points_info = _ExcludedAnnotationsInfo() # local for the function - excluded_gt_info = self._excluded_gt_info - gt_count_per_class = {} - bbox_point_mapping = {} # bbox id -> point id - for gt_sample in self._input_gt_dataset: - points_sample = self._points_dataset.get(gt_sample.id, gt_sample.subset) - assert points_sample - - gt_boxes = [a for a in gt_sample.annotations if isinstance(a, dm.Bbox)] - input_skeletons = [a for a in points_sample.annotations if isinstance(a, dm.Skeleton)] - - # Samples without boxes are allowed, so we just skip them without an error - if not gt_boxes: - continue - - matched_boxes = _find_good_gt_boxes(input_skeletons, gt_boxes) - if not matched_boxes: - continue - - updated_gt_dataset.put(gt_sample.wrap(annotations=matched_boxes)) - - if excluded_points_info.messages: - self.logger.warning( - "Some points were excluded from GT due to the problems found: \n{}".format( - format_sequence( - [e.message for e in excluded_points_info.messages], separator="\n" - ) - ) - ) - - if excluded_gt_info.messages: - self.logger.warning( - "Some GT annotations were excluded due to the problems found: \n{}".format( - format_sequence([e.message for e in excluded_gt_info.messages], separator="\n") - ) - ) - - if excluded_gt_info.excluded_count > ceil( - excluded_gt_info.total_count * self.max_discarded_threshold - ): - raise DatasetValidationError( - "Too many GT boxes discarded ({} out of {}). " - "Please make sure each GT box matches exactly 1 point".format( - excluded_gt_info.total_count - len(bbox_point_mapping), - excluded_gt_info.total_count, - ) - ) - - self.logger.info( - "GT counts per class to be used for validation: {}".format( - format_sequence( - [ - f"{gt_label_cat[label_id].name}: {count}" - for label_id, count in gt_count_per_class.items() - ] - ) - ) - ) - - gt_labels_without_anns = [ - gt_label_cat[label_id] - for label_id, label_count in gt_count_per_class.items() - if not label_count - ] - if gt_labels_without_anns: - raise DatasetValidationError( - "No matching GT boxes/points annotations found for some classes: {}".format( - format_sequence(gt_labels_without_anns) - ) - ) - - self._gt_dataset = updated_gt_dataset - self._bbox_point_mapping = bbox_point_mapping - - def _estimate_roi_sizes(self): - assert self._gt_dataset is not _unset - assert [label.name for label in self._gt_dataset.categories()[dm.AnnotationType.label]] == [ - label.name for label in self.manifest.annotation.labels - ] - - bbox_sizes_per_label = {} - for sample in self._gt_dataset: - image_h, image_w = self._points_dataset.get(sample.id, sample.subset).image.size - - for gt_bbox in sample.annotations: - gt_bbox = cast(dm.Bbox, gt_bbox) - bbox_sizes_per_label.setdefault(gt_bbox.label, []).append( - ( - gt_bbox.w / image_w, - gt_bbox.h / image_h, - ) - ) - - # Consider bbox sides as normally-distributed random variables, estimate max - # For big enough datasets, it should be reasonable approximation - # (due to the central limit theorem). This can work bad for small datasets, - # so we only do this if there are enough class samples. - classes_with_default_roi: dict[int, str] = {} # label_id -> reason - roi_size_estimations_per_label = {} # label id -> (w, h) - default_roi_size = (2, 2) # 2 will yield just the image size after halving - for label_id, label_sizes in bbox_sizes_per_label.items(): - if len(label_sizes) < self.min_class_samples_for_roi_estimation: - estimated_size = default_roi_size - classes_with_default_roi[label_id] = "too few GT provided" - else: - max_bbox = np.max(label_sizes, axis=0) - if np.any(max_bbox > self.max_class_roi_image_side_threshold): - estimated_size = default_roi_size - classes_with_default_roi[label_id] = "estimated RoI is unreliable" - else: - estimated_size = 2 * max_bbox * self.roi_size_mult - - roi_size_estimations_per_label[label_id] = estimated_size - - if classes_with_default_roi: - label_cat = self._gt_dataset.categories()[dm.AnnotationType.label] - labels_by_reason = { - g_reason: [v[0] for v in g_items] - for g_reason, g_items in groupby( - sorted(classes_with_default_roi.items(), key=lambda v: v[1]), key=lambda v: v[1] - ) - } - self.logger.warning( - "Some classes will use the full image instead of RoI - {}".format( - "; ".join( - "{}: {}".format( - g_reason, - format_sequence([label_cat[label_id].name for label_id in g_labels]), - ) - for g_reason, g_labels in labels_by_reason.items() - ) - ) - ) - - self._roi_size_estimations = roi_size_estimations_per_label - - def _prepare_roi_info(self): - assert self._gt_dataset is not _unset - assert self._roi_size_estimations is not _unset - assert self._points_dataset is not _unset - - rois: list[boxes_from_points_task.RoiInfo] = [] - for sample in self._points_dataset: - for skeleton in sample.annotations: - if not isinstance(skeleton, dm.Skeleton): - continue - - point_label_id = skeleton.label - original_point_x, original_point_y = skeleton.elements[0].points[:2] - original_point_x = int(original_point_x) - original_point_y = int(original_point_y) - - image_h, image_w = sample.image.size - - roi_est_w, roi_est_h = self._roi_size_estimations[point_label_id] - roi_est_w *= image_w - roi_est_h *= image_h - roi_est_w = max(roi_est_w, self.min_roi_size[0]) - roi_est_h = max(roi_est_h, self.min_roi_size[1]) - - roi_left = max(0, original_point_x - int(roi_est_w / 2)) - roi_top = max(0, original_point_y - int(roi_est_h / 2)) - roi_right = min(image_w, original_point_x + ceil(roi_est_w / 2)) - roi_bottom = min(image_h, original_point_y + ceil(roi_est_h / 2)) - - roi_w = roi_right - roi_left - roi_h = roi_bottom - roi_top - - new_point_x = original_point_x - roi_left - new_point_y = original_point_y - roi_top - - rois.append( - boxes_from_points_task.RoiInfo( - point_id=skeleton.id, - original_image_key=sample.attributes["id"], - point_x=new_point_x, - point_y=new_point_y, - roi_x=roi_left, - roi_y=roi_top, - roi_w=roi_w, - roi_h=roi_h, - ) - ) - - self._rois = rois - - def _mangle_filenames(self): - """ - Mangle filenames in the dataset to make them less recognizable by annotators - and hide private dataset info - """ - assert self._rois is not _unset - - # TODO: maybe add different names for the same GT images in - # different jobs to make them even less recognizable - self._roi_filenames = { - roi.point_id: str(uuid.uuid4()) + self.roi_file_ext for roi in self._rois - } - - def _prepare_job_layout(self): - assert self._rois is not _unset - assert self._bbox_point_mapping is not _unset - assert self._input_gt_dataset is not _unset - - # This list can be different from what is selected for validation - input_gt_filenames = set(sample.media.path for sample in self._input_gt_dataset) - original_image_id_to_filename = { - sample.attributes["id"]: sample.media.path for sample in self._points_dataset - } - point_id_to_original_image_id = {roi.point_id: roi.original_image_key for roi in self._rois} - - gt_point_ids = set(self._bbox_point_mapping.values()) - self._gt_roi_filenames = [self._roi_filenames[point_id] for point_id in gt_point_ids] - self._roi_filenames_to_be_annotated = [ - fn - for point_id, fn in self._roi_filenames.items() - if point_id not in gt_point_ids - if original_image_id_to_filename[point_id_to_original_image_id[point_id]] - not in input_gt_filenames - ] - - def _prepare_label_configuration(self): - self._label_configuration = make_label_configuration(self.manifest) - - def _upload_task_meta(self): - layout = boxes_from_points_task.TaskMetaLayout() - serializer = boxes_from_points_task.TaskMetaSerializer() - - file_list = [] - file_list.append((self._input_points_data, layout.POINTS_FILENAME)) - file_list.append( - ( - serializer.serialize_gt_annotations(self._gt_dataset), - layout.GT_FILENAME, - ) - ) - file_list.append( - ( - serializer.serialize_bbox_point_mapping(self._bbox_point_mapping), - layout.BBOX_POINT_MAPPING_FILENAME, - ) - ) - file_list.append((serializer.serialize_roi_info(self._rois), layout.ROI_INFO_FILENAME)) - file_list.append( - (serializer.serialize_roi_filenames(self._roi_filenames), layout.ROI_FILENAMES_FILENAME) - ) - - storage_client = self._make_cloud_storage_client(self.oracle_data_bucket) - for file_data, filename in file_list: - storage_client.create_file( - compose_data_bucket_filename(self.escrow_address, self.chain_id, filename), - file_data, - ) - - def _extract_roi( - self, source_pixels: np.ndarray, roi_info: boxes_from_points_task.RoiInfo - ) -> np.ndarray: - img_h, img_w, *_ = source_pixels.shape - - roi_pixels = source_pixels[ - max(0, roi_info.roi_y) : min(img_h, roi_info.roi_y + roi_info.roi_h), - max(0, roi_info.roi_x) : min(img_w, roi_info.roi_x + roi_info.roi_w), - ] - - if not ( - (0 <= roi_info.roi_x < roi_info.roi_x + roi_info.roi_w < img_w) - and (0 <= roi_info.roi_y < roi_info.roi_y + roi_info.roi_h < img_h) - ): - # Coords can be outside the original image - # In this case a border should be added to RoI, so that the image was centered on bbox - wrapped_roi_pixels = np.zeros((roi_info.roi_h, roi_info.roi_w, 3), dtype=np.float32) - wrapped_roi_pixels[:, :] = self.roi_background_color - - dst_y = max(-roi_info.roi_y, 0) - dst_x = max(-roi_info.roi_x, 0) - wrapped_roi_pixels[ - dst_y : dst_y + roi_pixels.shape[0], - dst_x : dst_x + roi_pixels.shape[1], - ] = roi_pixels - - roi_pixels = wrapped_roi_pixels - else: - roi_pixels = roi_pixels.copy() - - return roi_pixels - - def _draw_roi_point( - self, roi_pixels: np.ndarray, roi_info: boxes_from_points_task.RoiInfo - ) -> np.ndarray: - center = (roi_info.point_x, roi_info.point_y) - - roi_r = (roi_info.roi_w**2 + roi_info.roi_h**2) ** 0.5 / 2 - point_size = int( - min( - self.max_embedded_point_radius_percent * roi_r, - max(self.embedded_point_radius, self.min_embedded_point_radius_percent * roi_r), - ) - ) - - roi_pixels = roi_pixels.copy() - roi_pixels = cv2.circle( - roi_pixels, - center, - point_size + 1, - (255, 255, 255), - cv2.FILLED, - ) - return cv2.circle( - roi_pixels, - center, - point_size, - self.embedded_point_color, - cv2.FILLED, - ) - - def _extract_and_upload_rois(self): - assert self._points_dataset is not _unset - assert self._rois is not _unset - assert self._data_filenames is not _unset - assert self._roi_filenames is not _unset - - src_bucket = BucketAccessInfo.parse_obj(self.manifest.data.data_url) - src_prefix = src_bucket.path - dst_bucket = self.oracle_data_bucket - - src_client = self._make_cloud_storage_client(src_bucket) - dst_client = self._make_cloud_storage_client(dst_bucket) - - image_id_to_filename = { - sample.attributes["id"]: sample.image.path for sample in self._points_dataset - } - - filename_to_sample = {sample.image.path: sample for sample in self._points_dataset} - - def _roi_key(e): - return e.original_image_key - - rois_by_image: dict[str, Sequence[boxes_from_points_task.RoiInfo]] = { - image_id_to_filename[image_id]: list(g) - for image_id, g in groupby(sorted(self._rois, key=_roi_key), key=_roi_key) - } - - def process_file(filename: str, image_pixels: np.ndarray): - image_roi_infos = rois_by_image.get(filename, []) - if not image_roi_infos: - return - - sample = filename_to_sample[filename] - if tuple(sample.image.size) != tuple(image_pixels.shape[:2]): - # TODO: maybe rois should be regenerated instead - # Option 2: accumulate errors, fail when some threshold is reached - # Option 3: add special handling for cases when image is only rotated (exif etc.) - raise InvalidImageInfo( - f"Sample '{filename}': invalid size provided in the point annotations" - ) - - image_rois = {} - for roi_info in image_roi_infos: - roi_pixels = self._extract_roi(image_pixels, roi_info) - - if self.embed_point_in_roi_image: - roi_pixels = self._draw_roi_point(roi_pixels, roi_info) - - roi_filename = self._roi_filenames[roi_info.point_id] - roi_bytes = encode_image(roi_pixels, os.path.splitext(roi_filename)[-1]) - - image_rois[roi_filename] = roi_bytes - - for roi_filename, roi_bytes in image_rois.items(): - dst_client.create_file( - compose_data_bucket_filename(self.escrow_address, self.chain_id, roi_filename), - roi_bytes, - ) - - def download_and_decode(key: str): - image_bytes = src_client.download_file(key) - return decode_image(image_bytes) - - pool_size = Config.features.max_data_storage_connections - download_queue_size = 4 * pool_size - download_queue = Queue[tuple[str, Future[np.ndarray]]](download_queue_size) - roi_uploader = BufferedRoiImageUploader(queue=download_queue) - with ThreadPoolExecutor(pool_size) as pool: - - def put_callback(filename: str): - image_roi_infos = rois_by_image.get(filename, []) - if not image_roi_infos: - return None - - return ( - filename, - pool.submit(download_and_decode, os.path.join(src_prefix, filename)), - ) - - def process_callback(result: tuple[str, Future]): - filename, task = result - process_file(filename, task.result()) - - roi_uploader.process_all( - self._data_filenames, put_callback=put_callback, process_callback=process_callback - ) - - def _prepare_gt_roi_dataset(self): - self._gt_roi_dataset = dm.Dataset( - categories=self._gt_dataset.categories(), media_type=dm.Image - ) - - roi_info_by_point_id: dict[int, skeletons_from_boxes_task.RoiInfo] = { - roi_info.point_id: roi_info for roi_info in self._rois - } - - for sample in self._gt_dataset: - for gt_bbox in sample.annotations: - assert isinstance(gt_bbox, dm.Bbox) - - point_id = self._bbox_point_mapping[gt_bbox.id] - gt_roi_filename = compose_data_bucket_filename( - self.escrow_address, self.chain_id, self._roi_filenames[point_id] - ) - - # update gt bbox coordinates to match RoI shift - roi_info = roi_info_by_point_id[point_id] - new_x = gt_bbox.points[0] - roi_info.roi_x - new_y = gt_bbox.points[1] - roi_info.roi_y - - self._gt_roi_dataset.put( - sample.wrap( - id=os.path.splitext(gt_roi_filename)[0], - annotations=[gt_bbox.wrap(x=new_x, y=new_y)], - media=dm.Image(path=gt_roi_filename, size=sample.media_as(dm.Image).size), - attributes=filter_dict(sample.attributes, exclude_keys=["id"]), - ) - ) - - assert len(self._gt_roi_dataset) == len(self._gt_roi_filenames) - - def _create_on_cvat(self): - assert self._roi_filenames_to_be_annotated is not _unset - assert self._gt_roi_filenames is not _unset - assert self._label_configuration is not _unset - assert self._gt_roi_dataset is not _unset - - oracle_bucket = self.oracle_data_bucket - - # Register cloud storage on CVAT to pass user dataset - cvat_cloud_storage = cvat_api.create_cloudstorage( - **_make_cvat_cloud_storage_params(oracle_bucket) - ) - - # Create a project - cvat_project = cvat_api.create_project( - self.escrow_address, - labels=self._label_configuration, - user_guide=self.manifest.annotation.user_guide, - ) - - # Setup webhooks for the project - cvat_webhook = cvat_api.create_cvat_webhook(cvat_project.id) - - with SessionLocal.begin() as session: - segment_size = self._task_segment_size - total_jobs = math.ceil(len(self._roi_filenames_to_be_annotated) / segment_size) - self.logger.info( - "Task creation for escrow '%s': will create %s assignments", - self.escrow_address, - total_jobs, - ) - db_service.create_escrow_creation( - session, - escrow_address=self.escrow_address, - chain_id=self.chain_id, - total_jobs=total_jobs, - ) - - project_id = db_service.create_project( - session, - cvat_project.id, - cvat_cloud_storage.id, - self.manifest.annotation.type, - self.escrow_address, - self.chain_id, - oracle_bucket.to_url().rstrip("/") - + "/" - + compose_data_bucket_prefix(self.escrow_address, self.chain_id), - cvat_webhook_id=cvat_webhook.id, - ) - db_service.get_project_by_id(session, project_id, for_update=True) # lock the row - db_service.add_project_images( - session, - cvat_project.id, - [ - compose_data_bucket_filename(self.escrow_address, self.chain_id, fn) - for fn in self._roi_filenames.values() - ], - ) - - for data_subset in self._split_dataset_per_task( - self._roi_filenames_to_be_annotated, - subset_size=Config.cvat_config.max_jobs_per_task * segment_size, - ): - cvat_task = cvat_api.create_task( - cvat_project.id, self.escrow_address, segment_size=segment_size - ) - - task_id = db_service.create_task( - session, cvat_task.id, cvat_project.id, TaskStatuses[cvat_task.status] - ) - db_service.get_task_by_id(session, task_id, for_update=True) # lock the row - - filenames = [ - compose_data_bucket_filename(self.escrow_address, self.chain_id, fn) - for fn in data_subset - ] - gt_filenames = [ - compose_data_bucket_filename(self.escrow_address, self.chain_id, fn) - for fn in self._gt_roi_filenames - ] - - cvat_api.put_task_data( - cvat_task.id, - cvat_cloud_storage.id, - filenames=filenames, - validation_params={ - "gt_filenames": gt_filenames, - "gt_frames_per_job_count": self._job_val_frames_count, - }, - ) - - self._setup_gt_job_for_cvat_task( - cvat_task.id, self._gt_roi_dataset, dm_export_format="coco" - ) - self._setup_quality_settings(cvat_task.id) - - db_service.create_data_upload(session, cvat_task.id) - - db_service.touch(session, Project, [project_id]) - - def build(self): - self._download_input_data() - self._parse_gt() - self._parse_points() - self._validate_gt() - self._validate_points() - - # Task configuration creation - self._prepare_gt() - self._estimate_roi_sizes() - self._prepare_roi_info() - self._mangle_filenames() - self._prepare_label_configuration() - self._prepare_job_layout() - self._prepare_gt_roi_dataset() - - # Data preparation - self._extract_and_upload_rois() - self._upload_task_meta() - - self._create_on_cvat() - - -class SkeletonsFromBoxesTaskBuilder(_TaskBuilderBase): - @dataclass - class _TaskParams: - label_id: int - roi_ids: list[int] - roi_gt_ids: list[int] - - def __init__(self, manifest: TaskManifest, escrow_address: str, chain_id: int) -> None: - super().__init__(manifest=manifest, escrow_address=escrow_address, chain_id=chain_id) - - self._input_gt_data: _MaybeUnset[bytes] = _unset - self._input_boxes_data: _MaybeUnset[bytes] = _unset - - self._data_filenames: _MaybeUnset[Sequence[str]] = _unset - self._input_gt_dataset: _MaybeUnset[dm.Dataset] = _unset - self._gt_dataset: _MaybeUnset[dm.Dataset] = _unset - self._boxes_dataset: _MaybeUnset[dm.Dataset] = _unset - - self._skeleton_bbox_mapping: _MaybeUnset[skeletons_from_boxes_task.SkeletonBboxMapping] = ( - _unset - ) - self._roi_infos: _MaybeUnset[skeletons_from_boxes_task.RoiInfos] = _unset - self._roi_info_by_id: _MaybeUnset[dict[int, skeletons_from_boxes_task.RoiInfo]] = _unset - - self._gt_points_per_label: _MaybeUnset[ - dict[tuple[int, str], Sequence[tuple[int, dm.Points]]] - ] = _unset - "(skeleton_label_id, point_label_name) to [(skeleton_id, point), ...]" - - self._roi_filenames: _MaybeUnset[dict[int, str]] = _unset - - self._task_params: _MaybeUnset[list[self._TaskParams]] = _unset - - self._excluded_gt_info: _MaybeUnset[_ExcludedAnnotationsInfo] = _unset - self._excluded_boxes_info: _MaybeUnset[_ExcludedAnnotationsInfo] = _unset - - # Configuration / constants - self.job_size_mult = skeletons_from_boxes_task.DEFAULT_ASSIGNMENT_SIZE_MULTIPLIER - "Job size multiplier" - - # TODO: consider WebP if produced files are too big - self.roi_file_ext = ".png" # supposed to be lossless and reasonably compressing - "File extension for RoI images, with leading dot (.) included" - - self.list_display_threshold = 5 - "The maximum number of rendered list items in a message" - - self.roi_size_mult = 1.1 - "Additional point ROI size multiplier" - - self.min_roi_size = ( - Config.core_config.min_roi_size_w, - Config.core_config.min_roi_size_h, - ) - "Minimum absolute ROI size, (w, h)" - - self.boxes_format = "coco_person_keypoints" - - self.embed_bbox_in_roi_image = True - "Put a bbox into the extracted skeleton RoI images" - - self.embed_tile_border = True - - self.embedded_point_radius = 15 - self.min_embedded_point_radius_percent = 0.005 - self.max_embedded_point_radius_percent = 0.01 - self.embedded_point_color = (0, 255, 255) - - self.roi_embedded_bbox_color = (0, 255, 255) # BGR - self.roi_background_color = (245, 240, 242) # BGR - CVAT background color - - self.oracle_data_bucket = BucketAccessInfo.parse_obj(Config.storage_config) - - self.min_label_gt_samples = 2 # TODO: find good threshold - - self.max_discarded_threshold = 0.5 - """ - The maximum allowed percent of discarded - GT annotations or samples for successful job launch - """ - - self.gt_id_attribute = "object_id" - "An additional way to match GT skeletons with input boxes" - - # TODO: probably, need to also add an absolute number of minimum GT RoIs per class - - def _download_input_data(self): - data_bucket = BucketAccessInfo.parse_obj(self.manifest.data.data_url) - gt_bucket = BucketAccessInfo.parse_obj(self.manifest.validation.gt_url) - boxes_bucket = BucketAccessInfo.parse_obj(self.manifest.data.boxes_url) - - data_storage_client = self._make_cloud_storage_client(data_bucket) - gt_storage_client = self._make_cloud_storage_client(gt_bucket) - boxes_storage_client = self._make_cloud_storage_client(boxes_bucket) - - data_filenames = data_storage_client.list_files(prefix=data_bucket.path) - data_filenames = strip_bucket_prefix(data_filenames, prefix=data_bucket.path) - self._data_filenames = filter_image_files(data_filenames) - - self._input_gt_data = gt_storage_client.download_file(gt_bucket.path) - - self._input_boxes_data = boxes_storage_client.download_file(boxes_bucket.path) - - def _parse_dataset(self, annotation_file_data: bytes, dataset_format: str) -> dm.Dataset: - temp_dir = self.exit_stack.enter_context(TemporaryDirectory()) - - annotation_filename = os.path.join(temp_dir, "annotations.json") - with open(annotation_filename, "wb") as f: - f.write(annotation_file_data) - - return dm.Dataset.import_from(annotation_filename, format=dataset_format) - - def _parse_gt(self): - assert self._input_gt_data is not _unset - - self._input_gt_dataset = self._parse_dataset( - self._input_gt_data, - dataset_format=DM_GT_DATASET_FORMAT_MAPPING[self.manifest.annotation.type], - ) - - def _parse_boxes(self): - assert self._input_boxes_data is not _unset - - self._boxes_dataset = self._parse_dataset( - self._input_boxes_data, dataset_format=self.boxes_format - ) - - def _validate_gt_labels(self): - gt_labels = set( - (label.name, label.parent) - for label in self._input_gt_dataset.categories()[dm.AnnotationType.label] - ) - - manifest_labels = set() - for skeleton_label in self.manifest.annotation.labels: - manifest_labels.add((skeleton_label.name, "")) - for node_label in skeleton_label.nodes: - manifest_labels.add((node_label, skeleton_label.name)) - - if manifest_labels - gt_labels: - raise DatasetValidationError( - "Could not find GT for labels {}".format( - format_sequence( - [ - label_name if not parent_name else f"{parent_name}.{label_name}" - for label_name, parent_name in manifest_labels - gt_labels - ] - ), - ) - ) - - # It should not be an issue that there are some extra GT labels - they should - # just be skipped. - if gt_labels - manifest_labels: - self.logger.info( - "Skipping unknown GT labels: {}".format( - format_sequence( - [ - label_name if not parent_name else f"{parent_name}.{label_name}" - for label_name, parent_name in gt_labels - manifest_labels - ] - ) - ) - ) - - # Reorder and filter labels to match the manifest - self._input_gt_dataset.transform( - ProjectLabels, dst_labels=[label.name for label in self.manifest.annotation.labels] - ) - self._input_gt_dataset.init_cache() - - def _validate_gt_filenames(self): - gt_filenames = set(s.id + s.media.ext for s in self._input_gt_dataset) - - known_data_filenames = set(self._data_filenames) - matched_gt_filenames = gt_filenames.intersection(known_data_filenames) - - if len(gt_filenames) != len(matched_gt_filenames): - extra_gt = list(map(os.path.basename, gt_filenames - matched_gt_filenames)) - - raise MismatchingAnnotations( - "Failed to find several validation samples in the dataset files: {}".format( - format_sequence(extra_gt) - ) - ) - - if len(gt_filenames) < self._job_val_frames_count: - raise TooFewSamples( - f"Too few validation samples provided ({len(gt_filenames)}), " - f"at least {self._job_val_frames_count} required." - ) - - def _validate_gt_annotations(self): - def _validate_skeleton(skeleton: dm.Skeleton, *, sample_bbox: dm.Bbox): - if skeleton.id in visited_ids: - raise DatasetValidationError(f"repeated annotation id {skeleton.id}") - - for element in skeleton.elements: - # This is what Datumaro is expected to parse - assert len(element.points) == 2 - assert len(element.visibility) == 1 - - if element.visibility[0] == dm.Points.Visibility.absent: - continue - - px, py = element.points[:2] - if not is_point_in_bbox(int(px), int(py), sample_bbox): - raise InvalidCoordinates("skeleton point is outside the image") - - label_cat: dm.LabelCategories = self._input_gt_dataset.categories()[dm.AnnotationType.label] - - excluded_gt_info = _ExcludedAnnotationsInfo() - excluded_samples = set() - visited_ids = set() - for gt_sample in self._input_gt_dataset: - # Could fail on this as well - img_h, img_w = gt_sample.media_as(dm.Image).size - sample_bbox = dm.Bbox(0, 0, w=img_w, h=img_h) - - sample_skeletons = [a for a in gt_sample.annotations if isinstance(a, dm.Skeleton)] - valid_skeletons = [] - for skeleton in sample_skeletons: - try: - _validate_skeleton(skeleton, sample_bbox=sample_bbox) - except DatasetValidationError as error: - excluded_gt_info.add_message( - "Sample '{}': GT skeleton #{} ({}) skipped - {}".format( - gt_sample.id, skeleton.id, label_cat[skeleton.label].name, error - ), - sample_id=gt_sample.id, - sample_subset=gt_sample.subset, - ) - continue - - valid_skeletons.append(skeleton) - visited_ids.add(skeleton.id) - - excluded_gt_info.excluded_count += len(sample_skeletons) - len(valid_skeletons) - excluded_gt_info.total_count += len(sample_skeletons) - - if len(valid_skeletons) != len(sample_skeletons): - if not valid_skeletons: - excluded_samples.add((gt_sample.id, gt_sample.subset)) - else: - # Skeleton boxes can be in the list as well with the same ids / groups - skeleton_ids = set(a.id for a in valid_skeletons) - {0} - self._input_gt_dataset.put( - gt_sample.wrap( - annotations=[a for a in gt_sample.annotations if a.id in skeleton_ids] - ) - ) - - for excluded_sample in excluded_samples: - self._input_gt_dataset.remove(*excluded_sample) - - if excluded_gt_info.excluded_count: - self.logger.warning( - "Some GT skeletons were excluded due to the errors found: {}".format( - format_sequence([e.message for e in excluded_gt_info.messages], separator="\n") - ) - ) - - if excluded_gt_info.excluded_count > ceil( - excluded_gt_info.total_count * self.max_discarded_threshold - ): - raise TooFewSamples( - "Too many GT skeletons discarded, canceling job creation. Errors: {}".format( - format_sequence( - [error_info.message for error_info in excluded_gt_info.messages] - ) - ) - ) - - self._excluded_gt_info = excluded_gt_info - - def _validate_gt(self): - assert self._data_filenames is not _unset - assert self._input_gt_dataset is not _unset - - self._validate_gt_filenames() - self._validate_gt_labels() - self._validate_gt_annotations() - - def _validate_boxes_categories(self): - boxes_dataset_categories = self._boxes_dataset.categories() - boxes_dataset_label_cat: dm.LabelCategories = boxes_dataset_categories[ - dm.AnnotationType.label - ] - - boxes_labels = set(label.name for label in boxes_dataset_label_cat if not label.parent) - manifest_labels = set(label.name for label in self.manifest.annotation.labels) - if manifest_labels != boxes_labels: - raise DatasetValidationError("Bbox labels do not match job labels") - - # Reorder labels to match the manifest - self._boxes_dataset.transform( - ProjectLabels, dst_labels=[label.name for label in self.manifest.annotation.labels] - ) - self._boxes_dataset.init_cache() - - def _validate_boxes_filenames(self): - boxes_filenames = set(sample.id + sample.media.ext for sample in self._boxes_dataset) - - known_data_filenames = set(self._data_filenames) - matched_boxes_filenames = boxes_filenames.intersection(known_data_filenames) - - if len(matched_boxes_filenames) != len(boxes_filenames): - extra_bbox_samples = list( - map(os.path.basename, boxes_filenames - matched_boxes_filenames) - ) - - raise MismatchingAnnotations( - "Failed to find several samples in the dataset files: {}".format( - format_sequence(extra_bbox_samples), - ) - ) - - def _validate_boxes_annotations(self): # noqa: PLR0912 - # Convert possible polygons and masks into boxes - self._boxes_dataset.transform(InstanceSegmentsToBbox) - self._boxes_dataset.init_cache() - - excluded_boxes_info = _ExcludedAnnotationsInfo() - - label_cat: dm.LabelCategories = self._boxes_dataset.categories()[dm.AnnotationType.label] - - visited_ids = set() - for sample in self._boxes_dataset: - # Could fail on this as well - image_h, image_w = sample.media_as(dm.Image).size - - valid_instances: list[tuple[dm.Bbox, dm.Points]] = [] - instances = find_instances( - [a for a in sample.annotations if isinstance(a, dm.Bbox | dm.Skeleton)] - ) - for instance_anns in instances: - if len(instance_anns) != 2: - excluded_boxes_info.add_message( - "Sample '{}': object #{} ({}) skipped - unexpected group size ({})".format( - sample.id, - instance_anns[0].id, - label_cat[instance_anns[0].label].name, - len(instance_anns), - ), - sample_id=sample.id, - sample_subset=sample.subset, - ) - continue - - bbox = next((a for a in instance_anns if isinstance(a, dm.Bbox)), None) - if not bbox: - excluded_boxes_info.add_message( - "Sample '{}': object #{} ({}) skipped - no matching bbox".format( - sample.id, instance_anns[0].id, label_cat[instance_anns[0].label].name - ), - sample_id=sample.id, - sample_subset=sample.subset, - ) - continue - - skeleton = next((a for a in instance_anns if isinstance(a, dm.Skeleton)), None) - if not skeleton: - excluded_boxes_info.add_message( - "Sample '{}': object #{} ({}) skipped - no matching skeleton".format( - sample.id, instance_anns[0].id, label_cat[instance_anns[0].label].name - ), - sample_id=sample.id, - sample_subset=sample.subset, - ) - continue - - if len(skeleton.elements) != 1 or len(skeleton.elements[0].points) != 2: - excluded_boxes_info.add_message( - "Sample '{}': object #{} ({}) skipped - invalid skeleton points".format( - sample.id, skeleton.id, label_cat[skeleton.label].name - ), - sample_id=sample.id, - sample_subset=sample.subset, - ) - continue - - point = skeleton.elements[0] - if not is_point_in_bbox(point.points[0], point.points[1], (0, 0, image_w, image_h)): - excluded_boxes_info.add_message( - "Sample '{}': object #{} ({}) skipped - invalid point coordinates".format( - sample.id, skeleton.id, label_cat[skeleton.label].name - ), - sample_id=sample.id, - sample_subset=sample.subset, - ) - continue - - if not is_point_in_bbox(int(bbox.x), int(bbox.y), (0, 0, image_w, image_h)): - excluded_boxes_info.add_message( - "Sample '{}': object #{} ({}) skipped - invalid bbox coordinates".format( - sample.id, bbox.id, label_cat[bbox.label].name - ), - sample_id=sample.id, - sample_subset=sample.subset, - ) - continue - - if not is_point_in_bbox(point.points[0], point.points[1], bbox): - excluded_boxes_info.add_message( - "Sample '{}': object #{} ({}) skipped - point is outside the bbox".format( - sample.id, skeleton.id, label_cat[skeleton.label].name - ), - sample_id=sample.id, - sample_subset=sample.subset, - ) - continue - - if bbox.id in visited_ids: - excluded_boxes_info.add_message( - "Sample '{}': bbox #{} ({}) skipped - repeated annotation id {}".format( - sample.id, bbox.id, label_cat[bbox.label].name, bbox.id - ), - sample_id=sample.id, - sample_subset=sample.subset, - ) - continue - - valid_instances.append( - (bbox, point.wrap(group=bbox.group, id=bbox.id, attributes=bbox.attributes)) - ) - visited_ids.add(bbox.id) - - excluded_boxes_info.excluded_count += len(instances) - len(valid_instances) - excluded_boxes_info.total_count += len(instances) - - if len(valid_instances) != len(sample.annotations): - self._boxes_dataset.put( - sample.wrap(annotations=list(chain.from_iterable(valid_instances))) - ) - - if excluded_boxes_info.excluded_count > ceil( - excluded_boxes_info.total_count * self.max_discarded_threshold - ): - raise TooFewSamples( - "Too many boxes discarded, canceling job creation. Errors: {}".format( - format_sequence( - [error_info.message for error_info in excluded_boxes_info.messages] - ) - ) - ) - - excluded_samples = set((e.sample_id, e.sample_subset) for e in excluded_boxes_info.messages) - for excluded_sample in excluded_samples: - self._boxes_dataset.remove(*excluded_sample) - - if excluded_samples: - self.logger.warning( - "Some boxes were excluded due to the errors found: {}".format( - format_sequence( - [e.message for e in excluded_boxes_info.messages], separator="\n" - ) - ) - ) - - self._excluded_boxes_info = excluded_boxes_info - - def _validate_boxes(self): - assert self._data_filenames is not _unset - assert self._boxes_dataset is not _unset - - self._validate_boxes_categories() - self._validate_boxes_filenames() - self._validate_boxes_annotations() - - def _match_boxes(self, a: BboxCoords, b: BboxCoords) -> bool: - return bbox_iou(a, b) > 0 - - def _get_skeleton_bbox( - self, skeleton: dm.Skeleton, annotations: Sequence[dm.Annotation] - ) -> BboxCoords: - matching_bbox = None - if skeleton.group: - matching_bbox = next( - ( - bbox - for bbox in annotations - if isinstance(bbox, dm.Bbox) and bbox.group == skeleton.group - ), - None, - ) - - if matching_bbox: - bbox = matching_bbox.get_bbox() - else: - bbox = skeleton.get_bbox() - - if any( - v != dm.Points.Visibility.absent for e in skeleton.elements for v in e.visibility - ): - # If there's only 1 visible point, the bbox will have 0 w and h - bbox = [bbox[0], bbox[1], max(1, bbox[2]), max(1, bbox[3])] - - return bbox - - def _prepare_gt(self): - def _find_unambiguous_matches( - input_boxes: list[dm.Bbox], - gt_skeletons: list[dm.Skeleton], - *, - input_points: list[dm.Points], - gt_annotations: list[dm.Annotation], - ) -> list[tuple[dm.Bbox, dm.Skeleton]]: - bbox_point_mapping: dict[int, dm.Points] = { - bbox.id: next(p for p in input_points if p.group == bbox.group) - for bbox in input_boxes - } - - matches = [ - [ - (input_bbox.label == gt_skeleton.label) - and ( - self._match_boxes( - input_bbox.get_bbox(), - self._get_skeleton_bbox(gt_skeleton, gt_annotations), - ) - ) - and (input_point := bbox_point_mapping[input_bbox.id]) - and is_point_in_bbox( - input_point.points[0], - input_point.points[1], - self._get_skeleton_bbox(gt_skeleton, gt_annotations), - ) - and ( - # a way to customize matching if the default method is too rough - not (bbox_id := input_bbox.attributes.get(self.gt_id_attribute)) - or not (skeleton_id := gt_skeleton.attributes.get(self.gt_id_attribute)) - or bbox_id == skeleton_id - ) - for gt_skeleton in gt_skeletons - ] - for input_bbox in input_boxes - ] - - ambiguous_boxes: list[int] = set() - ambiguous_skeletons: list[int] = set() - for bbox_idx, input_bbox in enumerate(input_boxes): - matched_skeletons: list[dm.Skeleton] = [ - gt_skeletons[j] for j in range(len(gt_skeletons)) if matches[bbox_idx][j] - ] - - if len(matched_skeletons) > 1: - # Handle ambiguous matches - excluded_boxes_info.add_message( - "Sample '{}': bbox #{} ({}) and overlapping skeletons skipped - " - "too many matching skeletons ({}) found".format( - boxes_sample.id, - input_bbox.id, - boxes_label_cat[input_bbox.label].name, - format_sequence([f"#{a.id}" for a in matched_skeletons]), - ), - sample_id=boxes_sample.id, - sample_subset=boxes_sample.subset, - ) - # not an error, should not be counted as excluded for an error - ambiguous_boxes.add(input_bbox.id) - ambiguous_skeletons.update(s.id for s in matched_skeletons) - continue - - for skeleton_idx, gt_skeleton in enumerate(gt_skeletons): - matched_boxes: list[dm.Bbox] = [ - input_boxes[i] for i in range(len(input_boxes)) if matches[i][skeleton_idx] - ] - - if len(matched_boxes) > 1: - # Handle ambiguous matches - excluded_gt_info.add_message( - "Sample '{}': GT skeleton #{} ({}) and overlapping boxes skipped - " - "too many matching boxes ({}) found".format( - gt_sample.id, - gt_skeleton.id, - gt_label_cat[gt_skeleton.label].name, - format_sequence([f"#{a.id}" for a in matched_boxes]), - ), - sample_id=gt_sample.id, - sample_subset=gt_sample.subset, - ) - # not an error, should not be counted as excluded for an error - ambiguous_skeletons.add(gt_skeleton.id) - ambiguous_boxes.update(b.id for b in matched_boxes) - continue - if not matched_boxes: - # Handle unmatched skeletons - excluded_gt_info.add_message( - "Sample '{}': GT skeleton #{} ({}) skipped - " - "no matching boxes found".format( - gt_sample.id, - gt_skeleton.id, - gt_label_cat[gt_skeleton.label].name, - ), - sample_id=gt_sample.id, - sample_subset=gt_sample.subset, - ) - excluded_gt_info.excluded_count += 1 # an error - continue - - unambiguous_matches: list[tuple[dm.Bbox, dm.Skeleton]] = [] - for bbox_idx, input_bbox in enumerate(input_boxes): - if input_bbox.id in ambiguous_boxes: - continue - - matched_skeleton = None - for gt_idx, gt_skeleton in enumerate(gt_skeletons): - if gt_skeleton.id in ambiguous_skeletons: - continue - - if matches[bbox_idx][gt_idx]: - matched_skeleton = gt_skeleton - break - - if matched_skeleton: - unambiguous_matches.append((input_bbox, matched_skeleton)) - - return unambiguous_matches - - def _find_good_gt_skeletons( - input_boxes: list[dm.Bbox], - gt_skeletons: list[dm.Skeleton], - *, - input_points: list[dm.Points], - gt_annotations: list[dm.Annotation], - ) -> list[dm.Skeleton]: - matches = _find_unambiguous_matches( - input_boxes, gt_skeletons, input_points=input_points, gt_annotations=gt_annotations - ) - - matched_skeletons = [] - for input_bbox, gt_skeleton in matches: - gt_count_per_class[gt_skeleton.label] = ( - gt_count_per_class.get(gt_skeleton.label, 0) + 1 - ) - - matched_skeletons.append(gt_skeleton) - skeleton_bbox_mapping[gt_skeleton.id] = input_bbox.id - - return matched_skeletons - - assert self._data_filenames is not _unset - assert self._boxes_dataset is not _unset - assert self._input_gt_dataset is not _unset - assert [ - label.name - for label in self._input_gt_dataset.categories()[dm.AnnotationType.label] - if not label.parent - ] == [label.name for label in self.manifest.annotation.labels] - assert [ - label.name - for label in self._boxes_dataset.categories()[dm.AnnotationType.label] - if not label.parent - ] == [label.name for label in self.manifest.annotation.labels] - - boxes_label_cat: dm.LabelCategories = self._boxes_dataset.categories()[ - dm.AnnotationType.label - ] - gt_label_cat: dm.LabelCategories = self._input_gt_dataset.categories()[ - dm.AnnotationType.label - ] - - updated_gt_dataset = dm.Dataset( - categories=self._input_gt_dataset.categories(), media_type=dm.Image - ) - - excluded_boxes_info = _ExcludedAnnotationsInfo() # local for the function - excluded_gt_info = self._excluded_gt_info - gt_count_per_class = {} - skeleton_bbox_mapping = {} # skeleton id -> bbox id - for gt_sample in self._input_gt_dataset: - boxes_sample = self._boxes_dataset.get(gt_sample.id, gt_sample.subset) - # Samples could be discarded, so we just skip them without an error - if not boxes_sample: - continue - - gt_skeletons = [a for a in gt_sample.annotations if isinstance(a, dm.Skeleton)] - input_boxes = [a for a in boxes_sample.annotations if isinstance(a, dm.Bbox)] - input_points = [a for a in boxes_sample.annotations if isinstance(a, dm.Points)] - assert len(input_boxes) == len(input_points) - - # Samples without boxes are allowed, so we just skip them without an error - if not gt_skeletons: - continue - - matched_skeletons = _find_good_gt_skeletons( - input_boxes, - gt_skeletons, - input_points=input_points, - gt_annotations=gt_sample.annotations, - ) - if not matched_skeletons: - continue - - updated_gt_dataset.put(gt_sample.wrap(annotations=matched_skeletons)) - - if excluded_boxes_info.messages: - self.logger.warning( - "Some boxes were excluded from GT due to the problems found: {}".format( - format_sequence( - [e.message for e in excluded_boxes_info.messages], separator="\n" - ) - ) - ) - - if excluded_gt_info.messages: - self.logger.warning( - "Some GT annotations were excluded due to the errors found: {}".format( - format_sequence([e.message for e in excluded_gt_info.messages], separator="\n") - ) - ) - - if excluded_gt_info.excluded_count > ceil( - self.max_discarded_threshold * excluded_gt_info.total_count - ): - raise DatasetValidationError( - "Too many GT skeletons discarded ({} out of {}). " - "Please make sure each GT skeleton matches exactly 1 bbox " - "and has at least 1 visible point".format( - excluded_gt_info.total_count - len(skeleton_bbox_mapping), - excluded_gt_info.total_count, - ) - ) - - self.logger.info( - "GT counts per class to be used for validation: {}".format( - format_sequence( - [ - f"{gt_label_cat[label_id].name}: {count}" - for label_id, count in gt_count_per_class.items() - ] - ) - ) - ) - - labels_with_few_gt = [ - gt_label_cat[label_id] - for label_id, label_count in gt_count_per_class.items() - if label_count < self.min_label_gt_samples - ] - if labels_with_few_gt: - raise DatasetValidationError( - "Too few matching GT boxes/points annotations found for some classes: {}".format( - format_sequence(labels_with_few_gt) - ) - ) - - self._gt_dataset = updated_gt_dataset - self._skeleton_bbox_mapping = skeleton_bbox_mapping - - def _prepare_roi_infos(self): - assert self._gt_dataset is not _unset - assert self._boxes_dataset is not _unset - - rois: list[skeletons_from_boxes_task.RoiInfo] = [] - for sample in self._boxes_dataset: - instances = find_instances(sample.annotations) - for instance_anns in instances: - bbox = next(a for a in instance_anns if isinstance(a, dm.Bbox)) - point = next(a for a in instance_anns if isinstance(a, dm.Points)) - - # RoI is centered on bbox center - original_bbox_cx = int(bbox.x + bbox.w / 2) - original_bbox_cy = int(bbox.y + bbox.h / 2) - - roi_w = ceil(bbox.w * self.roi_size_mult) - roi_h = ceil(bbox.h * self.roi_size_mult) - roi_w = max(roi_w, self.min_roi_size[0]) - roi_h = max(roi_h, self.min_roi_size[1]) - - roi_x = original_bbox_cx - int(roi_w / 2) - roi_y = original_bbox_cy - int(roi_h / 2) - - new_bbox_x = bbox.x - roi_x - new_bbox_y = bbox.y - roi_y - - rois.append( - skeletons_from_boxes_task.RoiInfo( - original_image_key=sample.attributes["id"], - bbox_id=bbox.id, - bbox_label=bbox.label, - bbox_x=new_bbox_x, - bbox_y=new_bbox_y, - point_x=point.points[0] - roi_x, - point_y=point.points[1] - roi_y, - roi_x=roi_x, - roi_y=roi_y, - roi_w=roi_w, - roi_h=roi_h, - ) - ) - - self._roi_infos = rois - self._roi_info_by_id = {roi_info.bbox_id: roi_info for roi_info in self._roi_infos} - - def _mangle_filenames(self): - """ - Mangle filenames in the dataset to make them less recognizable by annotators - and hide private dataset info - """ - assert self._roi_infos is not _unset - - # TODO: maybe add different names for the same GT images in - # different jobs to make them even less recognizable - self._roi_filenames = { - roi_info.bbox_id: str(uuid.uuid4()) + self.roi_file_ext for roi_info in self._roi_infos - } - - @property - def _task_segment_size(self) -> int: - # Here we use a job size multiplier, because each image - # is supposed to be simple and the assignment is expected - # to take little time with the default job size. - # Then, we add a percent of job tiles for validation, keeping the requested ratio. - return super()._task_segment_size * self.job_size_mult - - @property - def _job_val_frames_count(self) -> int: - return super()._job_val_frames_count * self.job_size_mult - - def _prepare_task_params(self): - assert self._roi_infos is not _unset - assert self._skeleton_bbox_mapping is not _unset - assert self._input_gt_dataset is not _unset - - # This list can be different from what is selected for validation - input_gt_filenames = set(sample.media.path for sample in self._input_gt_dataset) - image_id_to_filename = { - sample.attributes["id"]: sample.media.path for sample in self._boxes_dataset - } - - task_params: list[self._TaskParams] = [] - segment_size = self._task_segment_size - for label_id, _ in enumerate(self.manifest.annotation.labels): - label_gt_roi_ids = set( - roi_id - for roi_id in self._skeleton_bbox_mapping.values() - if self._roi_info_by_id[roi_id].bbox_label == label_id - ) - - label_data_roi_ids = [ - roi_info.bbox_id - for roi_info in self._roi_infos - if roi_info.bbox_label == label_id - if roi_info.bbox_id not in label_gt_roi_ids - if image_id_to_filename[roi_info.original_image_key] not in input_gt_filenames - ] - random.shuffle(label_data_roi_ids) - - task_params.extend( - [ - self._TaskParams( - label_id=label_id, roi_ids=task_data_roi_ids, roi_gt_ids=label_gt_roi_ids - ) - for task_data_roi_ids in take_by( - label_data_roi_ids, Config.cvat_config.max_jobs_per_task * segment_size - ) - ] - ) - - self._task_params = task_params - - def _prepare_job_labels(self): - self._point_labels = {} - - for skeleton_label in self.manifest.annotation.labels: - for point_name in skeleton_label.nodes: - self._point_labels[(skeleton_label.name, point_name)] = point_name - - def _prepare_gt_points_mapping(self): - assert self._gt_dataset is not _unset - - self._gt_points_per_label = {} - - # GT should contain all GT images with only one point per skeleton node - for gt_sample in self._gt_dataset: - for gt_skeleton in gt_sample.annotations: - if not isinstance(gt_skeleton, dm.Skeleton): - continue - - for point in gt_skeleton.elements: - if point.visibility[0] in [ - dm.Points.Visibility.absent, - dm.Points.Visibility.hidden, - ]: - continue - - point_label_name = ( - self._gt_dataset.categories()[dm.AnnotationType.label] - .items[point.label] - .name - ) - - self._gt_points_per_label.setdefault( - (gt_skeleton.label, point_label_name), [] - ).append((gt_skeleton.id, point)) - - def _upload_task_meta(self): - layout = skeletons_from_boxes_task.TaskMetaLayout() - serializer = skeletons_from_boxes_task.TaskMetaSerializer() - - file_list = [] - file_list.append( - (serializer.serialize_bbox_annotations(self._boxes_dataset), layout.BOXES_FILENAME) - ) - file_list.append( - ( - serializer.serialize_gt_annotations(self._gt_dataset), - layout.GT_FILENAME, - ) - ) - file_list.append( - ( - serializer.serialize_skeleton_bbox_mapping(self._skeleton_bbox_mapping), - layout.SKELETON_BBOX_MAPPING_FILENAME, - ) - ) - file_list.append((serializer.serialize_roi_info(self._roi_infos), layout.ROI_INFO_FILENAME)) - file_list.append( - (serializer.serialize_roi_filenames(self._roi_filenames), layout.ROI_FILENAMES_FILENAME) - ) - file_list.append( - (serializer.serialize_point_labels(self._point_labels), layout.POINT_LABELS_FILENAME) - ) - - storage_client = self._make_cloud_storage_client(self.oracle_data_bucket) - for file_data, filename in file_list: - storage_client.create_file( - compose_data_bucket_filename(self.escrow_address, self.chain_id, filename), - file_data, - ) - - def _extract_roi( - self, source_pixels: np.ndarray, roi_info: skeletons_from_boxes_task.RoiInfo - ) -> np.ndarray: - img_h, img_w, *_ = source_pixels.shape - - roi_pixels = source_pixels[ - max(0, roi_info.roi_y) : min(img_h, roi_info.roi_y + roi_info.roi_h), - max(0, roi_info.roi_x) : min(img_w, roi_info.roi_x + roi_info.roi_w), - ] - - if not ( - (0 <= roi_info.roi_x < roi_info.roi_x + roi_info.roi_w < img_w) - and (0 <= roi_info.roi_y < roi_info.roi_y + roi_info.roi_h < img_h) - ): - # Coords can be outside the original image - # In this case a border should be added to RoI, so that the image was centered on bbox - wrapped_roi_pixels = np.zeros((roi_info.roi_h, roi_info.roi_w, 3), dtype=np.float32) - wrapped_roi_pixels[:, :] = self.roi_background_color - - dst_y = max(-roi_info.roi_y, 0) - dst_x = max(-roi_info.roi_x, 0) - wrapped_roi_pixels[ - dst_y : dst_y + roi_pixels.shape[0], - dst_x : dst_x + roi_pixels.shape[1], - ] = roi_pixels - - roi_pixels = wrapped_roi_pixels - else: - roi_pixels = roi_pixels.copy() - - return roi_pixels - - def _draw_roi_bbox(self, roi_image: np.ndarray, bbox: dm.Bbox) -> np.ndarray: - roi_cy = roi_image.shape[0] // 2 - roi_cx = roi_image.shape[1] // 2 - return cv2.rectangle( - roi_image, - tuple(map(int, (roi_cx - bbox.w / 2, roi_cy - bbox.h / 2))), - tuple(map(int, (roi_cx + bbox.w / 2, roi_cy + bbox.h / 2))), - self.roi_embedded_bbox_color, - 1, - cv2.LINE_4, - ) - - def _draw_roi_point(self, roi_image: np.ndarray, point: tuple[float, float]) -> np.ndarray: - roi_r = (roi_image.shape[0] ** 2 + roi_image.shape[1] ** 2) ** 0.5 / 2 - radius = int( - min( - self.max_embedded_point_radius_percent * roi_r, - max(self.embedded_point_radius, self.min_embedded_point_radius_percent * roi_r), - ) - ) - - roi_image = cv2.circle( - roi_image, - tuple(map(int, (point[0], point[1]))), - radius + 1, - (255, 255, 255), - -1, - cv2.LINE_4, - ) - return cv2.circle( - roi_image, - tuple(map(int, (point[0], point[1]))), - radius, - self.embedded_point_color, - -1, - cv2.LINE_4, - ) - - def _extract_and_upload_rois(self): - assert self._roi_filenames is not _unset - assert self._roi_infos is not _unset - - src_bucket = BucketAccessInfo.parse_obj(self.manifest.data.data_url) - src_prefix = src_bucket.path - dst_bucket = self.oracle_data_bucket - - src_client = self._make_cloud_storage_client(src_bucket) - dst_client = self._make_cloud_storage_client(dst_bucket) - - image_id_to_filename = { - sample.attributes["id"]: sample.image.path for sample in self._boxes_dataset - } - - filename_to_sample = {sample.image.path: sample for sample in self._boxes_dataset} - - def _roi_info_key(e): - return e.original_image_key - - roi_info_by_image: dict[str, Sequence[skeletons_from_boxes_task.RoiInfo]] = { - image_id_to_filename[image_id]: list(g) - for image_id, g in groupby( - sorted(self._roi_infos, key=_roi_info_key), key=_roi_info_key - ) - } - - bbox_by_id = { - bbox.id: bbox - for sample in self._boxes_dataset - for bbox in sample.annotations - if isinstance(bbox, dm.Bbox) - } - - def process_file(filename: str, image_pixels: np.ndarray): - image_roi_infos = roi_info_by_image.get(filename, []) - if not image_roi_infos: - return - - sample = filename_to_sample[filename] - if tuple(sample.image.size) != tuple(image_pixels.shape[:2]): - # TODO: maybe rois should be regenerated instead - # Option 2: accumulate errors, fail when some threshold is reached - # Option 3: add special handling for cases when image is only rotated (exif etc.) - raise InvalidImageInfo( - f"Sample '{filename}': invalid size provided in the point annotations" - ) - - for roi_info in image_roi_infos: - roi_pixels = self._extract_roi(image_pixels, roi_info) - - if self.embed_bbox_in_roi_image: - roi_pixels = self._draw_roi_bbox(roi_pixels, bbox_by_id[roi_info.bbox_id]) - roi_pixels = self._draw_roi_point( - roi_pixels, (roi_info.point_x, roi_info.point_y) - ) - - filename = self._roi_filenames[roi_info.bbox_id] - roi_bytes = encode_image(roi_pixels, os.path.splitext(filename)[-1]) - - dst_client.create_file( - compose_data_bucket_filename(self.escrow_address, self.chain_id, filename), - data=roi_bytes, - ) - - def download_and_decode(key: str): - image_bytes = src_client.download_file(key) - return decode_image(image_bytes) - - pool_size = Config.features.max_data_storage_connections - download_queue_size = 4 * pool_size - download_queue = Queue[tuple[str, Future[np.ndarray]]](download_queue_size) - roi_uploader = BufferedRoiImageUploader(queue=download_queue) - with ThreadPoolExecutor(pool_size) as pool: - - def put_callback(filename: str): - image_roi_infos = roi_info_by_image.get(filename, []) - if not image_roi_infos: - return None - - return ( - filename, - pool.submit(download_and_decode, os.path.join(src_prefix, filename)), - ) - - def process_callback(result: tuple[str, Future]): - filename, task = result - process_file(filename, task.result()) - - roi_uploader.process_all( - self._data_filenames, put_callback=put_callback, process_callback=process_callback - ) - - def _prepare_gt_dataset_for_skeleton_point( - self, - *, - point_label_name: str, - skeleton_label_id: int, - ) -> dm.Dataset: - assert self._gt_points_per_label is not _unset - - # Change annotations to Points for validation in CVAT, - # as annotators will use this annotation type - point_dataset = dm.Dataset( - categories={ - dm.AnnotationType.label: dm.LabelCategories.from_iterable([point_label_name]), - }, - media_type=dm.Image, - ) - - for gt_skeleton_id, gt_point in self._gt_points_per_label[ - (skeleton_label_id, point_label_name) - ]: - roi_key = self._skeleton_bbox_mapping[gt_skeleton_id] - roi_info = self._roi_info_by_id[roi_key] - - mangled_cvat_sample_id = compose_data_bucket_filename( - self.escrow_address, - self.chain_id, - os.path.splitext(self._roi_filenames[roi_key])[0], - ) - - # update points coordinates in accordance with roi coordinates - updated_points = [ - gt_point.points[0] - roi_info.roi_x, - gt_point.points[1] - roi_info.roi_y, - ] - - point_dataset.put( - dm.DatasetItem( - id=mangled_cvat_sample_id, - annotations=[dm.Points(id=gt_skeleton_id, points=updated_points, label=0)], - ) - ) - - return point_dataset - - def _save_cvat_gt_dataset_to_oracle_bucket( - self, - gt_dataset_path: Path, - *, - file_suffix: str = "", - ) -> None: - layout = skeletons_from_boxes_task.TaskMetaLayout() - - base_gt_filename = layout.GT_FILENAME.split(".", maxsplit=1)[0] - final_gt_filename = f"{base_gt_filename}_{file_suffix}" + ".zip" - gt_dataset_key = compose_data_bucket_filename( - self.escrow_address, self.chain_id, final_gt_filename - ) - - storage_client = self._make_cloud_storage_client(self._oracle_data_bucket) - storage_client.create_file(gt_dataset_key, gt_dataset_path.read_bytes()) - - def _setup_quality_settings(self, task_id: int, **overrides) -> None: - values = { - "oks_sigma": Config.cvat_config.oks_sigma, - "point_size_base": "image_size", # we don't expect any boxes or groups, so ignore them - } - values.update(overrides) - super()._setup_quality_settings(task_id, **values) - - def _create_on_cvat(self): - assert self._task_params is not _unset - assert self._point_labels is not _unset - assert self._gt_points_per_label is not _unset - - def _task_params_label_key(ts): - return ts.label_id - - tasks_by_skeleton_label = { - skeleton_label_id: list(g) - for skeleton_label_id, g in groupby( - sorted(self._task_params, key=_task_params_label_key), key=_task_params_label_key - ) - } - - label_specs_by_skeleton = { - skeleton_label_id: [ - { - "name": self._point_labels[(skeleton_label.name, skeleton_point)], - "type": "points", - } - for skeleton_point in skeleton_label.nodes - ] - for skeleton_label_id, skeleton_label in enumerate(self.manifest.annotation.labels) - } - - oracle_bucket = self.oracle_data_bucket - - # Register cloud storage on CVAT to pass user dataset - cvat_cloud_storage = cvat_api.create_cloudstorage( - **_make_cvat_cloud_storage_params(oracle_bucket) - ) - - segment_size = self._task_segment_size - - total_jobs = sum( - len(self.manifest.annotation.labels[tp.label_id].nodes) - * (math.ceil(len(tp.roi_ids) / segment_size)) - for tp in self._task_params - ) - self.logger.info( - "Task creation for escrow '%s': will create %s assignments", - self.escrow_address, - total_jobs, - ) - - with SessionLocal.begin() as session: - db_service.create_escrow_creation( - session, - escrow_address=self.escrow_address, - chain_id=self.chain_id, - total_jobs=total_jobs, - ) - created_projects = [] - - for skeleton_label_id, skeleton_label_tasks in tasks_by_skeleton_label.items(): - skeleton_label_filenames: list[list[str]] = [] - gt_skeleton_label_filenames: list[list[str]] = [] - - for skeleton_label_task in skeleton_label_tasks: - skeleton_label_filenames.append( - [ - compose_data_bucket_filename( - self.escrow_address, self.chain_id, self._roi_filenames[roi_id] - ) - for roi_id in skeleton_label_task.roi_ids - ], - ) - - gt_skeleton_label_filenames.append( - [ - compose_data_bucket_filename( - self.escrow_address, self.chain_id, self._roi_filenames[roi_id] - ) - for roi_id in skeleton_label_task.roi_gt_ids - ], - ) - - for point_label_spec in label_specs_by_skeleton[skeleton_label_id]: - point_label_name = point_label_spec["name"] - - # Create a project for each point label. - # CVAT doesn't support tasks with different labels in a project. - cvat_project = cvat_api.create_project( - name="{} ({} {})".format( - self.escrow_address, - self.manifest.annotation.labels[skeleton_label_id].name, - point_label_name, - ), - user_guide=self.manifest.annotation.user_guide, - labels=[point_label_spec], - # TODO: improve guide handling - split for different points - ) - - # Setup webhooks for the project - cvat_webhook = cvat_api.create_cvat_webhook(cvat_project.id) - - project_id = db_service.create_project( - session, - cvat_project.id, - cvat_cloud_storage.id, - self.manifest.annotation.type, - self.escrow_address, - self.chain_id, - oracle_bucket.to_url().rstrip("/") - + "/" - + compose_data_bucket_prefix(self.escrow_address, self.chain_id), - cvat_webhook_id=cvat_webhook.id, - ) - created_projects.append(project_id) - - db_service.get_project_by_id( - session, project_id, for_update=True - ) # lock the row - db_service.add_project_images( - session, - cvat_project.id, - list(set(chain.from_iterable(skeleton_label_filenames))), - ) - - for point_label_filenames, gt_point_label_filenames in zip( - skeleton_label_filenames, gt_skeleton_label_filenames, strict=False - ): - cvat_task = cvat_api.create_task( - cvat_project.id, - name=cvat_project.name, - segment_size=segment_size, - ) - - task_id = db_service.create_task( - session, cvat_task.id, cvat_project.id, TaskStatuses[cvat_task.status] - ) - db_service.get_task_by_id(session, task_id, for_update=True) # lock the row - - # The task is fully created once 'update:task' webhook is received. - cvat_api.put_task_data( - cvat_task.id, - cvat_cloud_storage.id, - filenames=point_label_filenames + gt_point_label_filenames, - validation_params={ - "gt_filenames": gt_point_label_filenames, - "gt_frames_per_job_count": self._job_val_frames_count, - }, - ) - - gt_point_dataset = self._prepare_gt_dataset_for_skeleton_point( - point_label_name=point_label_name, - skeleton_label_id=skeleton_label_id, - ) - - self._setup_gt_job_for_cvat_task( - cvat_task.id, gt_point_dataset, dm_export_format="cvat" - ) - self._setup_quality_settings( - cvat_task.id, oks_sigma=Config.cvat_config.oks_sigma - ) - - db_service.create_data_upload(session, cvat_task.id) - - db_service.touch(session, Project, created_projects) - - def build(self): - self._download_input_data() - self._parse_gt() - self._parse_boxes() - self._validate_gt() - self._validate_boxes() - - # Task configuration creation - self._prepare_gt() - self._prepare_roi_infos() - self._prepare_task_params() - self._mangle_filenames() - self._prepare_job_labels() - - # Data preparation - self._extract_and_upload_rois() - self._upload_task_meta() - self._prepare_gt_points_mapping() - - self._create_on_cvat() - - -def is_image(path: str) -> bool: - trunk, ext = os.path.splitext(os.path.basename(path)) - return trunk and ext.lower() in IMAGE_EXTENSIONS - - -def filter_image_files(data_filenames: list[str]) -> list[str]: - return [fn for fn in data_filenames if is_image(fn)] - - -def strip_bucket_prefix(data_filenames: list[str], prefix: str) -> list[str]: - return [os.path.relpath(fn, prefix) for fn in data_filenames] - - -def make_label_configuration(manifest: TaskManifest) -> list[dict]: - return [ - { - "name": label.name, - "type": LABEL_TYPE_MAPPING[manifest.annotation.type].value, - } - for label in manifest.annotation.labels - ] - - -def _make_cvat_cloud_storage_params(bucket_info: BucketAccessInfo) -> dict: - CLOUD_PROVIDER_TO_CVAT_CLOUD_PROVIDER = { - CloudProviders.aws: "AWS_S3_BUCKET", - CloudProviders.gcs: "GOOGLE_CLOUD_STORAGE", - } - - params = { - "provider": CLOUD_PROVIDER_TO_CVAT_CLOUD_PROVIDER[bucket_info.provider], - "bucket_name": bucket_info.bucket_name, - "bucket_host": bucket_info.host_url, - } - - if bucket_info.credentials: - params["credentials"] = bucket_info.credentials.to_dict() - - return params - - -def create_task(escrow_address: str, chain_id: int) -> None: - logger = get_function_logger(module_logger) - - manifest = parse_manifest(get_escrow_manifest(chain_id, escrow_address)) - - if manifest.annotation.type in [TaskTypes.image_boxes, TaskTypes.image_label_binary]: - builder_type = SimpleTaskBuilder - elif manifest.annotation.type is TaskTypes.image_polygons: - builder_type = PolygonTaskBuilder - elif manifest.annotation.type in [TaskTypes.image_points]: - builder_type = PointsTaskBuilder - elif manifest.annotation.type in [TaskTypes.image_boxes_from_points]: - builder_type = BoxesFromPointsTaskBuilder - elif manifest.annotation.type in [TaskTypes.image_skeletons_from_boxes]: - builder_type = SkeletonsFromBoxesTaskBuilder - else: - raise Exception(f"Unsupported task type {manifest.annotation.type}") - - with builder_type(manifest, escrow_address, chain_id) as task_builder: - task_builder.set_logger(logger) - task_builder.build() diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/__init__.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/__init__.py new file mode 100644 index 0000000000..9e4f056f12 --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/__init__.py @@ -0,0 +1,3 @@ +from src.handlers.job_creation.factory import create_task + +__all__ = ["create_task"] diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/__init__.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/__init__.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py new file mode 100644 index 0000000000..d3e9fe4940 --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from src.handlers.job_creation.builders.vision.base import _TaskBuilderBase + + +class AudioTranscriptionTaskBuilder(_TaskBuilderBase): + """ + Handles task creation for the AUDIO_TRANSCRIPTION task type. + + Stub: audio transcription task creation is not implemented yet. + """ + + def build(self) -> None: + raise NotImplementedError("Audio transcription task creation is not implemented yet") diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/__init__.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/base.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/base.py new file mode 100644 index 0000000000..fd5c0dd97c --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/base.py @@ -0,0 +1,155 @@ +from __future__ import annotations + +import random +from abc import ABCMeta, abstractmethod +from contextlib import ExitStack +from pathlib import Path +from tempfile import TemporaryDirectory +from time import sleep +from typing import TYPE_CHECKING + +from datumaro.util import take_by + +import src.cvat.api_calls as cvat_api +import src.services.cloud as cloud_service +from src.core.config import Config +from src.services.cloud import CloudProviders, StorageClient +from src.services.cloud.utils import BucketAccessInfo +from src.utils.logging import NullLogger +from src.utils.zip_archive import write_dir_to_zip_archive + +if TYPE_CHECKING: + from collections.abc import Generator + from logging import Logger + + import datumaro as dm + + from src.core.manifest import TaskManifest + + +class _TaskBuilderBase(metaclass=ABCMeta): + def __init__(self, manifest: TaskManifest, escrow_address: str, chain_id: int) -> None: + self.exit_stack = ExitStack() + self.manifest = manifest + self.escrow_address = escrow_address + self.chain_id = chain_id + + self.logger: Logger = NullLogger() + + self._oracle_data_bucket = BucketAccessInfo.parse_obj(Config.storage_config) + + @property + def _task_segment_size(self) -> int: + return self.manifest.annotation.job_size + + @property + def _job_val_frames_count(self) -> int: + return self.manifest.validation.val_size + + def __enter__(self): + return self + + def __exit__(self, *args, **kwargs): + self.close() + + def close(self): + self.exit_stack.close() + + def set_logger(self, logger: Logger): + # TODO: add escrow info into messages + self.logger = logger + return self + + @classmethod + def _make_cloud_storage_client(cls, bucket_info: BucketAccessInfo) -> StorageClient: + extra_args = {} + + if bucket_info.provider == CloudProviders.aws: + import boto3.session + + extra_args["config"] = boto3.session.Config( + max_pool_connections=Config.features.max_data_storage_connections + ) + elif bucket_info.provider == CloudProviders.gcs: + pass # TODO: test and add connections if needed + + return cloud_service.make_client(bucket_info, **extra_args) + + def _save_cvat_gt_dataset_to_oracle_bucket( # noqa: B027 + self, + gt_dataset_path: Path, + *, + file_suffix: str = "", + ) -> None: + # method saves gt annotations that will be uploaded to CVAT + # into oracle bucket + pass + + def _wait_task_creation(self, task_id: int) -> cvat_api.RequestStatus: + # TODO: add a timeout or + # save gt datasets in the oracle bucket and upload in track_task_creation() + while True: + task_status, _ = cvat_api.get_task_upload_status(task_id) + if task_status not in [cvat_api.RequestStatus.STARTED, cvat_api.RequestStatus.QUEUED]: + return task_status + + sleep(Config.cvat_config.task_creation_check_interval) + + def _setup_gt_job_for_cvat_task( + self, task_id: int, gt_dataset: dm.Dataset, *, dm_export_format: str = "coco" + ) -> None: + task_status = self._wait_task_creation(task_id) + if task_status != cvat_api.RequestStatus.FINISHED: + return # will be handled in state_trackers.py::track_task_creation + + dm_format_to_cvat_format = { + "coco": "COCO 1.0", + "cvat": "CVAT 1.1", + "datumaro": "Datumaro 1.0", + } + + with TemporaryDirectory() as tmp_dir: + export_dir = Path(tmp_dir) / "export" + gt_dataset.export(save_dir=str(export_dir), save_media=False, format=dm_export_format) + + annotations_archive_path = Path(tmp_dir) / "annotations.zip" + with annotations_archive_path.open("wb") as annotations_archive: + write_dir_to_zip_archive(export_dir, annotations_archive) + + if Config.is_development_mode(): + self._save_cvat_gt_dataset_to_oracle_bucket( + gt_dataset_path=annotations_archive_path, file_suffix=f"for_task_{task_id}" + ) + + self._setup_gt_job( + task_id, + annotations_archive_path, + format_name=dm_format_to_cvat_format[dm_export_format], + ) + + def _setup_gt_job(self, task_id: int, dataset_path: Path, format_name: str) -> None: + gt_job = cvat_api.get_gt_job(task_id) + cvat_api.upload_gt_annotations(gt_job.id, dataset_path, format_name=format_name) + cvat_api.finish_gt_job(gt_job.id) + + def _setup_quality_settings(self, task_id: int, **overrides) -> None: + settings = cvat_api.get_quality_control_settings(task_id) + + values = { + "target_metric_threshold": self.manifest.validation.min_quality, + "empty_is_annotated": True, + } + values.update(**overrides) + cvat_api.update_quality_control_settings(settings.id, **values) + + def _split_dataset_per_task( + self, + data_filenames: list[str], + *, + subset_size: int, + ) -> Generator[str]: + random.shuffle(data_filenames) + yield from take_by(data_filenames, subset_size) + + @abstractmethod + def build(self) -> None: ... diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/basic.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/basic.py new file mode 100644 index 0000000000..088c1520f2 --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/basic.py @@ -0,0 +1,327 @@ +from __future__ import annotations + +import math +import os +from tempfile import TemporaryDirectory +from typing import TYPE_CHECKING + +import datumaro as dm +import numpy as np + +import src.core.tasks.points as points_task +import src.core.tasks.simple as simple_task +import src.cvat.api_calls as cvat_api +import src.services.cloud as cloud_service +import src.services.cvat as db_service +from src.core.config import Config +from src.core.storage import compose_data_bucket_filename +from src.core.types import TaskStatuses +from src.db import SessionLocal +from src.models.cvat import Project +from src.services.cloud.utils import BucketAccessInfo +from src.utils.logging import format_sequence + +if TYPE_CHECKING: + from src.core.manifest import TaskManifest + +from src.core.tasks.cvat_formats import DM_GT_DATASET_FORMAT_MAPPING +from src.handlers.job_creation.builders.vision.base import _TaskBuilderBase +from src.handlers.job_creation.exceptions import ( + DatasetValidationError, + TooFewSamples, +) +from src.handlers.job_creation.utils import ( + _make_cvat_cloud_storage_params, + _MaybeUnset, + _unset, + filter_image_files, + make_label_configuration, +) + + +class SimpleTaskBuilder(_TaskBuilderBase): + """ + Handles task creation for the IMAGE_BOXES task type + """ + + def _upload_task_meta(self, gt_dataset: dm.Dataset): + layout = simple_task.TaskMetaLayout() + serializer = simple_task.TaskMetaSerializer() + + file_list = [] + file_list.append( + ( + serializer.serialize_gt_annotations(gt_dataset), + layout.GT_FILENAME, + ) + ) + + storage_client = self._make_cloud_storage_client(self._oracle_data_bucket) + for file_data, filename in file_list: + storage_client.create_file( + compose_data_bucket_filename(self.escrow_address, self.chain_id, filename), + file_data, + ) + + def _parse_gt_dataset( + self, gt_file_data: bytes, *, add_prefix: str | None = None + ) -> dm.Dataset: + with TemporaryDirectory() as gt_temp_dir: + gt_filename = os.path.join(gt_temp_dir, "gt_annotations.json") + with open(gt_filename, "wb") as f: + f.write(gt_file_data) + + gt_dataset = dm.Dataset.import_from( + gt_filename, + format=DM_GT_DATASET_FORMAT_MAPPING[self.manifest.annotation.type], + ) + + if add_prefix: + gt_dataset = dm.Dataset.from_iterable( + [s.wrap(id=os.path.join(add_prefix, s.id)) for s in gt_dataset], + categories=gt_dataset.categories(), + media_type=gt_dataset.media_type(), + ) + + gt_dataset.init_cache() + + return gt_dataset + + def _get_gt_filenames( + self, gt_dataset: dm.Dataset, data_filenames: list[str], *, manifest: TaskManifest + ) -> list[str]: + gt_filenames = set(s.id + s.media.ext for s in gt_dataset) + known_data_filenames = set(data_filenames) + matched_gt_filenames = gt_filenames.intersection(known_data_filenames) + + if len(gt_filenames) != len(matched_gt_filenames): + missing_gt = gt_filenames - matched_gt_filenames + raise DatasetValidationError( + "Failed to find several validation samples in the dataset files: {}".format( + format_sequence(list(missing_gt)) + ) + ) + + if len(gt_filenames) < manifest.validation.val_size: + raise TooFewSamples( + f"Too few validation samples provided ({len(gt_filenames)}), " + f"at least {manifest.validation.val_size} required." + ) + + return list(matched_gt_filenames) + + def build(self): + manifest = self.manifest + escrow_address = self.escrow_address + chain_id = self.chain_id + + data_bucket = BucketAccessInfo.parse_obj(manifest.data.data_url) + gt_bucket = BucketAccessInfo.parse_obj(manifest.validation.gt_url) + + data_bucket_client = cloud_service.make_client(data_bucket) + gt_bucket_client = cloud_service.make_client(gt_bucket) + + # Task configuration creation + data_filenames = data_bucket_client.list_files(prefix=data_bucket.path) + data_filenames = filter_image_files(data_filenames) + + gt_file_data = gt_bucket_client.download_file(gt_bucket.path) + + # Validate and parse GT + gt_dataset = self._parse_gt_dataset(gt_file_data, add_prefix=data_bucket.path) + + # Create task configuration + gt_filenames = self._get_gt_filenames(gt_dataset, data_filenames, manifest=manifest) + data_to_be_annotated = [f for f in data_filenames if f not in set(gt_filenames)] + label_configuration = make_label_configuration(manifest) + + self._upload_task_meta(gt_dataset) + + # Register cloud storage on CVAT to pass user dataset + cloud_storage = cvat_api.create_cloudstorage(**_make_cvat_cloud_storage_params(data_bucket)) + + # Create a project + cvat_project = cvat_api.create_project( + escrow_address, + labels=label_configuration, + user_guide=manifest.annotation.user_guide, + ) + + # Setup webhooks for the project + cvat_webhook = cvat_api.create_cvat_webhook(cvat_project.id) + + with SessionLocal.begin() as session: + segment_size = self._task_segment_size + total_jobs = math.ceil(len(data_to_be_annotated) / segment_size) + + self.logger.info( + "Task creation for escrow '%s': will create %s assignments", + escrow_address, + total_jobs, + ) + db_service.create_escrow_creation( + session, escrow_address=escrow_address, chain_id=chain_id, total_jobs=total_jobs + ) + + project_id = db_service.create_project( + session, + cvat_project.id, + cloud_storage.id, + manifest.annotation.type, + escrow_address, + chain_id, + data_bucket.to_url(), + cvat_webhook_id=cvat_webhook.id, + ) + + db_service.get_project_by_id(session, project_id, for_update=True) # lock the row + db_service.add_project_images(session, cvat_project.id, data_filenames) + + for data_subset in self._split_dataset_per_task( + data_to_be_annotated, + subset_size=Config.cvat_config.max_jobs_per_task * segment_size, + ): + cvat_task = cvat_api.create_task( + cvat_project.id, escrow_address, segment_size=segment_size + ) + task_id = db_service.create_task( + session, cvat_task.id, cvat_project.id, TaskStatuses[cvat_task.status] + ) + db_service.get_task_by_id(session, task_id, for_update=True) # lock the row + + # The task is fully created once 'update:task' webhook is received. + cvat_api.put_task_data( + cvat_task.id, + cloud_storage.id, + filenames=data_subset, + validation_params={ + "gt_filenames": gt_filenames, # include whole GT dataset into each task + "gt_frames_per_job_count": self._job_val_frames_count, + }, + ) + + self._setup_gt_job_for_cvat_task(cvat_task.id, gt_dataset) + self._setup_quality_settings(cvat_task.id) + + db_service.create_data_upload(session, cvat_task.id) + db_service.touch(session, Project, [project_id]) + + +class PointsTaskBuilder(SimpleTaskBuilder): + def __init__(self, manifest: TaskManifest, escrow_address: str, chain_id: int) -> None: + super().__init__(manifest=manifest, escrow_address=escrow_address, chain_id=chain_id) + + self._mean_gt_bbox_radius_estimation: _MaybeUnset[float] = _unset + + def _parse_gt_dataset(self, gt_file_data, *, add_prefix=None): + gt_dataset = super()._parse_gt_dataset(gt_file_data, add_prefix=add_prefix) + + assert len(gt_dataset.categories()[dm.AnnotationType.label]) == 1 + label_id = 0 + + updated_gt_dataset = dm.Dataset(categories=gt_dataset.categories(), media_type=dm.Image) + + # Replace boxes with their center points + # Collect radius statistics + radiuses = [] + for gt_sample in gt_dataset: + image_size = gt_sample.media_as(dm.Image).size + image_half_diag = ((image_size[0] ** 2 + image_size[1] ** 2) ** 0.5) / 2 + + sample_points = [] + for gt_bbox in gt_sample.annotations: + if not isinstance(gt_bbox, dm.Bbox): + continue + + x, y, w, h = gt_bbox.get_bbox() + bbox_center = dm.Points([x + w / 2, y + h / 2], label=gt_bbox.label, id=gt_bbox.id) + + rel_int_bbox_radius = min(w, h) / 2 / image_half_diag + radiuses.append(rel_int_bbox_radius) + + sample_points.extend(bbox_center.points) + + # Join points into a single annotation for compatibility with CVAT capabilities + updated_anns = [] + if sample_points: + updated_anns.append(dm.Points(sample_points, label=label_id)) + + updated_gt_dataset.put(gt_sample.wrap(annotations=updated_anns)) + + self._mean_gt_bbox_radius_estimation = min(0.5, np.mean(radiuses).item()) + + return updated_gt_dataset + + def _upload_task_meta(self, gt_dataset: dm.Dataset): + layout = points_task.TaskMetaLayout() + serializer = points_task.TaskMetaSerializer() + + file_list = [] + file_list.append( + ( + serializer.serialize_gt_annotations(gt_dataset), + layout.GT_FILENAME, + ) + ) + + storage_client = self._make_cloud_storage_client(self._oracle_data_bucket) + for file_data, filename in file_list: + storage_client.create_file( + compose_data_bucket_filename(self.escrow_address, self.chain_id, filename), + file_data, + ) + + def _setup_gt_job_for_cvat_task( + self, task_id: int, gt_dataset: dm.Dataset, *, dm_export_format: str = "datumaro" + ) -> None: + super()._setup_gt_job_for_cvat_task( + task_id=task_id, gt_dataset=gt_dataset, dm_export_format=dm_export_format + ) + + def _setup_quality_settings(self, task_id: int, **overrides) -> None: + assert self._mean_gt_bbox_radius_estimation is not _unset + + values = { + # We have at most 1 annotation per frame, so accuracy on each frame is either 0 or 1, + # regardless of the points count. For job accuracy we'll have: + # quality = mean frame accuracy = count of correct frames / validation frame count. + # If we set quality threshold from the manifest on this value, it can break quality + # requirements. + # Example: target quality is 80%, 6 frames, 1 has 20 points, 5 others have 1. + # Suppose the 5 frames with 1 point are annotated correctly and + # the one with 20 - is not annotated. The mean frame accuracy will be 5 / 6 ~ 83%, + # which is higher than the target quality, so the job will be accepted. + # The per-point mean (aka micro) accuracy will be 5 / (20 + 5) = 20%. + # + # Instead, we require that each frame matches with the required quality threshold, + # so the job accuracy is 1. This is a more strict requirement, from which + # follows that the job quality >= required. + # For each frame, as we have just 1 annotation, quality is computed as mean + # point quality on the frame with frame matching threshold. + # Point set accuracy is controlled by iou_threshold, + # so configure it with the requested quality value. + # + # TODO: consider adding a quality option to count points as separate, + # or implement and use the mean IoU as the target metric in CVAT. + "target_metric_threshold": 0.95, # some big number close to 1 + "iou_threshold": self.manifest.validation.min_quality, + "oks_sigma": self._mean_gt_bbox_radius_estimation, + "point_size_base": "image_size", + } + values.update(overrides) + super()._setup_quality_settings(task_id, **values) + + def build(self): + if len(self.manifest.annotation.labels) != 1: + # TODO: implement support for multiple labels + # Probably, need to split the whole task into projects per label + # for efficient annotation + raise NotImplementedError("Point annotation tasks can have only 1 label") + + return super().build() + + +class PolygonTaskBuilder(SimpleTaskBuilder): + def _setup_quality_settings(self, task_id: int, **overrides) -> None: + values = {"iou_threshold": Config.cvat_config.iou_threshold, **overrides} + super()._setup_quality_settings(task_id, **values) diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/boxes_from_points.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/boxes_from_points.py new file mode 100644 index 0000000000..e7bcafa60c --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/boxes_from_points.py @@ -0,0 +1,1142 @@ +from __future__ import annotations + +import math +import os +import uuid +from concurrent.futures import Future, ThreadPoolExecutor +from itertools import groupby +from math import ceil +from pathlib import Path +from queue import Queue +from tempfile import TemporaryDirectory +from typing import TYPE_CHECKING, cast + +import cv2 +import datumaro as dm +import numpy as np +from datumaro.util import filter_dict +from datumaro.util.image import decode_image, encode_image + +import src.core.tasks.boxes_from_points as boxes_from_points_task +import src.core.tasks.skeletons_from_boxes as skeletons_from_boxes_task +import src.cvat.api_calls as cvat_api +import src.services.cvat as db_service +from src.core.config import Config +from src.core.storage import compose_data_bucket_filename, compose_data_bucket_prefix +from src.core.types import TaskStatuses +from src.db import SessionLocal +from src.models.cvat import Project +from src.services.cloud.utils import BucketAccessInfo +from src.utils.annotations import ProjectLabels, is_point_in_bbox +from src.utils.logging import format_sequence +from src.utils.roi_uploader import BufferedRoiImageUploader + +if TYPE_CHECKING: + from collections.abc import Sequence + + from src.core.manifest import TaskManifest + +from src.core.tasks.cvat_formats import DM_GT_DATASET_FORMAT_MAPPING +from src.handlers.job_creation.builders.vision.base import _TaskBuilderBase +from src.handlers.job_creation.exceptions import ( + DatasetValidationError, + InvalidCategories, + InvalidCoordinates, + InvalidImageInfo, + MismatchingAnnotations, + TooFewSamples, +) +from src.handlers.job_creation.utils import ( + _ExcludedAnnotationsInfo, + _make_cvat_cloud_storage_params, + _MaybeUnset, + _unset, + filter_image_files, + make_label_configuration, + strip_bucket_prefix, +) + + +class BoxesFromPointsTaskBuilder(_TaskBuilderBase): + def __init__(self, manifest: TaskManifest, escrow_address: str, chain_id: int) -> None: + super().__init__(manifest=manifest, escrow_address=escrow_address, chain_id=chain_id) + + self._input_gt_data: _MaybeUnset[bytes] = _unset + self._input_points_data: _MaybeUnset[bytes] = _unset + + self._data_filenames: _MaybeUnset[Sequence[str]] = _unset + self._input_gt_dataset: _MaybeUnset[dm.Dataset] = _unset + self._gt_dataset: _MaybeUnset[dm.Dataset] = _unset + self._gt_roi_dataset: _MaybeUnset[dm.Dataset] = _unset + self._points_dataset: _MaybeUnset[dm.Dataset] = _unset + + self._bbox_point_mapping: _MaybeUnset[boxes_from_points_task.BboxPointMapping] = _unset + "bbox_id -> point_id" + + self._roi_size_estimations: _MaybeUnset[dict[int, tuple[float, float]]] = _unset + "label_id -> (rel. w, rel. h)" + + self._rois: _MaybeUnset[boxes_from_points_task.RoiInfos] = _unset + self._roi_filenames: _MaybeUnset[boxes_from_points_task.RoiFilenames] = _unset + self._roi_filenames_to_be_annotated: _MaybeUnset[Sequence[str]] = _unset + self._gt_roi_filenames: _MaybeUnset[Sequence[str]] = _unset + + self._job_layout: _MaybeUnset[Sequence[Sequence[str]]] = _unset + "File lists per CVAT job" + + self._label_configuration: _MaybeUnset[Sequence[dict]] = _unset + + self._excluded_points_info: _MaybeUnset[_ExcludedAnnotationsInfo] = _unset + self._excluded_gt_info: _MaybeUnset[_ExcludedAnnotationsInfo] = _unset + + # Configuration / constants + # TODO: consider WebP if produced files are too big + self.roi_file_ext = ".png" # supposed to be lossless and reasonably compressing + "File extension for RoI images, with leading dot (.) included" + + self.list_display_threshold = 5 + "The maximum number of rendered list items in a message" + + self.roi_size_mult = 1.1 + "Additional point ROI size multiplier" + + self.min_roi_size = ( + Config.core_config.min_roi_size_w, + Config.core_config.min_roi_size_h, + ) + "Minimum absolute ROI size, (w, h)" + + self.points_format = "coco_person_keypoints" + + self.embed_point_in_roi_image = True + "Put a small point into the extracted RoI images for the original point" + + self.embedded_point_radius = 15 + self.min_embedded_point_radius_percent = 0.005 + self.max_embedded_point_radius_percent = 0.01 + self.embedded_point_color = (0, 255, 255) + self.roi_background_color = (245, 240, 242) # BGR - CVAT background color + + self.oracle_data_bucket = BucketAccessInfo.parse_obj(Config.storage_config) + + self.min_class_samples_for_roi_estimation = 25 + + self.max_class_roi_image_side_threshold = 0.5 + """ + The maximum allowed percent of the image for the estimated class RoI, + before the default RoI is used. Too big RoI estimations reduce the overall + prediction quality, making them unreliable. + """ + + self.max_discarded_threshold = 0.5 + """ + The maximum allowed percent of discarded + GT boxes, points, or samples for successful job launch + """ + + # TODO: probably, need to also add an absolute number of minimum GT RoIs + + def _download_input_data(self): + data_bucket = BucketAccessInfo.parse_obj(self.manifest.data.data_url) + gt_bucket = BucketAccessInfo.parse_obj(self.manifest.validation.gt_url) + points_bucket = BucketAccessInfo.parse_obj(self.manifest.data.points_url) + + data_storage_client = self._make_cloud_storage_client(data_bucket) + gt_storage_client = self._make_cloud_storage_client(gt_bucket) + points_storage_client = self._make_cloud_storage_client(points_bucket) + + data_filenames = data_storage_client.list_files(prefix=data_bucket.path) + data_filenames = strip_bucket_prefix(data_filenames, prefix=data_bucket.path) + self._data_filenames = filter_image_files(data_filenames) + + self._input_gt_data = gt_storage_client.download_file(gt_bucket.path) + self._input_gt_filename = Path(gt_bucket.path).name + + self._input_points_data = points_storage_client.download_file(points_bucket.path) + + def _parse_dataset(self, annotation_file_data: bytes, dataset_format: str) -> dm.Dataset: + temp_dir = self.exit_stack.enter_context(TemporaryDirectory()) + + annotation_filename = os.path.join(temp_dir, "annotations.json") + with open(annotation_filename, "wb") as f: + f.write(annotation_file_data) + + return dm.Dataset.import_from(annotation_filename, format=dataset_format) + + def _parse_gt(self): + assert self._input_gt_data is not _unset + + self._input_gt_dataset = self._parse_dataset( + self._input_gt_data, + dataset_format=DM_GT_DATASET_FORMAT_MAPPING[self.manifest.annotation.type], + ) + + def _parse_points(self): + assert self._input_points_data is not _unset + + self._points_dataset = self._parse_dataset( + self._input_points_data, dataset_format=self.points_format + ) + + def _validate_gt_labels(self): + gt_labels = set( + label.name + for label in self._input_gt_dataset.categories()[dm.AnnotationType.label] + if not label.parent + ) + manifest_labels = set(label.name for label in self.manifest.annotation.labels) + if gt_labels - manifest_labels: + raise DatasetValidationError( + "GT labels do not match job labels. Unknown labels: {}".format( + format_sequence(list(gt_labels - manifest_labels)), + ) + ) + + self._input_gt_dataset.transform( + ProjectLabels, dst_labels=[label.name for label in self.manifest.annotation.labels] + ) + self._input_gt_dataset.init_cache() + + def _validate_gt_filenames(self): + gt_filenames = set(s.id + s.media.ext for s in self._input_gt_dataset) + + known_data_filenames = set(self._data_filenames) + matched_gt_filenames = gt_filenames.intersection(known_data_filenames) + + if len(gt_filenames) != len(matched_gt_filenames): + extra_gt = list(map(os.path.basename, gt_filenames - matched_gt_filenames)) + + raise MismatchingAnnotations( + "Failed to find several validation samples in the dataset files: {}".format( + format_sequence(extra_gt) + ) + ) + + if len(gt_filenames) < self._job_val_frames_count: + raise TooFewSamples( + f"Too few validation samples provided ({len(gt_filenames)}), " + f"at least {self._job_val_frames_count} required." + ) + + def _validate_gt_annotations(self): + label_cat: dm.LabelCategories = self._input_gt_dataset.categories()[dm.AnnotationType.label] + + excluded_gt_info = _ExcludedAnnotationsInfo() + excluded_samples = set() + visited_ids = set() + for gt_sample in self._input_gt_dataset: + # Could fail on this as well + img_h, img_w = gt_sample.media_as(dm.Image).size + + sample_boxes = [a for a in gt_sample.annotations if isinstance(a, dm.Bbox)] + valid_boxes = [] + for bbox in sample_boxes: + if not ( + (0 <= int(bbox.x) < int(bbox.x + bbox.w) <= img_w) + and (0 <= int(bbox.y) < int(bbox.y + bbox.h) <= img_h) + ): + excluded_gt_info.add_message( + "Sample '{}': GT bbox #{} ({}) - invalid coordinates".format( + gt_sample.id, bbox.id, label_cat[bbox.label].name + ), + sample_id=gt_sample.id, + sample_subset=gt_sample.subset, + ) + continue + + if bbox.id in visited_ids: + excluded_gt_info.add_message( + "Sample '{}': GT bbox #{} ({}) skipped - repeated annotation id {}".format( + gt_sample.id, bbox.id, label_cat[bbox.label].name, bbox.id + ), + sample_id=gt_sample.id, + sample_subset=gt_sample.subset, + ) + continue + + valid_boxes.append(bbox) + + excluded_gt_info.excluded_count += len(sample_boxes) - len(valid_boxes) + excluded_gt_info.total_count += len(sample_boxes) + + if len(valid_boxes) != len(sample_boxes): + if not valid_boxes: + excluded_samples.add((gt_sample.id, gt_sample.subset)) + else: + self._input_gt_dataset.put(gt_sample.wrap(annotations=valid_boxes)) + + for excluded_sample in excluded_samples: + self._input_gt_dataset.remove(*excluded_sample) + + if excluded_gt_info.excluded_count: + self.logger.warning( + "Some GT boxes were excluded due to the errors found: \n{}".format( + format_sequence([e.message for e in excluded_gt_info.messages], separator="\n") + ) + ) + + if excluded_gt_info.excluded_count > ceil( + excluded_gt_info.total_count * self.max_discarded_threshold + ): + raise TooFewSamples( + "Too many GT boxes discarded, canceling job creation. Errors: {}".format( + format_sequence( + [error_info.message for error_info in excluded_gt_info.messages] + ) + ) + ) + + self._excluded_gt_info = excluded_gt_info + + def _validate_gt(self): + assert self._data_filenames is not _unset + assert self._input_gt_dataset is not _unset + + self._validate_gt_filenames() + self._validate_gt_labels() + self._validate_gt_annotations() + + def _validate_points_categories(self): + invalid_point_categories_messages = [] + points_dataset_categories = self._points_dataset.categories() + points_dataset_label_cat: dm.LabelCategories = points_dataset_categories[ + dm.AnnotationType.label + ] + for category_id, category in points_dataset_categories[ + dm.AnnotationType.points + ].items.items(): + if len(category.labels) != 1: + invalid_point_categories_messages.append( + "Category '{}' (#{}): {}".format( + points_dataset_label_cat[category_id].name, + category_id, + f"too many skeleton points ({len(category.labels)}), only 1 expected", + ) + ) + + if invalid_point_categories_messages: + raise InvalidCategories( + "Invalid categories in the input point annotations: {}".format( + format_sequence(invalid_point_categories_messages, separator="; ") + ) + ) + + points_labels = set(label.name for label in points_dataset_label_cat if not label.parent) + manifest_labels = set(label.name for label in self.manifest.annotation.labels) + if manifest_labels != points_labels: + raise DatasetValidationError("Point labels do not match job labels") + + self._points_dataset.transform( + ProjectLabels, dst_labels=[label.name for label in self.manifest.annotation.labels] + ) + self._points_dataset.init_cache() + + def _validate_points_filenames(self): + points_filenames = set(sample.id + sample.media.ext for sample in self._points_dataset) + + known_data_filenames = set(self._data_filenames) + matched_points_filenames = points_filenames.intersection(known_data_filenames) + + if len(matched_points_filenames) != len(points_filenames): + extra_point_samples = list( + map(os.path.basename, points_filenames - matched_points_filenames) + ) + + raise MismatchingAnnotations( + "Failed to find several samples in the dataset files: {}".format( + format_sequence(extra_point_samples), + ) + ) + + def _validate_points_annotations(self): + def _validate_skeleton(skeleton: dm.Skeleton, *, sample_bbox: dm.Bbox): + if skeleton.id in visited_ids: + raise DatasetValidationError(f"repeated annotation id ({skeleton.id})") + + if len(skeleton.elements) != 1: + raise DatasetValidationError( + f"invalid points count ({len(skeleton.elements)}), expected 1" + ) + + point = skeleton.elements[0] + px, py = point.points[:2] + if not is_point_in_bbox(px, py, sample_bbox): + raise InvalidCoordinates("coordinates are outside image") + + label_cat: dm.LabelCategories = self._points_dataset.categories()[dm.AnnotationType.label] + + excluded_points_info = _ExcludedAnnotationsInfo() + excluded_samples = set() + visited_ids = set() + for sample in self._points_dataset: + # Could fail on this as well + image_h, image_w = sample.image.size + sample_bbox = dm.Bbox(0, 0, w=image_w, h=image_h) + + sample_skeletons = [a for a in sample.annotations if isinstance(a, dm.Skeleton)] + valid_skeletons = [] + for skeleton in sample_skeletons: + # Here 1 skeleton describes 1 point + try: + _validate_skeleton(skeleton, sample_bbox=sample_bbox) + except InvalidCoordinates as error: + excluded_points_info.add_message( + "Sample '{}': point #{} ({}) skipped - {}".format( + sample.id, skeleton.id, label_cat[skeleton.label].name, error + ), + sample_id=sample.id, + sample_subset=sample.subset, + ) + continue + except DatasetValidationError as error: + excluded_points_info.add_message( + "Sample '{}': point #{} ({}) - {}".format( + sample.id, skeleton.id, label_cat[skeleton.label].name, error + ), + sample_id=sample.id, + sample_subset=sample.subset, + ) + continue + + valid_skeletons.append(skeleton) + + excluded_points_info.excluded_count += len(sample_skeletons) - len(valid_skeletons) + excluded_points_info.total_count += len(sample_skeletons) + + if len(valid_skeletons) != len(sample_skeletons): + if not valid_skeletons: + excluded_samples.add((sample.id, sample.subset)) + else: + self._points_dataset.put(sample.wrap(annotations=valid_skeletons)) + + for excluded_sample in excluded_samples: + self._points_dataset.remove(*excluded_sample) + + if excluded_points_info.excluded_count: + self.logger.warning( + "Some points were excluded due to the errors found: \n{}".format( + format_sequence( + [e.message for e in excluded_points_info.messages], separator="\n" + ) + ) + ) + + if excluded_points_info.excluded_count > ceil( + excluded_points_info.total_count * self.max_discarded_threshold + ): + raise TooFewSamples( + "Too many points discarded, canceling job creation. Errors: {}".format( + format_sequence( + [error_info.message for error_info in excluded_points_info.messages] + ) + ) + ) + + self._excluded_points_info = excluded_points_info + + def _validate_points(self): + assert self._data_filenames is not _unset + assert self._points_dataset is not _unset + + self._validate_points_categories() + self._validate_points_filenames() + self._validate_points_annotations() + + @staticmethod + def _is_point_in_bbox(px: float, py: float, bbox: dm.Bbox) -> bool: + return is_point_in_bbox(px, py, bbox) + + def _prepare_gt(self): + def _find_unambiguous_matches( + input_skeletons: list[dm.Skeleton], + gt_boxes: list[dm.Bbox], + ) -> list[tuple[dm.Skeleton, dm.Bbox]]: + matches = [ + [ + (input_skeleton.label == gt_bbox.label) + and ( + self._is_point_in_bbox( + *input_skeleton.elements[0].points[0:2], bbox=gt_bbox + ) + ) + for gt_bbox in gt_boxes + ] + for input_skeleton in input_skeletons + ] + + ambiguous_boxes: list[int] = set() + ambiguous_skeletons: list[int] = set() + for skeleton_idx, input_skeleton in enumerate(input_skeletons): + matched_boxes: list[dm.Bbox] = [ + gt_boxes[j] for j in range(len(gt_boxes)) if matches[skeleton_idx][j] + ] + + if len(matched_boxes) > 1: + # Handle ambiguous matches + excluded_points_info.add_message( + "Sample '{}': point #{} ({}) and overlapping boxes skipped - " + "too many matching boxes ({}) found".format( + points_sample.id, + input_skeleton.id, + points_label_cat[input_skeleton.label].name, + format_sequence([f"#{a.id}" for a in matched_boxes]), + ), + sample_id=points_sample.id, + sample_subset=points_sample.subset, + ) + # not an error, should not be counted as excluded for an error + ambiguous_skeletons.add(input_skeleton.id) + ambiguous_boxes.update(a.id for a in matched_boxes) + continue + + for gt_idx, gt_bbox in enumerate(gt_boxes): + matched_skeletons: list[dm.Skeleton] = [ + input_skeletons[i] for i in range(len(input_skeletons)) if matches[i][gt_idx] + ] + + if len(matched_skeletons) > 1: + # Handle ambiguous matches + excluded_gt_info.add_message( + "Sample '{}': GT bbox #{} ({}) and overlapping points skipped - " + "too many matching points ({}) found".format( + gt_sample.id, + gt_bbox.id, + gt_label_cat[gt_bbox.label].name, + format_sequence([f"#{a.id}" for a in matched_skeletons]), + ), + sample_id=gt_sample.id, + sample_subset=gt_sample.subset, + ) + # not an error, should not be counted as excluded for an error + ambiguous_boxes.add(gt_bbox.id) + ambiguous_skeletons.update(a.id for a in matched_skeletons) + continue + if not matched_skeletons: + # Handle unmatched skeletons + excluded_gt_info.add_message( + "Sample '{}': GT bbox #{} ({}) skipped - no matching points found".format( + gt_sample.id, + gt_bbox.id, + gt_label_cat[gt_bbox.label].name, + ), + sample_id=gt_sample.id, + sample_subset=gt_sample.subset, + ) + excluded_gt_info.excluded_count += 1 # an error + continue + + unambiguous_matches: list[tuple[dm.Bbox, dm.Skeleton]] = [] + for skeleton_idx, input_skeleton in enumerate(input_skeletons): + if input_skeleton.id in ambiguous_skeletons: + continue + + matched_bbox = None + for gt_idx, gt_bbox in enumerate(gt_boxes): + if gt_bbox.id in ambiguous_boxes: + continue + + if matches[skeleton_idx][gt_idx]: + matched_bbox = gt_bbox + break + + if matched_bbox: + unambiguous_matches.append((input_skeleton, matched_bbox)) + + return unambiguous_matches + + def _find_good_gt_boxes( + input_skeletons: list[dm.Skeleton], + gt_boxes: list[dm.Bbox], + ) -> list[dm.Bbox]: + matches = _find_unambiguous_matches(input_skeletons, gt_boxes) + + matched_boxes = [] + for input_skeleton, gt_bbox in matches: + gt_count_per_class[gt_bbox.label] = gt_count_per_class.get(gt_bbox.label, 0) + 1 + + matched_boxes.append(gt_bbox) + bbox_point_mapping[gt_bbox.id] = input_skeleton.id + + return matched_boxes + + assert self._data_filenames is not _unset + assert self._points_dataset is not _unset + assert self._input_gt_dataset is not _unset + assert [ + label.name for label in self._input_gt_dataset.categories()[dm.AnnotationType.label] + ] == [label.name for label in self.manifest.annotation.labels] + assert [ + label.name + for label in self._points_dataset.categories()[dm.AnnotationType.label] + if not label.parent + ] == [label.name for label in self.manifest.annotation.labels] + + points_label_cat: dm.LabelCategories = self._points_dataset.categories()[ + dm.AnnotationType.label + ] + gt_label_cat: dm.LabelCategories = self._input_gt_dataset.categories()[ + dm.AnnotationType.label + ] + + updated_gt_dataset = dm.Dataset( + categories=self._input_gt_dataset.categories(), media_type=dm.Image + ) + + excluded_points_info = _ExcludedAnnotationsInfo() # local for the function + excluded_gt_info = self._excluded_gt_info + gt_count_per_class = {} + bbox_point_mapping = {} # bbox id -> point id + for gt_sample in self._input_gt_dataset: + points_sample = self._points_dataset.get(gt_sample.id, gt_sample.subset) + assert points_sample + + gt_boxes = [a for a in gt_sample.annotations if isinstance(a, dm.Bbox)] + input_skeletons = [a for a in points_sample.annotations if isinstance(a, dm.Skeleton)] + + # Samples without boxes are allowed, so we just skip them without an error + if not gt_boxes: + continue + + matched_boxes = _find_good_gt_boxes(input_skeletons, gt_boxes) + if not matched_boxes: + continue + + updated_gt_dataset.put(gt_sample.wrap(annotations=matched_boxes)) + + if excluded_points_info.messages: + self.logger.warning( + "Some points were excluded from GT due to the problems found: \n{}".format( + format_sequence( + [e.message for e in excluded_points_info.messages], separator="\n" + ) + ) + ) + + if excluded_gt_info.messages: + self.logger.warning( + "Some GT annotations were excluded due to the problems found: \n{}".format( + format_sequence([e.message for e in excluded_gt_info.messages], separator="\n") + ) + ) + + if excluded_gt_info.excluded_count > ceil( + excluded_gt_info.total_count * self.max_discarded_threshold + ): + raise DatasetValidationError( + "Too many GT boxes discarded ({} out of {}). " + "Please make sure each GT box matches exactly 1 point".format( + excluded_gt_info.total_count - len(bbox_point_mapping), + excluded_gt_info.total_count, + ) + ) + + self.logger.info( + "GT counts per class to be used for validation: {}".format( + format_sequence( + [ + f"{gt_label_cat[label_id].name}: {count}" + for label_id, count in gt_count_per_class.items() + ] + ) + ) + ) + + gt_labels_without_anns = [ + gt_label_cat[label_id] + for label_id, label_count in gt_count_per_class.items() + if not label_count + ] + if gt_labels_without_anns: + raise DatasetValidationError( + "No matching GT boxes/points annotations found for some classes: {}".format( + format_sequence(gt_labels_without_anns) + ) + ) + + self._gt_dataset = updated_gt_dataset + self._bbox_point_mapping = bbox_point_mapping + + def _estimate_roi_sizes(self): + assert self._gt_dataset is not _unset + assert [label.name for label in self._gt_dataset.categories()[dm.AnnotationType.label]] == [ + label.name for label in self.manifest.annotation.labels + ] + + bbox_sizes_per_label = {} + for sample in self._gt_dataset: + image_h, image_w = self._points_dataset.get(sample.id, sample.subset).image.size + + for gt_bbox in sample.annotations: + gt_bbox = cast("dm.Bbox", gt_bbox) + bbox_sizes_per_label.setdefault(gt_bbox.label, []).append( + ( + gt_bbox.w / image_w, + gt_bbox.h / image_h, + ) + ) + + # Consider bbox sides as normally-distributed random variables, estimate max + # For big enough datasets, it should be reasonable approximation + # (due to the central limit theorem). This can work bad for small datasets, + # so we only do this if there are enough class samples. + classes_with_default_roi: dict[int, str] = {} # label_id -> reason + roi_size_estimations_per_label = {} # label id -> (w, h) + default_roi_size = (2, 2) # 2 will yield just the image size after halving + for label_id, label_sizes in bbox_sizes_per_label.items(): + if len(label_sizes) < self.min_class_samples_for_roi_estimation: + estimated_size = default_roi_size + classes_with_default_roi[label_id] = "too few GT provided" + else: + max_bbox = np.max(label_sizes, axis=0) + if np.any(max_bbox > self.max_class_roi_image_side_threshold): + estimated_size = default_roi_size + classes_with_default_roi[label_id] = "estimated RoI is unreliable" + else: + estimated_size = 2 * max_bbox * self.roi_size_mult + + roi_size_estimations_per_label[label_id] = estimated_size + + if classes_with_default_roi: + label_cat = self._gt_dataset.categories()[dm.AnnotationType.label] + labels_by_reason = { + g_reason: [v[0] for v in g_items] + for g_reason, g_items in groupby( + sorted(classes_with_default_roi.items(), key=lambda v: v[1]), key=lambda v: v[1] + ) + } + self.logger.warning( + "Some classes will use the full image instead of RoI - {}".format( + "; ".join( + "{}: {}".format( + g_reason, + format_sequence([label_cat[label_id].name for label_id in g_labels]), + ) + for g_reason, g_labels in labels_by_reason.items() + ) + ) + ) + + self._roi_size_estimations = roi_size_estimations_per_label + + def _prepare_roi_info(self): + assert self._gt_dataset is not _unset + assert self._roi_size_estimations is not _unset + assert self._points_dataset is not _unset + + rois: list[boxes_from_points_task.RoiInfo] = [] + for sample in self._points_dataset: + for skeleton in sample.annotations: + if not isinstance(skeleton, dm.Skeleton): + continue + + point_label_id = skeleton.label + original_point_x, original_point_y = skeleton.elements[0].points[:2] + original_point_x = int(original_point_x) + original_point_y = int(original_point_y) + + image_h, image_w = sample.image.size + + roi_est_w, roi_est_h = self._roi_size_estimations[point_label_id] + roi_est_w *= image_w + roi_est_h *= image_h + roi_est_w = max(roi_est_w, self.min_roi_size[0]) + roi_est_h = max(roi_est_h, self.min_roi_size[1]) + + roi_left = max(0, original_point_x - int(roi_est_w / 2)) + roi_top = max(0, original_point_y - int(roi_est_h / 2)) + roi_right = min(image_w, original_point_x + ceil(roi_est_w / 2)) + roi_bottom = min(image_h, original_point_y + ceil(roi_est_h / 2)) + + roi_w = roi_right - roi_left + roi_h = roi_bottom - roi_top + + new_point_x = original_point_x - roi_left + new_point_y = original_point_y - roi_top + + rois.append( + boxes_from_points_task.RoiInfo( + point_id=skeleton.id, + original_image_key=sample.attributes["id"], + point_x=new_point_x, + point_y=new_point_y, + roi_x=roi_left, + roi_y=roi_top, + roi_w=roi_w, + roi_h=roi_h, + ) + ) + + self._rois = rois + + def _mangle_filenames(self): + """ + Mangle filenames in the dataset to make them less recognizable by annotators + and hide private dataset info + """ + assert self._rois is not _unset + + # TODO: maybe add different names for the same GT images in + # different jobs to make them even less recognizable + self._roi_filenames = { + roi.point_id: str(uuid.uuid4()) + self.roi_file_ext for roi in self._rois + } + + def _prepare_job_layout(self): + assert self._rois is not _unset + assert self._bbox_point_mapping is not _unset + assert self._input_gt_dataset is not _unset + + # This list can be different from what is selected for validation + input_gt_filenames = set(sample.media.path for sample in self._input_gt_dataset) + original_image_id_to_filename = { + sample.attributes["id"]: sample.media.path for sample in self._points_dataset + } + point_id_to_original_image_id = {roi.point_id: roi.original_image_key for roi in self._rois} + + gt_point_ids = set(self._bbox_point_mapping.values()) + self._gt_roi_filenames = [self._roi_filenames[point_id] for point_id in gt_point_ids] + self._roi_filenames_to_be_annotated = [ + fn + for point_id, fn in self._roi_filenames.items() + if point_id not in gt_point_ids + if original_image_id_to_filename[point_id_to_original_image_id[point_id]] + not in input_gt_filenames + ] + + def _prepare_label_configuration(self): + self._label_configuration = make_label_configuration(self.manifest) + + def _upload_task_meta(self): + layout = boxes_from_points_task.TaskMetaLayout() + serializer = boxes_from_points_task.TaskMetaSerializer() + + file_list = [] + file_list.append((self._input_points_data, layout.POINTS_FILENAME)) + file_list.append( + ( + serializer.serialize_gt_annotations(self._gt_dataset), + layout.GT_FILENAME, + ) + ) + file_list.append( + ( + serializer.serialize_bbox_point_mapping(self._bbox_point_mapping), + layout.BBOX_POINT_MAPPING_FILENAME, + ) + ) + file_list.append((serializer.serialize_roi_info(self._rois), layout.ROI_INFO_FILENAME)) + file_list.append( + (serializer.serialize_roi_filenames(self._roi_filenames), layout.ROI_FILENAMES_FILENAME) + ) + + storage_client = self._make_cloud_storage_client(self.oracle_data_bucket) + for file_data, filename in file_list: + storage_client.create_file( + compose_data_bucket_filename(self.escrow_address, self.chain_id, filename), + file_data, + ) + + def _extract_roi( + self, source_pixels: np.ndarray, roi_info: boxes_from_points_task.RoiInfo + ) -> np.ndarray: + img_h, img_w, *_ = source_pixels.shape + + roi_pixels = source_pixels[ + max(0, roi_info.roi_y) : min(img_h, roi_info.roi_y + roi_info.roi_h), + max(0, roi_info.roi_x) : min(img_w, roi_info.roi_x + roi_info.roi_w), + ] + + if not ( + (0 <= roi_info.roi_x < roi_info.roi_x + roi_info.roi_w < img_w) + and (0 <= roi_info.roi_y < roi_info.roi_y + roi_info.roi_h < img_h) + ): + # Coords can be outside the original image + # In this case a border should be added to RoI, so that the image was centered on bbox + wrapped_roi_pixels = np.zeros((roi_info.roi_h, roi_info.roi_w, 3), dtype=np.float32) + wrapped_roi_pixels[:, :] = self.roi_background_color + + dst_y = max(-roi_info.roi_y, 0) + dst_x = max(-roi_info.roi_x, 0) + wrapped_roi_pixels[ + dst_y : dst_y + roi_pixels.shape[0], + dst_x : dst_x + roi_pixels.shape[1], + ] = roi_pixels + + roi_pixels = wrapped_roi_pixels + else: + roi_pixels = roi_pixels.copy() + + return roi_pixels + + def _draw_roi_point( + self, roi_pixels: np.ndarray, roi_info: boxes_from_points_task.RoiInfo + ) -> np.ndarray: + center = (roi_info.point_x, roi_info.point_y) + + roi_r = (roi_info.roi_w**2 + roi_info.roi_h**2) ** 0.5 / 2 + point_size = int( + min( + self.max_embedded_point_radius_percent * roi_r, + max(self.embedded_point_radius, self.min_embedded_point_radius_percent * roi_r), + ) + ) + + roi_pixels = roi_pixels.copy() + roi_pixels = cv2.circle( + roi_pixels, + center, + point_size + 1, + (255, 255, 255), + cv2.FILLED, + ) + return cv2.circle( + roi_pixels, + center, + point_size, + self.embedded_point_color, + cv2.FILLED, + ) + + def _extract_and_upload_rois(self): + assert self._points_dataset is not _unset + assert self._rois is not _unset + assert self._data_filenames is not _unset + assert self._roi_filenames is not _unset + + src_bucket = BucketAccessInfo.parse_obj(self.manifest.data.data_url) + src_prefix = src_bucket.path + dst_bucket = self.oracle_data_bucket + + src_client = self._make_cloud_storage_client(src_bucket) + dst_client = self._make_cloud_storage_client(dst_bucket) + + image_id_to_filename = { + sample.attributes["id"]: sample.image.path for sample in self._points_dataset + } + + filename_to_sample = {sample.image.path: sample for sample in self._points_dataset} + + def _roi_key(e): + return e.original_image_key + + rois_by_image: dict[str, Sequence[boxes_from_points_task.RoiInfo]] = { + image_id_to_filename[image_id]: list(g) + for image_id, g in groupby(sorted(self._rois, key=_roi_key), key=_roi_key) + } + + def process_file(filename: str, image_pixels: np.ndarray): + image_roi_infos = rois_by_image.get(filename, []) + if not image_roi_infos: + return + + sample = filename_to_sample[filename] + if tuple(sample.image.size) != tuple(image_pixels.shape[:2]): + # TODO: maybe rois should be regenerated instead + # Option 2: accumulate errors, fail when some threshold is reached + # Option 3: add special handling for cases when image is only rotated (exif etc.) + raise InvalidImageInfo( + f"Sample '{filename}': invalid size provided in the point annotations" + ) + + image_rois = {} + for roi_info in image_roi_infos: + roi_pixels = self._extract_roi(image_pixels, roi_info) + + if self.embed_point_in_roi_image: + roi_pixels = self._draw_roi_point(roi_pixels, roi_info) + + roi_filename = self._roi_filenames[roi_info.point_id] + roi_bytes = encode_image(roi_pixels, os.path.splitext(roi_filename)[-1]) + + image_rois[roi_filename] = roi_bytes + + for roi_filename, roi_bytes in image_rois.items(): + dst_client.create_file( + compose_data_bucket_filename(self.escrow_address, self.chain_id, roi_filename), + roi_bytes, + ) + + def download_and_decode(key: str): + image_bytes = src_client.download_file(key) + return decode_image(image_bytes) + + pool_size = Config.features.max_data_storage_connections + download_queue_size = 4 * pool_size + download_queue = Queue[tuple[str, Future[np.ndarray]]](download_queue_size) + roi_uploader = BufferedRoiImageUploader(queue=download_queue) + with ThreadPoolExecutor(pool_size) as pool: + + def put_callback(filename: str): + image_roi_infos = rois_by_image.get(filename, []) + if not image_roi_infos: + return None + + return ( + filename, + pool.submit(download_and_decode, os.path.join(src_prefix, filename)), + ) + + def process_callback(result: tuple[str, Future]): + filename, task = result + process_file(filename, task.result()) + + roi_uploader.process_all( + self._data_filenames, put_callback=put_callback, process_callback=process_callback + ) + + def _prepare_gt_roi_dataset(self): + self._gt_roi_dataset = dm.Dataset( + categories=self._gt_dataset.categories(), media_type=dm.Image + ) + + roi_info_by_point_id: dict[int, skeletons_from_boxes_task.RoiInfo] = { + roi_info.point_id: roi_info for roi_info in self._rois + } + + for sample in self._gt_dataset: + for gt_bbox in sample.annotations: + assert isinstance(gt_bbox, dm.Bbox) + + point_id = self._bbox_point_mapping[gt_bbox.id] + gt_roi_filename = compose_data_bucket_filename( + self.escrow_address, self.chain_id, self._roi_filenames[point_id] + ) + + # update gt bbox coordinates to match RoI shift + roi_info = roi_info_by_point_id[point_id] + new_x = gt_bbox.points[0] - roi_info.roi_x + new_y = gt_bbox.points[1] - roi_info.roi_y + + self._gt_roi_dataset.put( + sample.wrap( + id=os.path.splitext(gt_roi_filename)[0], + annotations=[gt_bbox.wrap(x=new_x, y=new_y)], + media=dm.Image(path=gt_roi_filename, size=sample.media_as(dm.Image).size), + attributes=filter_dict(sample.attributes, exclude_keys=["id"]), + ) + ) + + assert len(self._gt_roi_dataset) == len(self._gt_roi_filenames) + + def _create_on_cvat(self): + assert self._roi_filenames_to_be_annotated is not _unset + assert self._gt_roi_filenames is not _unset + assert self._label_configuration is not _unset + assert self._gt_roi_dataset is not _unset + + oracle_bucket = self.oracle_data_bucket + + # Register cloud storage on CVAT to pass user dataset + cvat_cloud_storage = cvat_api.create_cloudstorage( + **_make_cvat_cloud_storage_params(oracle_bucket) + ) + + # Create a project + cvat_project = cvat_api.create_project( + self.escrow_address, + labels=self._label_configuration, + user_guide=self.manifest.annotation.user_guide, + ) + + # Setup webhooks for the project + cvat_webhook = cvat_api.create_cvat_webhook(cvat_project.id) + + with SessionLocal.begin() as session: + segment_size = self._task_segment_size + total_jobs = math.ceil(len(self._roi_filenames_to_be_annotated) / segment_size) + self.logger.info( + "Task creation for escrow '%s': will create %s assignments", + self.escrow_address, + total_jobs, + ) + db_service.create_escrow_creation( + session, + escrow_address=self.escrow_address, + chain_id=self.chain_id, + total_jobs=total_jobs, + ) + + project_id = db_service.create_project( + session, + cvat_project.id, + cvat_cloud_storage.id, + self.manifest.annotation.type, + self.escrow_address, + self.chain_id, + oracle_bucket.to_url().rstrip("/") + + "/" + + compose_data_bucket_prefix(self.escrow_address, self.chain_id), + cvat_webhook_id=cvat_webhook.id, + ) + db_service.get_project_by_id(session, project_id, for_update=True) # lock the row + db_service.add_project_images( + session, + cvat_project.id, + [ + compose_data_bucket_filename(self.escrow_address, self.chain_id, fn) + for fn in self._roi_filenames.values() + ], + ) + + for data_subset in self._split_dataset_per_task( + self._roi_filenames_to_be_annotated, + subset_size=Config.cvat_config.max_jobs_per_task * segment_size, + ): + cvat_task = cvat_api.create_task( + cvat_project.id, self.escrow_address, segment_size=segment_size + ) + + task_id = db_service.create_task( + session, cvat_task.id, cvat_project.id, TaskStatuses[cvat_task.status] + ) + db_service.get_task_by_id(session, task_id, for_update=True) # lock the row + + filenames = [ + compose_data_bucket_filename(self.escrow_address, self.chain_id, fn) + for fn in data_subset + ] + gt_filenames = [ + compose_data_bucket_filename(self.escrow_address, self.chain_id, fn) + for fn in self._gt_roi_filenames + ] + + cvat_api.put_task_data( + cvat_task.id, + cvat_cloud_storage.id, + filenames=filenames, + validation_params={ + "gt_filenames": gt_filenames, + "gt_frames_per_job_count": self._job_val_frames_count, + }, + ) + + self._setup_gt_job_for_cvat_task( + cvat_task.id, self._gt_roi_dataset, dm_export_format="coco" + ) + self._setup_quality_settings(cvat_task.id) + + db_service.create_data_upload(session, cvat_task.id) + + db_service.touch(session, Project, [project_id]) + + def build(self): + self._download_input_data() + self._parse_gt() + self._parse_points() + self._validate_gt() + self._validate_points() + + # Task configuration creation + self._prepare_gt() + self._estimate_roi_sizes() + self._prepare_roi_info() + self._mangle_filenames() + self._prepare_label_configuration() + self._prepare_job_layout() + self._prepare_gt_roi_dataset() + + # Data preparation + self._extract_and_upload_rois() + self._upload_task_meta() + + self._create_on_cvat() diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/skeletons_from_boxes.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/skeletons_from_boxes.py new file mode 100644 index 0000000000..042429442b --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/skeletons_from_boxes.py @@ -0,0 +1,1404 @@ +from __future__ import annotations + +import math +import os +import random +import uuid +from concurrent.futures import Future, ThreadPoolExecutor +from dataclasses import dataclass +from itertools import chain, groupby +from math import ceil +from queue import Queue +from tempfile import TemporaryDirectory +from typing import TYPE_CHECKING + +import cv2 +import datumaro as dm +import numpy as np +from datumaro.util import take_by +from datumaro.util.annotation_util import BboxCoords, bbox_iou, find_instances +from datumaro.util.image import decode_image, encode_image + +import src.core.tasks.skeletons_from_boxes as skeletons_from_boxes_task +import src.cvat.api_calls as cvat_api +import src.services.cvat as db_service +from src.core.config import Config +from src.core.storage import compose_data_bucket_filename, compose_data_bucket_prefix +from src.core.types import TaskStatuses +from src.db import SessionLocal +from src.models.cvat import Project +from src.services.cloud.utils import BucketAccessInfo +from src.utils.annotations import InstanceSegmentsToBbox, ProjectLabels, is_point_in_bbox +from src.utils.logging import format_sequence +from src.utils.roi_uploader import BufferedRoiImageUploader + +if TYPE_CHECKING: + from collections.abc import Sequence + from pathlib import Path + + from src.core.manifest import TaskManifest + +from src.core.tasks.cvat_formats import DM_GT_DATASET_FORMAT_MAPPING +from src.handlers.job_creation.builders.vision.base import _TaskBuilderBase +from src.handlers.job_creation.exceptions import ( + DatasetValidationError, + InvalidCoordinates, + InvalidImageInfo, + MismatchingAnnotations, + TooFewSamples, +) +from src.handlers.job_creation.utils import ( + _ExcludedAnnotationsInfo, + _make_cvat_cloud_storage_params, + _MaybeUnset, + _unset, + filter_image_files, + strip_bucket_prefix, +) + + +class SkeletonsFromBoxesTaskBuilder(_TaskBuilderBase): + @dataclass + class _TaskParams: + label_id: int + roi_ids: list[int] + roi_gt_ids: list[int] + + def __init__(self, manifest: TaskManifest, escrow_address: str, chain_id: int) -> None: + super().__init__(manifest=manifest, escrow_address=escrow_address, chain_id=chain_id) + + self._input_gt_data: _MaybeUnset[bytes] = _unset + self._input_boxes_data: _MaybeUnset[bytes] = _unset + + self._data_filenames: _MaybeUnset[Sequence[str]] = _unset + self._input_gt_dataset: _MaybeUnset[dm.Dataset] = _unset + self._gt_dataset: _MaybeUnset[dm.Dataset] = _unset + self._boxes_dataset: _MaybeUnset[dm.Dataset] = _unset + + self._skeleton_bbox_mapping: _MaybeUnset[skeletons_from_boxes_task.SkeletonBboxMapping] = ( + _unset + ) + self._roi_infos: _MaybeUnset[skeletons_from_boxes_task.RoiInfos] = _unset + self._roi_info_by_id: _MaybeUnset[dict[int, skeletons_from_boxes_task.RoiInfo]] = _unset + + self._gt_points_per_label: _MaybeUnset[ + dict[tuple[int, str], Sequence[tuple[int, dm.Points]]] + ] = _unset + "(skeleton_label_id, point_label_name) to [(skeleton_id, point), ...]" + + self._roi_filenames: _MaybeUnset[dict[int, str]] = _unset + + self._task_params: _MaybeUnset[list[self._TaskParams]] = _unset + + self._excluded_gt_info: _MaybeUnset[_ExcludedAnnotationsInfo] = _unset + self._excluded_boxes_info: _MaybeUnset[_ExcludedAnnotationsInfo] = _unset + + # Configuration / constants + self.job_size_mult = skeletons_from_boxes_task.DEFAULT_ASSIGNMENT_SIZE_MULTIPLIER + "Job size multiplier" + + # TODO: consider WebP if produced files are too big + self.roi_file_ext = ".png" # supposed to be lossless and reasonably compressing + "File extension for RoI images, with leading dot (.) included" + + self.list_display_threshold = 5 + "The maximum number of rendered list items in a message" + + self.roi_size_mult = 1.1 + "Additional point ROI size multiplier" + + self.min_roi_size = ( + Config.core_config.min_roi_size_w, + Config.core_config.min_roi_size_h, + ) + "Minimum absolute ROI size, (w, h)" + + self.boxes_format = "coco_person_keypoints" + + self.embed_bbox_in_roi_image = True + "Put a bbox into the extracted skeleton RoI images" + + self.embed_tile_border = True + + self.embedded_point_radius = 15 + self.min_embedded_point_radius_percent = 0.005 + self.max_embedded_point_radius_percent = 0.01 + self.embedded_point_color = (0, 255, 255) + + self.roi_embedded_bbox_color = (0, 255, 255) # BGR + self.roi_background_color = (245, 240, 242) # BGR - CVAT background color + + self.oracle_data_bucket = BucketAccessInfo.parse_obj(Config.storage_config) + + self.min_label_gt_samples = 2 # TODO: find good threshold + + self.max_discarded_threshold = 0.5 + """ + The maximum allowed percent of discarded + GT annotations or samples for successful job launch + """ + + self.gt_id_attribute = "object_id" + "An additional way to match GT skeletons with input boxes" + + # TODO: probably, need to also add an absolute number of minimum GT RoIs per class + + def _download_input_data(self): + data_bucket = BucketAccessInfo.parse_obj(self.manifest.data.data_url) + gt_bucket = BucketAccessInfo.parse_obj(self.manifest.validation.gt_url) + boxes_bucket = BucketAccessInfo.parse_obj(self.manifest.data.boxes_url) + + data_storage_client = self._make_cloud_storage_client(data_bucket) + gt_storage_client = self._make_cloud_storage_client(gt_bucket) + boxes_storage_client = self._make_cloud_storage_client(boxes_bucket) + + data_filenames = data_storage_client.list_files(prefix=data_bucket.path) + data_filenames = strip_bucket_prefix(data_filenames, prefix=data_bucket.path) + self._data_filenames = filter_image_files(data_filenames) + + self._input_gt_data = gt_storage_client.download_file(gt_bucket.path) + + self._input_boxes_data = boxes_storage_client.download_file(boxes_bucket.path) + + def _parse_dataset(self, annotation_file_data: bytes, dataset_format: str) -> dm.Dataset: + temp_dir = self.exit_stack.enter_context(TemporaryDirectory()) + + annotation_filename = os.path.join(temp_dir, "annotations.json") + with open(annotation_filename, "wb") as f: + f.write(annotation_file_data) + + return dm.Dataset.import_from(annotation_filename, format=dataset_format) + + def _parse_gt(self): + assert self._input_gt_data is not _unset + + self._input_gt_dataset = self._parse_dataset( + self._input_gt_data, + dataset_format=DM_GT_DATASET_FORMAT_MAPPING[self.manifest.annotation.type], + ) + + def _parse_boxes(self): + assert self._input_boxes_data is not _unset + + self._boxes_dataset = self._parse_dataset( + self._input_boxes_data, dataset_format=self.boxes_format + ) + + def _validate_gt_labels(self): + gt_labels = set( + (label.name, label.parent) + for label in self._input_gt_dataset.categories()[dm.AnnotationType.label] + ) + + manifest_labels = set() + for skeleton_label in self.manifest.annotation.labels: + manifest_labels.add((skeleton_label.name, "")) + for node_label in skeleton_label.nodes: + manifest_labels.add((node_label, skeleton_label.name)) + + if manifest_labels - gt_labels: + raise DatasetValidationError( + "Could not find GT for labels {}".format( + format_sequence( + [ + label_name if not parent_name else f"{parent_name}.{label_name}" + for label_name, parent_name in manifest_labels - gt_labels + ] + ), + ) + ) + + # It should not be an issue that there are some extra GT labels - they should + # just be skipped. + if gt_labels - manifest_labels: + self.logger.info( + "Skipping unknown GT labels: {}".format( + format_sequence( + [ + label_name if not parent_name else f"{parent_name}.{label_name}" + for label_name, parent_name in gt_labels - manifest_labels + ] + ) + ) + ) + + # Reorder and filter labels to match the manifest + self._input_gt_dataset.transform( + ProjectLabels, dst_labels=[label.name for label in self.manifest.annotation.labels] + ) + self._input_gt_dataset.init_cache() + + def _validate_gt_filenames(self): + gt_filenames = set(s.id + s.media.ext for s in self._input_gt_dataset) + + known_data_filenames = set(self._data_filenames) + matched_gt_filenames = gt_filenames.intersection(known_data_filenames) + + if len(gt_filenames) != len(matched_gt_filenames): + extra_gt = list(map(os.path.basename, gt_filenames - matched_gt_filenames)) + + raise MismatchingAnnotations( + "Failed to find several validation samples in the dataset files: {}".format( + format_sequence(extra_gt) + ) + ) + + if len(gt_filenames) < self._job_val_frames_count: + raise TooFewSamples( + f"Too few validation samples provided ({len(gt_filenames)}), " + f"at least {self._job_val_frames_count} required." + ) + + def _validate_gt_annotations(self): + def _validate_skeleton(skeleton: dm.Skeleton, *, sample_bbox: dm.Bbox): + if skeleton.id in visited_ids: + raise DatasetValidationError(f"repeated annotation id {skeleton.id}") + + for element in skeleton.elements: + # This is what Datumaro is expected to parse + assert len(element.points) == 2 + assert len(element.visibility) == 1 + + if element.visibility[0] == dm.Points.Visibility.absent: + continue + + px, py = element.points[:2] + if not is_point_in_bbox(int(px), int(py), sample_bbox): + raise InvalidCoordinates("skeleton point is outside the image") + + label_cat: dm.LabelCategories = self._input_gt_dataset.categories()[dm.AnnotationType.label] + + excluded_gt_info = _ExcludedAnnotationsInfo() + excluded_samples = set() + visited_ids = set() + for gt_sample in self._input_gt_dataset: + # Could fail on this as well + img_h, img_w = gt_sample.media_as(dm.Image).size + sample_bbox = dm.Bbox(0, 0, w=img_w, h=img_h) + + sample_skeletons = [a for a in gt_sample.annotations if isinstance(a, dm.Skeleton)] + valid_skeletons = [] + for skeleton in sample_skeletons: + try: + _validate_skeleton(skeleton, sample_bbox=sample_bbox) + except DatasetValidationError as error: + excluded_gt_info.add_message( + "Sample '{}': GT skeleton #{} ({}) skipped - {}".format( + gt_sample.id, skeleton.id, label_cat[skeleton.label].name, error + ), + sample_id=gt_sample.id, + sample_subset=gt_sample.subset, + ) + continue + + valid_skeletons.append(skeleton) + visited_ids.add(skeleton.id) + + excluded_gt_info.excluded_count += len(sample_skeletons) - len(valid_skeletons) + excluded_gt_info.total_count += len(sample_skeletons) + + if len(valid_skeletons) != len(sample_skeletons): + if not valid_skeletons: + excluded_samples.add((gt_sample.id, gt_sample.subset)) + else: + # Skeleton boxes can be in the list as well with the same ids / groups + skeleton_ids = set(a.id for a in valid_skeletons) - {0} + self._input_gt_dataset.put( + gt_sample.wrap( + annotations=[a for a in gt_sample.annotations if a.id in skeleton_ids] + ) + ) + + for excluded_sample in excluded_samples: + self._input_gt_dataset.remove(*excluded_sample) + + if excluded_gt_info.excluded_count: + self.logger.warning( + "Some GT skeletons were excluded due to the errors found: {}".format( + format_sequence([e.message for e in excluded_gt_info.messages], separator="\n") + ) + ) + + if excluded_gt_info.excluded_count > ceil( + excluded_gt_info.total_count * self.max_discarded_threshold + ): + raise TooFewSamples( + "Too many GT skeletons discarded, canceling job creation. Errors: {}".format( + format_sequence( + [error_info.message for error_info in excluded_gt_info.messages] + ) + ) + ) + + self._excluded_gt_info = excluded_gt_info + + def _validate_gt(self): + assert self._data_filenames is not _unset + assert self._input_gt_dataset is not _unset + + self._validate_gt_filenames() + self._validate_gt_labels() + self._validate_gt_annotations() + + def _validate_boxes_categories(self): + boxes_dataset_categories = self._boxes_dataset.categories() + boxes_dataset_label_cat: dm.LabelCategories = boxes_dataset_categories[ + dm.AnnotationType.label + ] + + boxes_labels = set(label.name for label in boxes_dataset_label_cat if not label.parent) + manifest_labels = set(label.name for label in self.manifest.annotation.labels) + if manifest_labels != boxes_labels: + raise DatasetValidationError("Bbox labels do not match job labels") + + # Reorder labels to match the manifest + self._boxes_dataset.transform( + ProjectLabels, dst_labels=[label.name for label in self.manifest.annotation.labels] + ) + self._boxes_dataset.init_cache() + + def _validate_boxes_filenames(self): + boxes_filenames = set(sample.id + sample.media.ext for sample in self._boxes_dataset) + + known_data_filenames = set(self._data_filenames) + matched_boxes_filenames = boxes_filenames.intersection(known_data_filenames) + + if len(matched_boxes_filenames) != len(boxes_filenames): + extra_bbox_samples = list( + map(os.path.basename, boxes_filenames - matched_boxes_filenames) + ) + + raise MismatchingAnnotations( + "Failed to find several samples in the dataset files: {}".format( + format_sequence(extra_bbox_samples), + ) + ) + + def _validate_boxes_annotations(self): # noqa: PLR0912 + # Convert possible polygons and masks into boxes + self._boxes_dataset.transform(InstanceSegmentsToBbox) + self._boxes_dataset.init_cache() + + excluded_boxes_info = _ExcludedAnnotationsInfo() + + label_cat: dm.LabelCategories = self._boxes_dataset.categories()[dm.AnnotationType.label] + + visited_ids = set() + for sample in self._boxes_dataset: + # Could fail on this as well + image_h, image_w = sample.media_as(dm.Image).size + + valid_instances: list[tuple[dm.Bbox, dm.Points]] = [] + instances = find_instances( + [a for a in sample.annotations if isinstance(a, dm.Bbox | dm.Skeleton)] + ) + for instance_anns in instances: + if len(instance_anns) != 2: + excluded_boxes_info.add_message( + "Sample '{}': object #{} ({}) skipped - unexpected group size ({})".format( + sample.id, + instance_anns[0].id, + label_cat[instance_anns[0].label].name, + len(instance_anns), + ), + sample_id=sample.id, + sample_subset=sample.subset, + ) + continue + + bbox = next((a for a in instance_anns if isinstance(a, dm.Bbox)), None) + if not bbox: + excluded_boxes_info.add_message( + "Sample '{}': object #{} ({}) skipped - no matching bbox".format( + sample.id, instance_anns[0].id, label_cat[instance_anns[0].label].name + ), + sample_id=sample.id, + sample_subset=sample.subset, + ) + continue + + skeleton = next((a for a in instance_anns if isinstance(a, dm.Skeleton)), None) + if not skeleton: + excluded_boxes_info.add_message( + "Sample '{}': object #{} ({}) skipped - no matching skeleton".format( + sample.id, instance_anns[0].id, label_cat[instance_anns[0].label].name + ), + sample_id=sample.id, + sample_subset=sample.subset, + ) + continue + + if len(skeleton.elements) != 1 or len(skeleton.elements[0].points) != 2: + excluded_boxes_info.add_message( + "Sample '{}': object #{} ({}) skipped - invalid skeleton points".format( + sample.id, skeleton.id, label_cat[skeleton.label].name + ), + sample_id=sample.id, + sample_subset=sample.subset, + ) + continue + + point = skeleton.elements[0] + if not is_point_in_bbox(point.points[0], point.points[1], (0, 0, image_w, image_h)): + excluded_boxes_info.add_message( + "Sample '{}': object #{} ({}) skipped - invalid point coordinates".format( + sample.id, skeleton.id, label_cat[skeleton.label].name + ), + sample_id=sample.id, + sample_subset=sample.subset, + ) + continue + + if not is_point_in_bbox(int(bbox.x), int(bbox.y), (0, 0, image_w, image_h)): + excluded_boxes_info.add_message( + "Sample '{}': object #{} ({}) skipped - invalid bbox coordinates".format( + sample.id, bbox.id, label_cat[bbox.label].name + ), + sample_id=sample.id, + sample_subset=sample.subset, + ) + continue + + if not is_point_in_bbox(point.points[0], point.points[1], bbox): + excluded_boxes_info.add_message( + "Sample '{}': object #{} ({}) skipped - point is outside the bbox".format( + sample.id, skeleton.id, label_cat[skeleton.label].name + ), + sample_id=sample.id, + sample_subset=sample.subset, + ) + continue + + if bbox.id in visited_ids: + excluded_boxes_info.add_message( + "Sample '{}': bbox #{} ({}) skipped - repeated annotation id {}".format( + sample.id, bbox.id, label_cat[bbox.label].name, bbox.id + ), + sample_id=sample.id, + sample_subset=sample.subset, + ) + continue + + valid_instances.append( + (bbox, point.wrap(group=bbox.group, id=bbox.id, attributes=bbox.attributes)) + ) + visited_ids.add(bbox.id) + + excluded_boxes_info.excluded_count += len(instances) - len(valid_instances) + excluded_boxes_info.total_count += len(instances) + + if len(valid_instances) != len(sample.annotations): + self._boxes_dataset.put( + sample.wrap(annotations=list(chain.from_iterable(valid_instances))) + ) + + if excluded_boxes_info.excluded_count > ceil( + excluded_boxes_info.total_count * self.max_discarded_threshold + ): + raise TooFewSamples( + "Too many boxes discarded, canceling job creation. Errors: {}".format( + format_sequence( + [error_info.message for error_info in excluded_boxes_info.messages] + ) + ) + ) + + excluded_samples = set((e.sample_id, e.sample_subset) for e in excluded_boxes_info.messages) + for excluded_sample in excluded_samples: + self._boxes_dataset.remove(*excluded_sample) + + if excluded_samples: + self.logger.warning( + "Some boxes were excluded due to the errors found: {}".format( + format_sequence( + [e.message for e in excluded_boxes_info.messages], separator="\n" + ) + ) + ) + + self._excluded_boxes_info = excluded_boxes_info + + def _validate_boxes(self): + assert self._data_filenames is not _unset + assert self._boxes_dataset is not _unset + + self._validate_boxes_categories() + self._validate_boxes_filenames() + self._validate_boxes_annotations() + + def _match_boxes(self, a: BboxCoords, b: BboxCoords) -> bool: + return bbox_iou(a, b) > 0 + + def _get_skeleton_bbox( + self, skeleton: dm.Skeleton, annotations: Sequence[dm.Annotation] + ) -> BboxCoords: + matching_bbox = None + if skeleton.group: + matching_bbox = next( + ( + bbox + for bbox in annotations + if isinstance(bbox, dm.Bbox) and bbox.group == skeleton.group + ), + None, + ) + + if matching_bbox: + bbox = matching_bbox.get_bbox() + else: + bbox = skeleton.get_bbox() + + if any( + v != dm.Points.Visibility.absent for e in skeleton.elements for v in e.visibility + ): + # If there's only 1 visible point, the bbox will have 0 w and h + bbox = [bbox[0], bbox[1], max(1, bbox[2]), max(1, bbox[3])] + + return bbox + + def _prepare_gt(self): + def _find_unambiguous_matches( + input_boxes: list[dm.Bbox], + gt_skeletons: list[dm.Skeleton], + *, + input_points: list[dm.Points], + gt_annotations: list[dm.Annotation], + ) -> list[tuple[dm.Bbox, dm.Skeleton]]: + bbox_point_mapping: dict[int, dm.Points] = { + bbox.id: next(p for p in input_points if p.group == bbox.group) + for bbox in input_boxes + } + + matches = [ + [ + (input_bbox.label == gt_skeleton.label) + and ( + self._match_boxes( + input_bbox.get_bbox(), + self._get_skeleton_bbox(gt_skeleton, gt_annotations), + ) + ) + and (input_point := bbox_point_mapping[input_bbox.id]) + and is_point_in_bbox( + input_point.points[0], + input_point.points[1], + self._get_skeleton_bbox(gt_skeleton, gt_annotations), + ) + and ( + # a way to customize matching if the default method is too rough + not (bbox_id := input_bbox.attributes.get(self.gt_id_attribute)) + or not (skeleton_id := gt_skeleton.attributes.get(self.gt_id_attribute)) + or bbox_id == skeleton_id + ) + for gt_skeleton in gt_skeletons + ] + for input_bbox in input_boxes + ] + + ambiguous_boxes: list[int] = set() + ambiguous_skeletons: list[int] = set() + for bbox_idx, input_bbox in enumerate(input_boxes): + matched_skeletons: list[dm.Skeleton] = [ + gt_skeletons[j] for j in range(len(gt_skeletons)) if matches[bbox_idx][j] + ] + + if len(matched_skeletons) > 1: + # Handle ambiguous matches + excluded_boxes_info.add_message( + "Sample '{}': bbox #{} ({}) and overlapping skeletons skipped - " + "too many matching skeletons ({}) found".format( + boxes_sample.id, + input_bbox.id, + boxes_label_cat[input_bbox.label].name, + format_sequence([f"#{a.id}" for a in matched_skeletons]), + ), + sample_id=boxes_sample.id, + sample_subset=boxes_sample.subset, + ) + # not an error, should not be counted as excluded for an error + ambiguous_boxes.add(input_bbox.id) + ambiguous_skeletons.update(s.id for s in matched_skeletons) + continue + + for skeleton_idx, gt_skeleton in enumerate(gt_skeletons): + matched_boxes: list[dm.Bbox] = [ + input_boxes[i] for i in range(len(input_boxes)) if matches[i][skeleton_idx] + ] + + if len(matched_boxes) > 1: + # Handle ambiguous matches + excluded_gt_info.add_message( + "Sample '{}': GT skeleton #{} ({}) and overlapping boxes skipped - " + "too many matching boxes ({}) found".format( + gt_sample.id, + gt_skeleton.id, + gt_label_cat[gt_skeleton.label].name, + format_sequence([f"#{a.id}" for a in matched_boxes]), + ), + sample_id=gt_sample.id, + sample_subset=gt_sample.subset, + ) + # not an error, should not be counted as excluded for an error + ambiguous_skeletons.add(gt_skeleton.id) + ambiguous_boxes.update(b.id for b in matched_boxes) + continue + if not matched_boxes: + # Handle unmatched skeletons + excluded_gt_info.add_message( + "Sample '{}': GT skeleton #{} ({}) skipped - " + "no matching boxes found".format( + gt_sample.id, + gt_skeleton.id, + gt_label_cat[gt_skeleton.label].name, + ), + sample_id=gt_sample.id, + sample_subset=gt_sample.subset, + ) + excluded_gt_info.excluded_count += 1 # an error + continue + + unambiguous_matches: list[tuple[dm.Bbox, dm.Skeleton]] = [] + for bbox_idx, input_bbox in enumerate(input_boxes): + if input_bbox.id in ambiguous_boxes: + continue + + matched_skeleton = None + for gt_idx, gt_skeleton in enumerate(gt_skeletons): + if gt_skeleton.id in ambiguous_skeletons: + continue + + if matches[bbox_idx][gt_idx]: + matched_skeleton = gt_skeleton + break + + if matched_skeleton: + unambiguous_matches.append((input_bbox, matched_skeleton)) + + return unambiguous_matches + + def _find_good_gt_skeletons( + input_boxes: list[dm.Bbox], + gt_skeletons: list[dm.Skeleton], + *, + input_points: list[dm.Points], + gt_annotations: list[dm.Annotation], + ) -> list[dm.Skeleton]: + matches = _find_unambiguous_matches( + input_boxes, gt_skeletons, input_points=input_points, gt_annotations=gt_annotations + ) + + matched_skeletons = [] + for input_bbox, gt_skeleton in matches: + gt_count_per_class[gt_skeleton.label] = ( + gt_count_per_class.get(gt_skeleton.label, 0) + 1 + ) + + matched_skeletons.append(gt_skeleton) + skeleton_bbox_mapping[gt_skeleton.id] = input_bbox.id + + return matched_skeletons + + assert self._data_filenames is not _unset + assert self._boxes_dataset is not _unset + assert self._input_gt_dataset is not _unset + assert [ + label.name + for label in self._input_gt_dataset.categories()[dm.AnnotationType.label] + if not label.parent + ] == [label.name for label in self.manifest.annotation.labels] + assert [ + label.name + for label in self._boxes_dataset.categories()[dm.AnnotationType.label] + if not label.parent + ] == [label.name for label in self.manifest.annotation.labels] + + boxes_label_cat: dm.LabelCategories = self._boxes_dataset.categories()[ + dm.AnnotationType.label + ] + gt_label_cat: dm.LabelCategories = self._input_gt_dataset.categories()[ + dm.AnnotationType.label + ] + + updated_gt_dataset = dm.Dataset( + categories=self._input_gt_dataset.categories(), media_type=dm.Image + ) + + excluded_boxes_info = _ExcludedAnnotationsInfo() # local for the function + excluded_gt_info = self._excluded_gt_info + gt_count_per_class = {} + skeleton_bbox_mapping = {} # skeleton id -> bbox id + for gt_sample in self._input_gt_dataset: + boxes_sample = self._boxes_dataset.get(gt_sample.id, gt_sample.subset) + # Samples could be discarded, so we just skip them without an error + if not boxes_sample: + continue + + gt_skeletons = [a for a in gt_sample.annotations if isinstance(a, dm.Skeleton)] + input_boxes = [a for a in boxes_sample.annotations if isinstance(a, dm.Bbox)] + input_points = [a for a in boxes_sample.annotations if isinstance(a, dm.Points)] + assert len(input_boxes) == len(input_points) + + # Samples without boxes are allowed, so we just skip them without an error + if not gt_skeletons: + continue + + matched_skeletons = _find_good_gt_skeletons( + input_boxes, + gt_skeletons, + input_points=input_points, + gt_annotations=gt_sample.annotations, + ) + if not matched_skeletons: + continue + + updated_gt_dataset.put(gt_sample.wrap(annotations=matched_skeletons)) + + if excluded_boxes_info.messages: + self.logger.warning( + "Some boxes were excluded from GT due to the problems found: {}".format( + format_sequence( + [e.message for e in excluded_boxes_info.messages], separator="\n" + ) + ) + ) + + if excluded_gt_info.messages: + self.logger.warning( + "Some GT annotations were excluded due to the errors found: {}".format( + format_sequence([e.message for e in excluded_gt_info.messages], separator="\n") + ) + ) + + if excluded_gt_info.excluded_count > ceil( + self.max_discarded_threshold * excluded_gt_info.total_count + ): + raise DatasetValidationError( + "Too many GT skeletons discarded ({} out of {}). " + "Please make sure each GT skeleton matches exactly 1 bbox " + "and has at least 1 visible point".format( + excluded_gt_info.total_count - len(skeleton_bbox_mapping), + excluded_gt_info.total_count, + ) + ) + + self.logger.info( + "GT counts per class to be used for validation: {}".format( + format_sequence( + [ + f"{gt_label_cat[label_id].name}: {count}" + for label_id, count in gt_count_per_class.items() + ] + ) + ) + ) + + labels_with_few_gt = [ + gt_label_cat[label_id] + for label_id, label_count in gt_count_per_class.items() + if label_count < self.min_label_gt_samples + ] + if labels_with_few_gt: + raise DatasetValidationError( + "Too few matching GT boxes/points annotations found for some classes: {}".format( + format_sequence(labels_with_few_gt) + ) + ) + + self._gt_dataset = updated_gt_dataset + self._skeleton_bbox_mapping = skeleton_bbox_mapping + + def _prepare_roi_infos(self): + assert self._gt_dataset is not _unset + assert self._boxes_dataset is not _unset + + rois: list[skeletons_from_boxes_task.RoiInfo] = [] + for sample in self._boxes_dataset: + instances = find_instances(sample.annotations) + for instance_anns in instances: + bbox = next(a for a in instance_anns if isinstance(a, dm.Bbox)) + point = next(a for a in instance_anns if isinstance(a, dm.Points)) + + # RoI is centered on bbox center + original_bbox_cx = int(bbox.x + bbox.w / 2) + original_bbox_cy = int(bbox.y + bbox.h / 2) + + roi_w = ceil(bbox.w * self.roi_size_mult) + roi_h = ceil(bbox.h * self.roi_size_mult) + roi_w = max(roi_w, self.min_roi_size[0]) + roi_h = max(roi_h, self.min_roi_size[1]) + + roi_x = original_bbox_cx - int(roi_w / 2) + roi_y = original_bbox_cy - int(roi_h / 2) + + new_bbox_x = bbox.x - roi_x + new_bbox_y = bbox.y - roi_y + + rois.append( + skeletons_from_boxes_task.RoiInfo( + original_image_key=sample.attributes["id"], + bbox_id=bbox.id, + bbox_label=bbox.label, + bbox_x=new_bbox_x, + bbox_y=new_bbox_y, + point_x=point.points[0] - roi_x, + point_y=point.points[1] - roi_y, + roi_x=roi_x, + roi_y=roi_y, + roi_w=roi_w, + roi_h=roi_h, + ) + ) + + self._roi_infos = rois + self._roi_info_by_id = {roi_info.bbox_id: roi_info for roi_info in self._roi_infos} + + def _mangle_filenames(self): + """ + Mangle filenames in the dataset to make them less recognizable by annotators + and hide private dataset info + """ + assert self._roi_infos is not _unset + + # TODO: maybe add different names for the same GT images in + # different jobs to make them even less recognizable + self._roi_filenames = { + roi_info.bbox_id: str(uuid.uuid4()) + self.roi_file_ext for roi_info in self._roi_infos + } + + @property + def _task_segment_size(self) -> int: + # Here we use a job size multiplier, because each image + # is supposed to be simple and the assignment is expected + # to take little time with the default job size. + # Then, we add a percent of job tiles for validation, keeping the requested ratio. + return super()._task_segment_size * self.job_size_mult + + @property + def _job_val_frames_count(self) -> int: + return super()._job_val_frames_count * self.job_size_mult + + def _prepare_task_params(self): + assert self._roi_infos is not _unset + assert self._skeleton_bbox_mapping is not _unset + assert self._input_gt_dataset is not _unset + + # This list can be different from what is selected for validation + input_gt_filenames = set(sample.media.path for sample in self._input_gt_dataset) + image_id_to_filename = { + sample.attributes["id"]: sample.media.path for sample in self._boxes_dataset + } + + task_params: list[self._TaskParams] = [] + segment_size = self._task_segment_size + for label_id, _ in enumerate(self.manifest.annotation.labels): + label_gt_roi_ids = set( + roi_id + for roi_id in self._skeleton_bbox_mapping.values() + if self._roi_info_by_id[roi_id].bbox_label == label_id + ) + + label_data_roi_ids = [ + roi_info.bbox_id + for roi_info in self._roi_infos + if roi_info.bbox_label == label_id + if roi_info.bbox_id not in label_gt_roi_ids + if image_id_to_filename[roi_info.original_image_key] not in input_gt_filenames + ] + random.shuffle(label_data_roi_ids) + + task_params.extend( + [ + self._TaskParams( + label_id=label_id, roi_ids=task_data_roi_ids, roi_gt_ids=label_gt_roi_ids + ) + for task_data_roi_ids in take_by( + label_data_roi_ids, Config.cvat_config.max_jobs_per_task * segment_size + ) + ] + ) + + self._task_params = task_params + + def _prepare_job_labels(self): + self._point_labels = {} + + for skeleton_label in self.manifest.annotation.labels: + for point_name in skeleton_label.nodes: + self._point_labels[(skeleton_label.name, point_name)] = point_name + + def _prepare_gt_points_mapping(self): + assert self._gt_dataset is not _unset + + self._gt_points_per_label = {} + + # GT should contain all GT images with only one point per skeleton node + for gt_sample in self._gt_dataset: + for gt_skeleton in gt_sample.annotations: + if not isinstance(gt_skeleton, dm.Skeleton): + continue + + for point in gt_skeleton.elements: + if point.visibility[0] in [ + dm.Points.Visibility.absent, + dm.Points.Visibility.hidden, + ]: + continue + + point_label_name = ( + self._gt_dataset.categories()[dm.AnnotationType.label] + .items[point.label] + .name + ) + + self._gt_points_per_label.setdefault( + (gt_skeleton.label, point_label_name), [] + ).append((gt_skeleton.id, point)) + + def _upload_task_meta(self): + layout = skeletons_from_boxes_task.TaskMetaLayout() + serializer = skeletons_from_boxes_task.TaskMetaSerializer() + + file_list = [] + file_list.append( + (serializer.serialize_bbox_annotations(self._boxes_dataset), layout.BOXES_FILENAME) + ) + file_list.append( + ( + serializer.serialize_gt_annotations(self._gt_dataset), + layout.GT_FILENAME, + ) + ) + file_list.append( + ( + serializer.serialize_skeleton_bbox_mapping(self._skeleton_bbox_mapping), + layout.SKELETON_BBOX_MAPPING_FILENAME, + ) + ) + file_list.append((serializer.serialize_roi_info(self._roi_infos), layout.ROI_INFO_FILENAME)) + file_list.append( + (serializer.serialize_roi_filenames(self._roi_filenames), layout.ROI_FILENAMES_FILENAME) + ) + file_list.append( + (serializer.serialize_point_labels(self._point_labels), layout.POINT_LABELS_FILENAME) + ) + + storage_client = self._make_cloud_storage_client(self.oracle_data_bucket) + for file_data, filename in file_list: + storage_client.create_file( + compose_data_bucket_filename(self.escrow_address, self.chain_id, filename), + file_data, + ) + + def _extract_roi( + self, source_pixels: np.ndarray, roi_info: skeletons_from_boxes_task.RoiInfo + ) -> np.ndarray: + img_h, img_w, *_ = source_pixels.shape + + roi_pixels = source_pixels[ + max(0, roi_info.roi_y) : min(img_h, roi_info.roi_y + roi_info.roi_h), + max(0, roi_info.roi_x) : min(img_w, roi_info.roi_x + roi_info.roi_w), + ] + + if not ( + (0 <= roi_info.roi_x < roi_info.roi_x + roi_info.roi_w < img_w) + and (0 <= roi_info.roi_y < roi_info.roi_y + roi_info.roi_h < img_h) + ): + # Coords can be outside the original image + # In this case a border should be added to RoI, so that the image was centered on bbox + wrapped_roi_pixels = np.zeros((roi_info.roi_h, roi_info.roi_w, 3), dtype=np.float32) + wrapped_roi_pixels[:, :] = self.roi_background_color + + dst_y = max(-roi_info.roi_y, 0) + dst_x = max(-roi_info.roi_x, 0) + wrapped_roi_pixels[ + dst_y : dst_y + roi_pixels.shape[0], + dst_x : dst_x + roi_pixels.shape[1], + ] = roi_pixels + + roi_pixels = wrapped_roi_pixels + else: + roi_pixels = roi_pixels.copy() + + return roi_pixels + + def _draw_roi_bbox(self, roi_image: np.ndarray, bbox: dm.Bbox) -> np.ndarray: + roi_cy = roi_image.shape[0] // 2 + roi_cx = roi_image.shape[1] // 2 + return cv2.rectangle( + roi_image, + tuple(map(int, (roi_cx - bbox.w / 2, roi_cy - bbox.h / 2))), + tuple(map(int, (roi_cx + bbox.w / 2, roi_cy + bbox.h / 2))), + self.roi_embedded_bbox_color, + 1, + cv2.LINE_4, + ) + + def _draw_roi_point(self, roi_image: np.ndarray, point: tuple[float, float]) -> np.ndarray: + roi_r = (roi_image.shape[0] ** 2 + roi_image.shape[1] ** 2) ** 0.5 / 2 + radius = int( + min( + self.max_embedded_point_radius_percent * roi_r, + max(self.embedded_point_radius, self.min_embedded_point_radius_percent * roi_r), + ) + ) + + roi_image = cv2.circle( + roi_image, + tuple(map(int, (point[0], point[1]))), + radius + 1, + (255, 255, 255), + -1, + cv2.LINE_4, + ) + return cv2.circle( + roi_image, + tuple(map(int, (point[0], point[1]))), + radius, + self.embedded_point_color, + -1, + cv2.LINE_4, + ) + + def _extract_and_upload_rois(self): + assert self._roi_filenames is not _unset + assert self._roi_infos is not _unset + + src_bucket = BucketAccessInfo.parse_obj(self.manifest.data.data_url) + src_prefix = src_bucket.path + dst_bucket = self.oracle_data_bucket + + src_client = self._make_cloud_storage_client(src_bucket) + dst_client = self._make_cloud_storage_client(dst_bucket) + + image_id_to_filename = { + sample.attributes["id"]: sample.image.path for sample in self._boxes_dataset + } + + filename_to_sample = {sample.image.path: sample for sample in self._boxes_dataset} + + def _roi_info_key(e): + return e.original_image_key + + roi_info_by_image: dict[str, Sequence[skeletons_from_boxes_task.RoiInfo]] = { + image_id_to_filename[image_id]: list(g) + for image_id, g in groupby( + sorted(self._roi_infos, key=_roi_info_key), key=_roi_info_key + ) + } + + bbox_by_id = { + bbox.id: bbox + for sample in self._boxes_dataset + for bbox in sample.annotations + if isinstance(bbox, dm.Bbox) + } + + def process_file(filename: str, image_pixels: np.ndarray): + image_roi_infos = roi_info_by_image.get(filename, []) + if not image_roi_infos: + return + + sample = filename_to_sample[filename] + if tuple(sample.image.size) != tuple(image_pixels.shape[:2]): + # TODO: maybe rois should be regenerated instead + # Option 2: accumulate errors, fail when some threshold is reached + # Option 3: add special handling for cases when image is only rotated (exif etc.) + raise InvalidImageInfo( + f"Sample '{filename}': invalid size provided in the point annotations" + ) + + for roi_info in image_roi_infos: + roi_pixels = self._extract_roi(image_pixels, roi_info) + + if self.embed_bbox_in_roi_image: + roi_pixels = self._draw_roi_bbox(roi_pixels, bbox_by_id[roi_info.bbox_id]) + roi_pixels = self._draw_roi_point( + roi_pixels, (roi_info.point_x, roi_info.point_y) + ) + + filename = self._roi_filenames[roi_info.bbox_id] + roi_bytes = encode_image(roi_pixels, os.path.splitext(filename)[-1]) + + dst_client.create_file( + compose_data_bucket_filename(self.escrow_address, self.chain_id, filename), + data=roi_bytes, + ) + + def download_and_decode(key: str): + image_bytes = src_client.download_file(key) + return decode_image(image_bytes) + + pool_size = Config.features.max_data_storage_connections + download_queue_size = 4 * pool_size + download_queue = Queue[tuple[str, Future[np.ndarray]]](download_queue_size) + roi_uploader = BufferedRoiImageUploader(queue=download_queue) + with ThreadPoolExecutor(pool_size) as pool: + + def put_callback(filename: str): + image_roi_infos = roi_info_by_image.get(filename, []) + if not image_roi_infos: + return None + + return ( + filename, + pool.submit(download_and_decode, os.path.join(src_prefix, filename)), + ) + + def process_callback(result: tuple[str, Future]): + filename, task = result + process_file(filename, task.result()) + + roi_uploader.process_all( + self._data_filenames, put_callback=put_callback, process_callback=process_callback + ) + + def _prepare_gt_dataset_for_skeleton_point( + self, + *, + point_label_name: str, + skeleton_label_id: int, + ) -> dm.Dataset: + assert self._gt_points_per_label is not _unset + + # Change annotations to Points for validation in CVAT, + # as annotators will use this annotation type + point_dataset = dm.Dataset( + categories={ + dm.AnnotationType.label: dm.LabelCategories.from_iterable([point_label_name]), + }, + media_type=dm.Image, + ) + + for gt_skeleton_id, gt_point in self._gt_points_per_label[ + (skeleton_label_id, point_label_name) + ]: + roi_key = self._skeleton_bbox_mapping[gt_skeleton_id] + roi_info = self._roi_info_by_id[roi_key] + + mangled_cvat_sample_id = compose_data_bucket_filename( + self.escrow_address, + self.chain_id, + os.path.splitext(self._roi_filenames[roi_key])[0], + ) + + # update points coordinates in accordance with roi coordinates + updated_points = [ + gt_point.points[0] - roi_info.roi_x, + gt_point.points[1] - roi_info.roi_y, + ] + + point_dataset.put( + dm.DatasetItem( + id=mangled_cvat_sample_id, + annotations=[dm.Points(id=gt_skeleton_id, points=updated_points, label=0)], + ) + ) + + return point_dataset + + def _save_cvat_gt_dataset_to_oracle_bucket( + self, + gt_dataset_path: Path, + *, + file_suffix: str = "", + ) -> None: + layout = skeletons_from_boxes_task.TaskMetaLayout() + + base_gt_filename = layout.GT_FILENAME.split(".", maxsplit=1)[0] + final_gt_filename = f"{base_gt_filename}_{file_suffix}" + ".zip" + gt_dataset_key = compose_data_bucket_filename( + self.escrow_address, self.chain_id, final_gt_filename + ) + + storage_client = self._make_cloud_storage_client(self._oracle_data_bucket) + storage_client.create_file(gt_dataset_key, gt_dataset_path.read_bytes()) + + def _setup_quality_settings(self, task_id: int, **overrides) -> None: + values = { + "oks_sigma": Config.cvat_config.oks_sigma, + "point_size_base": "image_size", # we don't expect any boxes or groups, so ignore them + } + values.update(overrides) + super()._setup_quality_settings(task_id, **values) + + def _create_on_cvat(self): + assert self._task_params is not _unset + assert self._point_labels is not _unset + assert self._gt_points_per_label is not _unset + + def _task_params_label_key(ts): + return ts.label_id + + tasks_by_skeleton_label = { + skeleton_label_id: list(g) + for skeleton_label_id, g in groupby( + sorted(self._task_params, key=_task_params_label_key), key=_task_params_label_key + ) + } + + label_specs_by_skeleton = { + skeleton_label_id: [ + { + "name": self._point_labels[(skeleton_label.name, skeleton_point)], + "type": "points", + } + for skeleton_point in skeleton_label.nodes + ] + for skeleton_label_id, skeleton_label in enumerate(self.manifest.annotation.labels) + } + + oracle_bucket = self.oracle_data_bucket + + # Register cloud storage on CVAT to pass user dataset + cvat_cloud_storage = cvat_api.create_cloudstorage( + **_make_cvat_cloud_storage_params(oracle_bucket) + ) + + segment_size = self._task_segment_size + + total_jobs = sum( + len(self.manifest.annotation.labels[tp.label_id].nodes) + * (math.ceil(len(tp.roi_ids) / segment_size)) + for tp in self._task_params + ) + self.logger.info( + "Task creation for escrow '%s': will create %s assignments", + self.escrow_address, + total_jobs, + ) + + with SessionLocal.begin() as session: + db_service.create_escrow_creation( + session, + escrow_address=self.escrow_address, + chain_id=self.chain_id, + total_jobs=total_jobs, + ) + created_projects = [] + + for skeleton_label_id, skeleton_label_tasks in tasks_by_skeleton_label.items(): + skeleton_label_filenames: list[list[str]] = [] + gt_skeleton_label_filenames: list[list[str]] = [] + + for skeleton_label_task in skeleton_label_tasks: + skeleton_label_filenames.append( + [ + compose_data_bucket_filename( + self.escrow_address, self.chain_id, self._roi_filenames[roi_id] + ) + for roi_id in skeleton_label_task.roi_ids + ], + ) + + gt_skeleton_label_filenames.append( + [ + compose_data_bucket_filename( + self.escrow_address, self.chain_id, self._roi_filenames[roi_id] + ) + for roi_id in skeleton_label_task.roi_gt_ids + ], + ) + + for point_label_spec in label_specs_by_skeleton[skeleton_label_id]: + point_label_name = point_label_spec["name"] + + # Create a project for each point label. + # CVAT doesn't support tasks with different labels in a project. + cvat_project = cvat_api.create_project( + name="{} ({} {})".format( + self.escrow_address, + self.manifest.annotation.labels[skeleton_label_id].name, + point_label_name, + ), + user_guide=self.manifest.annotation.user_guide, + labels=[point_label_spec], + # TODO: improve guide handling - split for different points + ) + + # Setup webhooks for the project + cvat_webhook = cvat_api.create_cvat_webhook(cvat_project.id) + + project_id = db_service.create_project( + session, + cvat_project.id, + cvat_cloud_storage.id, + self.manifest.annotation.type, + self.escrow_address, + self.chain_id, + oracle_bucket.to_url().rstrip("/") + + "/" + + compose_data_bucket_prefix(self.escrow_address, self.chain_id), + cvat_webhook_id=cvat_webhook.id, + ) + created_projects.append(project_id) + + db_service.get_project_by_id( + session, project_id, for_update=True + ) # lock the row + db_service.add_project_images( + session, + cvat_project.id, + list(set(chain.from_iterable(skeleton_label_filenames))), + ) + + for point_label_filenames, gt_point_label_filenames in zip( + skeleton_label_filenames, gt_skeleton_label_filenames, strict=False + ): + cvat_task = cvat_api.create_task( + cvat_project.id, + name=cvat_project.name, + segment_size=segment_size, + ) + + task_id = db_service.create_task( + session, cvat_task.id, cvat_project.id, TaskStatuses[cvat_task.status] + ) + db_service.get_task_by_id(session, task_id, for_update=True) # lock the row + + # The task is fully created once 'update:task' webhook is received. + cvat_api.put_task_data( + cvat_task.id, + cvat_cloud_storage.id, + filenames=point_label_filenames + gt_point_label_filenames, + validation_params={ + "gt_filenames": gt_point_label_filenames, + "gt_frames_per_job_count": self._job_val_frames_count, + }, + ) + + gt_point_dataset = self._prepare_gt_dataset_for_skeleton_point( + point_label_name=point_label_name, + skeleton_label_id=skeleton_label_id, + ) + + self._setup_gt_job_for_cvat_task( + cvat_task.id, gt_point_dataset, dm_export_format="cvat" + ) + self._setup_quality_settings( + cvat_task.id, oks_sigma=Config.cvat_config.oks_sigma + ) + + db_service.create_data_upload(session, cvat_task.id) + + db_service.touch(session, Project, created_projects) + + def build(self): + self._download_input_data() + self._parse_gt() + self._parse_boxes() + self._validate_gt() + self._validate_boxes() + + # Task configuration creation + self._prepare_gt() + self._prepare_roi_infos() + self._prepare_task_params() + self._mangle_filenames() + self._prepare_job_labels() + + # Data preparation + self._extract_and_upload_rois() + self._upload_task_meta() + self._prepare_gt_points_mapping() + + self._create_on_cvat() diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/exceptions.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/exceptions.py new file mode 100644 index 0000000000..bf737ef20f --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/exceptions.py @@ -0,0 +1,29 @@ +from __future__ import annotations + + +class DatasetValidationError(Exception): + pass + + +class MismatchingAnnotations(DatasetValidationError): + pass + + +class TooFewSamples(DatasetValidationError): + pass + + +class InvalidCategories(DatasetValidationError): + pass + + +class InvalidImageInfo(DatasetValidationError): + pass + + +class InvalidCoordinates(DatasetValidationError): + pass + + +class InvisibleSkeletonError(DatasetValidationError): + pass diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/factory.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/factory.py new file mode 100644 index 0000000000..8cd0b3bc20 --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/factory.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from src.chain.escrow import get_escrow_manifest +from src.core.types import TaskTypes +from src.handlers.job_creation.builders.audio.transcription import AudioTranscriptionTaskBuilder +from src.handlers.job_creation.builders.vision.basic import ( + PointsTaskBuilder, + PolygonTaskBuilder, + SimpleTaskBuilder, +) +from src.handlers.job_creation.builders.vision.boxes_from_points import BoxesFromPointsTaskBuilder +from src.handlers.job_creation.builders.vision.skeletons_from_boxes import ( + SkeletonsFromBoxesTaskBuilder, +) +from src.log import ROOT_LOGGER_NAME +from src.utils.assignments import parse_manifest +from src.utils.logging import get_function_logger + +module_logger = f"{ROOT_LOGGER_NAME}.cron.cvat" + + +def create_task(escrow_address: str, chain_id: int) -> None: + logger = get_function_logger(module_logger) + + manifest = parse_manifest(get_escrow_manifest(chain_id, escrow_address)) + + if manifest.annotation.type in [TaskTypes.image_boxes, TaskTypes.image_label_binary]: + builder_type = SimpleTaskBuilder + elif manifest.annotation.type is TaskTypes.image_polygons: + builder_type = PolygonTaskBuilder + elif manifest.annotation.type in [TaskTypes.image_points]: + builder_type = PointsTaskBuilder + elif manifest.annotation.type in [TaskTypes.image_boxes_from_points]: + builder_type = BoxesFromPointsTaskBuilder + elif manifest.annotation.type in [TaskTypes.image_skeletons_from_boxes]: + builder_type = SkeletonsFromBoxesTaskBuilder + elif manifest.annotation.type in [TaskTypes.audio_transcription]: + builder_type = AudioTranscriptionTaskBuilder + else: + raise Exception(f"Unsupported task type {manifest.annotation.type}") + + with builder_type(manifest, escrow_address, chain_id) as task_builder: + task_builder.set_logger(logger) + task_builder.build() diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py new file mode 100644 index 0000000000..fb40f5ab19 --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +import os +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, TypeVar + +from datumaro.util.image import IMAGE_EXTENSIONS + +import src.cvat.api_calls as cvat_api +from src.core.types import TaskTypes +from src.services.cloud import CloudProviders + +if TYPE_CHECKING: + from src.core.manifest import TaskManifest + from src.services.cloud.utils import BucketAccessInfo + +LABEL_TYPE_MAPPING = { + TaskTypes.image_label_binary: cvat_api.LabelType.tag, + TaskTypes.image_points: cvat_api.LabelType.points, + TaskTypes.image_boxes: cvat_api.LabelType.rectangle, + TaskTypes.image_polygons: cvat_api.LabelType.polygon, + TaskTypes.image_boxes_from_points: cvat_api.LabelType.rectangle, + TaskTypes.image_skeletons_from_boxes: cvat_api.LabelType.points, +} + + +T = TypeVar("T") + + +class _Undefined: + def __bool__(self) -> bool: + return False + + +_unset = _Undefined() + +_MaybeUnset = T | _Undefined + + +@dataclass +class _ExcludedAnnotationInfo: + message: str + sample_id: str = field(kw_only=True) + sample_subset: str = field(kw_only=True) + + +@dataclass +class _ExcludedAnnotationsInfo: + messages: list[_ExcludedAnnotationInfo] = field(default_factory=list) + + excluded_count: int = 0 + "The number of excluded annotations. Can be different from len(messages)" + + total_count: int = 0 + + def add_message(self, message: str, *, sample_id: str, sample_subset: str): + self.messages.append( + _ExcludedAnnotationInfo( + message=message, sample_id=sample_id, sample_subset=sample_subset + ) + ) + + +def is_image(path: str) -> bool: + trunk, ext = os.path.splitext(os.path.basename(path)) + return trunk and ext.lower() in IMAGE_EXTENSIONS + + +def filter_image_files(data_filenames: list[str]) -> list[str]: + return [fn for fn in data_filenames if is_image(fn)] + + +def strip_bucket_prefix(data_filenames: list[str], prefix: str) -> list[str]: + return [os.path.relpath(fn, prefix) for fn in data_filenames] + + +def make_label_configuration(manifest: TaskManifest) -> list[dict]: + return [ + { + "name": label.name, + "type": LABEL_TYPE_MAPPING[manifest.annotation.type].value, + } + for label in manifest.annotation.labels + ] + + +def _make_cvat_cloud_storage_params(bucket_info: BucketAccessInfo) -> dict: + CLOUD_PROVIDER_TO_CVAT_CLOUD_PROVIDER = { + CloudProviders.aws: "AWS_S3_BUCKET", + CloudProviders.gcs: "GOOGLE_CLOUD_STORAGE", + } + + params = { + "provider": CLOUD_PROVIDER_TO_CVAT_CLOUD_PROVIDER[bucket_info.provider], + "bucket_name": bucket_info.bucket_name, + "bucket_host": bucket_info.host_url, + } + + if bucket_info.credentials: + params["credentials"] = bucket_info.credentials.to_dict() + + return params diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_export.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_export.py index a3d67c7eb6..e85ea9b79c 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_export.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_export.py @@ -15,8 +15,8 @@ from src.core.config import Config from src.core.manifest import TaskManifest from src.core.storage import compose_data_bucket_filename +from src.core.tasks.cvat_formats import DM_DATASET_FORMAT_MAPPING from src.core.types import TaskTypes -from src.handlers.job_creation import DM_DATASET_FORMAT_MAPPING from src.models.cvat import Image, Job from src.services.cloud import make_client as make_cloud_client from src.services.cloud.utils import BucketAccessInfo diff --git a/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_job_launcher_webhooks.py b/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_job_launcher_webhooks.py index 89b166425a..bd999774ad 100644 --- a/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_job_launcher_webhooks.py +++ b/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_job_launcher_webhooks.py @@ -63,9 +63,11 @@ def test_process_incoming_job_launcher_webhooks_escrow_created_type(self): with ( patch("src.chain.escrow.get_escrow") as mock_escrow, open("tests/utils/manifest.json") as data, - patch("src.handlers.job_creation.get_escrow_manifest") as mock_get_manifest, - patch("src.handlers.job_creation.cvat_api") as mock_cvat_api, - patch("src.handlers.job_creation.cloud_service.make_client") as mock_make_cloud_client, + patch("src.handlers.job_creation.factory.get_escrow_manifest") as mock_get_manifest, + patch("src.handlers.job_creation.builders.vision.basic.cvat_api") as mock_cvat_api, + patch( + "src.handlers.job_creation.builders.vision.basic.cloud_service.make_client" + ) as mock_make_cloud_client, ): manifest = json.load(data) mock_get_manifest.return_value = manifest @@ -247,11 +249,13 @@ def test_process_incoming_job_launcher_webhooks_escrow_created_type_remove_when_ with ( patch("src.chain.escrow.get_escrow") as mock_escrow, open("tests/utils/manifest.json") as data, - patch("src.handlers.job_creation.get_escrow_manifest") as mock_get_manifest, - patch("src.handlers.job_creation.cvat_api") as mock_cvat_api, - patch("src.handlers.job_creation.cloud_service.make_client") as mock_make_cloud_client, + patch("src.handlers.job_creation.factory.get_escrow_manifest") as mock_get_manifest, + patch("src.handlers.job_creation.builders.vision.basic.cvat_api") as mock_cvat_api, + patch( + "src.handlers.job_creation.builders.vision.basic.cloud_service.make_client" + ) as mock_make_cloud_client, patch( - "src.handlers.job_creation.db_service.add_project_images", + "src.handlers.job_creation.builders.vision.basic.db_service.add_project_images", side_effect=Exception("Error"), ), ): From b5ada8ccd30b7f8d9345d004647f87d76e5d909c Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Tue, 30 Jun 2026 15:00:53 +0300 Subject: [PATCH 02/53] refactor(exchange-oracle): move entrypoints and app factory out of package roots - Move run.py and debug.py into src/entrypoints/; launch them via 'python -m src.entrypoints.{run,debug}' in bin/start_*.sh. Fix debug.py repo_root to Path(__file__).parents[2] after the move. - Move the FastAPI app out of src/__init__.py into a new src/apps/exchange_oracle.py; src/__init__.py is now empty. - Repoint consumers: tests/conftest.py and the uvicorn app strings now use src.apps.exchange_oracle:app, and src/crons/_cron_job.py imports Config from src.core.config instead of the package root. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../examples/cvat/exchange-oracle/README.md | 2 +- .../cvat/exchange-oracle/bin/start_debug.sh | 2 +- .../cvat/exchange-oracle/bin/start_dev.sh | 2 +- .../cvat/exchange-oracle/bin/start_prod.sh | 2 +- .../cvat/exchange-oracle/src/__init__.py | 36 ------------- .../cvat/exchange-oracle/src/apps/__init__.py | 0 .../src/apps/exchange_oracle.py | 36 +++++++++++++ .../exchange-oracle/src/crons/_cron_job.py | 2 +- .../src/entrypoints/__init__.py | 0 .../{ => src/entrypoints}/debug.py | 54 +++++++++++++------ .../{ => src/entrypoints}/run.py | 2 +- .../cvat/exchange-oracle/tests/conftest.py | 2 +- 12 files changed, 81 insertions(+), 59 deletions(-) create mode 100644 packages/examples/cvat/exchange-oracle/src/apps/__init__.py create mode 100644 packages/examples/cvat/exchange-oracle/src/apps/exchange_oracle.py create mode 100644 packages/examples/cvat/exchange-oracle/src/entrypoints/__init__.py rename packages/examples/cvat/exchange-oracle/{ => src/entrypoints}/debug.py (87%) rename packages/examples/cvat/exchange-oracle/{ => src/entrypoints}/run.py (89%) diff --git a/packages/examples/cvat/exchange-oracle/README.md b/packages/examples/cvat/exchange-oracle/README.md index b1bcb63f6e..e618389c7d 100644 --- a/packages/examples/cvat/exchange-oracle/README.md +++ b/packages/examples/cvat/exchange-oracle/README.md @@ -30,7 +30,7 @@ docker compose -f docker-compose.dev.yml up -d ./bin/start_debug.sh ``` -When running service from `./bin/start_debug.sh` (`debug.py`), simplified development flow is available: +When running service from `./bin/start_debug.sh` (`src/entrypoints/debug.py`), simplified development flow is available: - When JWT token is required, simple JSON can be used instead of JWT token. - When webhook signature is required, `{oracle_name}:unique_string` can be used diff --git a/packages/examples/cvat/exchange-oracle/bin/start_debug.sh b/packages/examples/cvat/exchange-oracle/bin/start_debug.sh index ff93f4765c..a7d31fdc4f 100755 --- a/packages/examples/cvat/exchange-oracle/bin/start_debug.sh +++ b/packages/examples/cvat/exchange-oracle/bin/start_debug.sh @@ -1,4 +1,4 @@ export ENVIRONMENT=development alembic upgrade head -python debug.py \ No newline at end of file +python -m src.entrypoints.debug \ No newline at end of file diff --git a/packages/examples/cvat/exchange-oracle/bin/start_dev.sh b/packages/examples/cvat/exchange-oracle/bin/start_dev.sh index 64f2bd6fe1..66f642002c 100755 --- a/packages/examples/cvat/exchange-oracle/bin/start_dev.sh +++ b/packages/examples/cvat/exchange-oracle/bin/start_dev.sh @@ -1,4 +1,4 @@ export ENVIRONMENT=development alembic upgrade head -python run.py \ No newline at end of file +python -m src.entrypoints.run \ No newline at end of file diff --git a/packages/examples/cvat/exchange-oracle/bin/start_prod.sh b/packages/examples/cvat/exchange-oracle/bin/start_prod.sh index 01e23572e6..bd5322ceb1 100755 --- a/packages/examples/cvat/exchange-oracle/bin/start_prod.sh +++ b/packages/examples/cvat/exchange-oracle/bin/start_prod.sh @@ -1,4 +1,4 @@ export ENVIRONMENT=production alembic upgrade head -python run.py \ No newline at end of file +python -m src.entrypoints.run \ No newline at end of file diff --git a/packages/examples/cvat/exchange-oracle/src/__init__.py b/packages/examples/cvat/exchange-oracle/src/__init__.py index a6067b3e7b..e69de29bb2 100644 --- a/packages/examples/cvat/exchange-oracle/src/__init__.py +++ b/packages/examples/cvat/exchange-oracle/src/__init__.py @@ -1,36 +0,0 @@ -import logging - -from fastapi import FastAPI - -from src.core.config import Config -from src.crons import setup_cron_jobs -from src.endpoints import init_api -from src.handlers.error_handlers import setup_error_handlers -from src.log import setup_logging - -setup_logging() - -app = FastAPI( - title="Human Exchange Oracle", - description=""" - Exchange Oracle is a HUMAN oracle which main goal is to: - 1. Receive webhooks from a Job Launcher - 2. Process them and create jobs on CVAT - 3. Retrieve annotations and store them in a s3 bucket - 4. Notify recording oracle that raw annotations are ready - """, - version="0.1.0", -) - -init_api(app) -setup_error_handlers(app) - - -@app.on_event("startup") -async def startup_event(): - logger = logging.getLogger("app") - logger.info("Exchange Oracle is up and running!") - - -if not Config.is_test_mode(): - setup_cron_jobs(app) diff --git a/packages/examples/cvat/exchange-oracle/src/apps/__init__.py b/packages/examples/cvat/exchange-oracle/src/apps/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/examples/cvat/exchange-oracle/src/apps/exchange_oracle.py b/packages/examples/cvat/exchange-oracle/src/apps/exchange_oracle.py new file mode 100644 index 0000000000..a6067b3e7b --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/apps/exchange_oracle.py @@ -0,0 +1,36 @@ +import logging + +from fastapi import FastAPI + +from src.core.config import Config +from src.crons import setup_cron_jobs +from src.endpoints import init_api +from src.handlers.error_handlers import setup_error_handlers +from src.log import setup_logging + +setup_logging() + +app = FastAPI( + title="Human Exchange Oracle", + description=""" + Exchange Oracle is a HUMAN oracle which main goal is to: + 1. Receive webhooks from a Job Launcher + 2. Process them and create jobs on CVAT + 3. Retrieve annotations and store them in a s3 bucket + 4. Notify recording oracle that raw annotations are ready + """, + version="0.1.0", +) + +init_api(app) +setup_error_handlers(app) + + +@app.on_event("startup") +async def startup_event(): + logger = logging.getLogger("app") + logger.info("Exchange Oracle is up and running!") + + +if not Config.is_test_mode(): + setup_cron_jobs(app) diff --git a/packages/examples/cvat/exchange-oracle/src/crons/_cron_job.py b/packages/examples/cvat/exchange-oracle/src/crons/_cron_job.py index 2ed2b83b2c..ec0ce7c929 100644 --- a/packages/examples/cvat/exchange-oracle/src/crons/_cron_job.py +++ b/packages/examples/cvat/exchange-oracle/src/crons/_cron_job.py @@ -6,7 +6,7 @@ from sqlalchemy.orm import Session -from src import Config +from src.core.config import Config from src.db import SessionLocal from src.log import get_logger_name diff --git a/packages/examples/cvat/exchange-oracle/src/entrypoints/__init__.py b/packages/examples/cvat/exchange-oracle/src/entrypoints/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/examples/cvat/exchange-oracle/debug.py b/packages/examples/cvat/exchange-oracle/src/entrypoints/debug.py similarity index 87% rename from packages/examples/cvat/exchange-oracle/debug.py rename to packages/examples/cvat/exchange-oracle/src/entrypoints/debug.py index 4b89982d89..e5709d2a5b 100644 --- a/packages/examples/cvat/exchange-oracle/debug.py +++ b/packages/examples/cvat/exchange-oracle/src/entrypoints/debug.py @@ -1,7 +1,9 @@ import datetime import inspect import json +import sys import uuid +from argparse import ArgumentParser from collections.abc import Generator from contextlib import ExitStack, contextmanager from logging import Logger @@ -24,14 +26,16 @@ @contextmanager def _mock_cvat_cloud_storage_params(logger: Logger) -> Generator[None, None, None]: - from src.handlers.job_creation import ( + from src.handlers.job_creation.utils import ( _make_cvat_cloud_storage_params as original_make_cvat_cloud_storage_params, ) def patched_make_cvat_cloud_storage_params(bucket_info: BucketAccessInfo) -> dict: original_host_url = bucket_info.host_url - if Config.development_config.cvat_in_docker: + if Config.development_config.cvat_in_docker and ( + "localhost" in original_host_url or "127.0.0.1" in original_host_url + ): bucket_info.host_url = str( URL(original_host_url).copy_with( host=Config.development_config.exchange_oracle_host @@ -46,10 +50,15 @@ def patched_make_cvat_cloud_storage_params(bucket_info: BucketAccessInfo) -> dic finally: bucket_info.host_url = original_host_url - with mock.patch( - "src.handlers.job_creation._make_cvat_cloud_storage_params", - patched_make_cvat_cloud_storage_params, - ): + # The helper is looked up in each builder module that imports it, so patch all of them. + patch_targets = [ + "src.handlers.job_creation.builders.vision.basic._make_cvat_cloud_storage_params", + "src.handlers.job_creation.builders.vision.boxes_from_points._make_cvat_cloud_storage_params", + "src.handlers.job_creation.builders.vision.skeletons_from_boxes._make_cvat_cloud_storage_params", + ] + with ExitStack() as stack: + for target in patch_targets: + stack.enter_context(mock.patch(target, patched_make_cvat_cloud_storage_params)) yield @@ -69,8 +78,7 @@ def patched_get_escrow(chain_id: int, escrow_address: str) -> EscrowData: fn for fn in minio_manifests if ( - "/" in escrow_address - and fn == f"{escrow_address}.json" + ("/" in escrow_address and fn == f"{escrow_address}.json") or PurePosixPath(fn).name == f"{escrow_address}.json" ) ] @@ -235,7 +243,7 @@ def _mock_human_app_keys(_: Logger) -> Generator[None, None, None]: from tests.api.test_exchange_api import generate_ecdsa_keys # generating keys for local development - repo_root = Path(__file__).parent + repo_root = Path(__file__).parents[2] dev_dir = repo_root / "dev" dev_dir.mkdir(exist_ok=True) @@ -280,7 +288,20 @@ def apply_local_development_patches() -> Generator[None, None, None]: yield -if __name__ == "__main__": +def run_server(): + uvicorn.run( + app="src.apps.exchange_oracle:app", + host="0.0.0.0", # noqa: S104 + port=int(Config.port), + workers=Config.workers_amount, + ) + + +def main(args: list[str] | None = None) -> int: + parser = ArgumentParser() + parser.add_argument("-e", "--entrypoint", default=run_server) + parsed_args = parser.parse_args(args) + with ExitStack() as es: is_dev = Config.environment == "development" if is_dev: @@ -289,9 +310,10 @@ def apply_local_development_patches() -> Generator[None, None, None]: Config.validate() register_in_kvstore() - uvicorn.run( - app="src:app", - host="0.0.0.0", # noqa: S104 - port=int(Config.port), - workers=Config.workers_amount, - ) + parsed_args.entrypoint() + + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/packages/examples/cvat/exchange-oracle/run.py b/packages/examples/cvat/exchange-oracle/src/entrypoints/run.py similarity index 89% rename from packages/examples/cvat/exchange-oracle/run.py rename to packages/examples/cvat/exchange-oracle/src/entrypoints/run.py index d663c2f2a0..071c80f64f 100644 --- a/packages/examples/cvat/exchange-oracle/run.py +++ b/packages/examples/cvat/exchange-oracle/src/entrypoints/run.py @@ -9,7 +9,7 @@ register_in_kvstore() uvicorn.run( - app="src:app", + app="src.apps.exchange_oracle:app", host="0.0.0.0", # noqa: S104 port=int(Config.port), workers=Config.workers_amount, diff --git a/packages/examples/cvat/exchange-oracle/tests/conftest.py b/packages/examples/cvat/exchange-oracle/tests/conftest.py index c2b92ac0ed..0f3077cac5 100644 --- a/packages/examples/cvat/exchange-oracle/tests/conftest.py +++ b/packages/examples/cvat/exchange-oracle/tests/conftest.py @@ -14,7 +14,7 @@ from alembic import command as alembic_command from alembic.config import Config -from src import app +from src.apps.exchange_oracle import app from src.db import Base, SessionLocal, engine alembic_config = Config(Path(__file__).parent.parent / "alembic.ini") From a28c4e5c07ec9fdb482b45e67217fd2f2b63a423 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Wed, 1 Jul 2026 19:10:16 +0300 Subject: [PATCH 03/53] Prepare code for manifest version update --- .../cvat/exchange-oracle/src/core/{manifest.py => manifest/v1.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/examples/cvat/exchange-oracle/src/core/{manifest.py => manifest/v1.py} (100%) diff --git a/packages/examples/cvat/exchange-oracle/src/core/manifest.py b/packages/examples/cvat/exchange-oracle/src/core/manifest/v1.py similarity index 100% rename from packages/examples/cvat/exchange-oracle/src/core/manifest.py rename to packages/examples/cvat/exchange-oracle/src/core/manifest/v1.py From 1df26e024c95fa58e94f29106be0ca913b4fff99 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Thu, 2 Jul 2026 13:20:41 +0300 Subject: [PATCH 04/53] Introduce manifest v2 --- .../src/core/manifest/__init__.py | 23 +++ .../exchange-oracle/src/core/manifest/base.py | 6 + .../src/core/manifest/shared.py | 105 +++++++++++ .../exchange-oracle/src/core/manifest/v1.py | 112 +----------- .../exchange-oracle/src/core/manifest/v2.py | 169 ++++++++++++++++++ .../cvat/exchange-oracle/src/core/types.py | 1 + 6 files changed, 309 insertions(+), 107 deletions(-) create mode 100644 packages/examples/cvat/exchange-oracle/src/core/manifest/__init__.py create mode 100644 packages/examples/cvat/exchange-oracle/src/core/manifest/base.py create mode 100644 packages/examples/cvat/exchange-oracle/src/core/manifest/shared.py create mode 100644 packages/examples/cvat/exchange-oracle/src/core/manifest/v2.py diff --git a/packages/examples/cvat/exchange-oracle/src/core/manifest/__init__.py b/packages/examples/cvat/exchange-oracle/src/core/manifest/__init__.py new file mode 100644 index 0000000000..d19a7550a7 --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/core/manifest/__init__.py @@ -0,0 +1,23 @@ +from typing import Any + +from src.core.manifest import v1, v2 +from src.core.manifest.base import ManifestBase + +__all__ = [ + "ManifestBase", + "parse_manifest", + "v1", + "v2", +] + + +def parse_manifest(manifest: Any) -> ManifestBase: + if isinstance(manifest, dict): + version = manifest.get("version") + else: + version = getattr(manifest, "version", None) + + if version == 2: + return v2.JobManifest.model_validate(manifest) + else: + return v1.JobManifest.model_validate(manifest) diff --git a/packages/examples/cvat/exchange-oracle/src/core/manifest/base.py b/packages/examples/cvat/exchange-oracle/src/core/manifest/base.py new file mode 100644 index 0000000000..5c14e1bc11 --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/core/manifest/base.py @@ -0,0 +1,6 @@ +from typing import Protocol, runtime_checkable + + +@runtime_checkable +class ManifestBase(Protocol): + version: int diff --git a/packages/examples/cvat/exchange-oracle/src/core/manifest/shared.py b/packages/examples/cvat/exchange-oracle/src/core/manifest/shared.py new file mode 100644 index 0000000000..089429cc94 --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/core/manifest/shared.py @@ -0,0 +1,105 @@ +from enum import Enum +from typing import Annotated, Any, Literal + +from pydantic import BaseModel, Field, model_validator + +from src.utils.enums import BetterEnumMeta + + +class BucketProviders(str, Enum): + aws = "AWS" + gcs = "GCS" + + +class BucketUrlBase(BaseModel): + provider: BucketProviders + host_url: str + bucket_name: str + path: str = "" + + +class AwsBucketUrl(BucketUrlBase, BaseModel): + provider: Literal[BucketProviders.aws] + access_key: str = "" # (optional) AWS Access key + secret_key: str = "" # (optional) AWS Secret key + + +class GcsBucketUrl(BucketUrlBase, BaseModel): + provider: Literal[BucketProviders.gcs] + service_account_key: dict[str, Any] = {} # (optional) Contents of GCS key file + + +BucketUrl = Annotated[AwsBucketUrl | GcsBucketUrl, Field(discriminator="provider")] + + +class LabelTypes(str, Enum, metaclass=BetterEnumMeta): + plain = "plain" + skeleton = "skeleton" + + +class LabelInfoBase(BaseModel): + name: str = Field(min_length=1) + # https://docs.cvat.ai/docs/api_sdk/sdk/reference/models/label/ + + type: LabelTypes = LabelTypes.plain + + +class PlainLabelInfo(LabelInfoBase): + type: Literal[LabelTypes.plain] + + +class SkeletonLabelInfo(LabelInfoBase): + type: Literal[LabelTypes.skeleton] + + nodes: list[str] = Field(min_length=1) + """ + A list of node label names (only points are supposed to be nodes). + Example: + [ + "left hand", "torso", "right hand", "head" + ] + """ + + joints: list[tuple[int, int]] | None = Field(default_factory=list) + "A list of node adjacency, e.g. [[0, 1], [1, 2], [1, 3]]" + + @model_validator(mode="before") + @classmethod + def validate_type(cls, values: dict[str, Any]) -> dict[str, Any]: + if not isinstance(values, dict): + raise NotImplementedError + + if values["type"] != LabelTypes.skeleton: + raise ValueError(f"Label type must be {LabelTypes.skeleton}") + + skeleton_name = values["name"] + + existing_names = set() + for node_name in values["nodes"]: + node_name = node_name.strip() + + if not node_name: + raise ValueError(f"Skeleton '{skeleton_name}': point name is empty") + + if node_name.lower() in existing_names: + raise ValueError( + f"Skeleton '{skeleton_name}' point {node_name}: label is duplicated" + ) + + existing_names.add(node_name.lower()) + + nodes_count = len(values["nodes"]) + joints = values.get("joints") + if joints is not None: + for joint_idx, joint in enumerate(joints): + for v in joint: + if not (0 <= v < nodes_count): + raise ValueError( + f"Skeleton '{skeleton_name}' joint #{joint_idx}: invalid value. " + f"Expected a number in the range [0; {nodes_count - 1}]" + ) + + return values + + +LabelInfo = Annotated[PlainLabelInfo | SkeletonLabelInfo, Field(discriminator="type")] diff --git a/packages/examples/cvat/exchange-oracle/src/core/manifest/v1.py b/packages/examples/cvat/exchange-oracle/src/core/manifest/v1.py index 4a0f7a81c8..5d6abafe62 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/manifest/v1.py +++ b/packages/examples/cvat/exchange-oracle/src/core/manifest/v1.py @@ -1,37 +1,10 @@ from decimal import Decimal -from enum import Enum -from typing import Annotated, Any, Literal +from typing import Any, Literal from pydantic import AnyUrl, BaseModel, Field, model_validator +from src.core.manifest.shared import BucketUrl, LabelInfo, LabelTypes from src.core.types import TaskTypes -from src.utils.enums import BetterEnumMeta - - -class BucketProviders(str, Enum): - aws = "AWS" - gcs = "GCS" - - -class BucketUrlBase(BaseModel): - provider: BucketProviders - host_url: str - bucket_name: str - path: str = "" - - -class AwsBucketUrl(BucketUrlBase, BaseModel): - provider: Literal[BucketProviders.aws] - access_key: str = "" # (optional) AWS Access key - secret_key: str = "" # (optional) AWS Secret key - - -class GcsBucketUrl(BucketUrlBase, BaseModel): - provider: Literal[BucketProviders.gcs] - service_account_key: dict[str, Any] = {} # (optional) Contents of GCS key file - - -BucketUrl = Annotated[AwsBucketUrl | GcsBucketUrl, Field(discriminator="provider")] class DataInfo(BaseModel): @@ -48,79 +21,6 @@ class DataInfo(BaseModel): "which provides information about all objects on images" -class LabelTypes(str, Enum, metaclass=BetterEnumMeta): - plain = "plain" - skeleton = "skeleton" - - -class LabelInfoBase(BaseModel): - name: str = Field(min_length=1) - # https://opencv.github.io/cvat/docs/api_sdk/sdk/reference/models/label/ - - type: LabelTypes = LabelTypes.plain - - -class PlainLabelInfo(LabelInfoBase): - type: Literal[LabelTypes.plain] - - -class SkeletonLabelInfo(LabelInfoBase): - type: Literal[LabelTypes.skeleton] - - nodes: list[str] = Field(min_length=1) - """ - A list of node label names (only points are supposed to be nodes). - Example: - [ - "left hand", "torso", "right hand", "head" - ] - """ - - joints: list[tuple[int, int]] | None = Field(default_factory=list) - "A list of node adjacency, e.g. [[0, 1], [1, 2], [1, 3]]" - - @model_validator(mode="before") - @classmethod - def validate_type(cls, values: dict[str, Any]) -> dict[str, Any]: - if not isinstance(values, dict): - raise NotImplementedError - - if values["type"] != LabelTypes.skeleton: - raise ValueError(f"Label type must be {LabelTypes.skeleton}") - - skeleton_name = values["name"] - - existing_names = set() - for node_name in values["nodes"]: - node_name = node_name.strip() - - if not node_name: - raise ValueError(f"Skeleton '{skeleton_name}': point name is empty") - - if node_name.lower() in existing_names: - raise ValueError( - f"Skeleton '{skeleton_name}' point {node_name}: label is duplicated" - ) - - existing_names.add(node_name.lower()) - - nodes_count = len(values["nodes"]) - joints = values.get("joints") - if joints is not None: - for joint_idx, joint in enumerate(joints): - for v in joint: - if not (0 <= v < nodes_count): - raise ValueError( - f"Skeleton '{skeleton_name}' joint #{joint_idx}: invalid value. " - f"Expected a number in the range [0; {nodes_count - 1}]" - ) - - return values - - -LabelInfo = Annotated[PlainLabelInfo | SkeletonLabelInfo, Field(discriminator="type")] - - class AnnotationInfo(BaseModel): type: TaskTypes @@ -172,14 +72,12 @@ class ValidationInfo(BaseModel): "URL to the archive with Ground Truth annotations, the format is COCO keypoints" -class TaskManifest(BaseModel): +class JobManifest(BaseModel): + version: Literal[1] = 1 + data: DataInfo annotation: AnnotationInfo validation: ValidationInfo job_bounty: Decimal = Field(ge=0) "Assignment bounty, a decimal value in HMT" - - -def parse_manifest(manifest: Any) -> TaskManifest: - return TaskManifest.model_validate(manifest) diff --git a/packages/examples/cvat/exchange-oracle/src/core/manifest/v2.py b/packages/examples/cvat/exchange-oracle/src/core/manifest/v2.py new file mode 100644 index 0000000000..4d1e03449f --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/core/manifest/v2.py @@ -0,0 +1,169 @@ +from enum import Enum +from typing import Any, Literal + +from pydantic import AnyUrl, BaseModel, ConfigDict, Field, model_validator + +from src.core.manifest.shared import BucketUrl, LabelInfo, LabelTypes +from src.core.types import TaskTypes +from src.utils.enums import BetterEnumMeta + + +class DataInfo(BaseModel): + media_url: AnyUrl | BucketUrl + "Bucket URL, AWS S3 | GCS, virtual-hosted-style access" + + gt_url: AnyUrl | BucketUrl + "URL to the archive with Ground Truth annotations" + + points_url: AnyUrl | BucketUrl | None = None + """ + A path to a file or an archive with a set of points in COCO Keypoints format. + Applicable for: image_boxes_from_points + """ + + boxes_url: AnyUrl | BucketUrl | None = None + """ + A path to a file or an archive with a set of boxes in COCO Instances format. + Applicable for: image_skeletons_from_boxes + """ + + regions_url: AnyUrl | BucketUrl | None = None + """ + A path to a file or an archive with audio regions in the CVAT Generic TSV format. + Applicable for: audio_transcription + """ + + +class TargetMetrics(str, Enum, metaclass=BetterEnumMeta): + accuracy = "accuracy" + wer = "wer" + cer = "cer" + + +class ValidationInfo(BaseModel): + target_score: float = Field(ge=0) + "Required annotation score. Can be minimal or maximal depending on the metric used" + + target_metric: TargetMetrics = TargetMetrics.accuracy + "Metric used to score annotations against the target" + + +class MinComposition(BaseModel): + gt: int + "Minimal number of Ground Truth samples per composition" + + ds: int + "Minimal number of dataset samples per composition" + + +class DetailsInfoBase(BaseModel): + model_config = ConfigDict(extra="forbid") + + max_time: int | None = None + "Maximum assignment time, seconds" + + +class ImageJobDetails(DetailsInfoBase): + job_size: int | None = None + "Frames per job, validation frames are not included" + + val_size: int | None = None + "Validation frames per job" + + +class AudioJobDetails(DetailsInfoBase): + normalizer: str | None = "basic" + "Text normalizer for audio transcription" + + max_segment_duration: int | None = None + "Maximum audio segment duration, seconds" + + min_composition: MinComposition | None = None + "Minimal composition of a job" + + validation_overhead: int | None = None + "Extra validation samples added on top of the required amount" + + +DetailsInfo = ImageJobDetails | AudioJobDetails + + +class SharedAttribute(BaseModel): + name: str = Field(min_length=1) + type: str + values: list[str] = Field(default_factory=list) + default_value: str = "" + + +class AnnotationInfo(BaseModel): + description: str = "" + "Brief task description" + + user_guide_url: str = "" + "URL to the user guide in markdown format" + + labels: list[LabelInfo] = Field(min_length=1) + "Label declarations with accepted annotation types" + + shared_attributes: list[SharedAttribute] = Field(default_factory=list) + "Shared annotation attributes" + + validation: ValidationInfo + + qualifications: list[str] = Field(default_factory=list) + "A list of annotator qualifications required for participation" + + details: ImageJobDetails | AudioJobDetails | None = None + "Task-specific details" + + @model_validator(mode="before") + @classmethod + def validate_label_type(cls, values: dict[str, Any]) -> dict[str, Any]: + if not isinstance(values, dict): + raise NotImplementedError + + default_label_type = LabelTypes.plain + if values["type"] == TaskTypes.image_skeletons_from_boxes: + default_label_type = LabelTypes.skeleton + + # Add default value for labels, if none provided. + # pydantic can't do this for tagged unions + try: + labels = values["labels"] + for label_info in labels: + label_info["type"] = label_info.get("type", default_label_type) + except KeyError: + pass + + return values + + +class JobManifest(BaseModel): + version: Literal[2] + + task_type: TaskTypes + data: DataInfo + annotation: AnnotationInfo + + @model_validator(mode="before") + @classmethod + def validate_task_details(cls, values: dict[str, Any]) -> dict[str, Any]: + if not isinstance(values, dict): + raise NotImplementedError + + annotation = values.get("annotation") + if not isinstance(annotation, dict): + raise NotImplementedError + + if details := annotation.get("details"): + if not isinstance(details, dict): + raise NotImplementedError + + details_cls = ( + AudioJobDetails + if values.get("job_type") == TaskTypes.audio_transcription + else ImageJobDetails + ) + annotation["details"] = details_cls.model_validate(details) + + return values diff --git a/packages/examples/cvat/exchange-oracle/src/core/types.py b/packages/examples/cvat/exchange-oracle/src/core/types.py index 091e2d6133..53d816b33c 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/types.py +++ b/packages/examples/cvat/exchange-oracle/src/core/types.py @@ -38,6 +38,7 @@ class TaskTypes(str, Enum, metaclass=BetterEnumMeta): image_boxes_from_points = "image_boxes_from_points" image_skeletons_from_boxes = "image_skeletons_from_boxes" image_polygons = "image_polygons" + audio_transcription = "audio_transcription" class OracleWebhookTypes(str, Enum, metaclass=BetterEnumMeta): From 7e3138dbb0dbc75074abb1568228d99223ef3845 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Thu, 2 Jul 2026 16:14:29 +0300 Subject: [PATCH 05/53] Move TaskTypes to core.tasks --- .../exchange-oracle/src/core/manifest/v1.py | 2 +- .../exchange-oracle/src/core/manifest/v2.py | 2 +- .../exchange-oracle/src/core/tasks/__init__.py | 5 +++++ .../src/core/tasks/cvat_formats.py | 2 +- .../exchange-oracle/src/core/tasks/types.py | 13 +++++++++++++ .../cvat/exchange-oracle/src/core/types.py | 10 ---------- .../exchange-oracle/src/endpoints/exchange.py | 3 ++- .../src/handlers/completed_escrows.py | 3 ++- .../src/handlers/job_creation/factory.py | 2 +- .../src/handlers/job_creation/utils.py | 2 +- .../exchange-oracle/src/handlers/job_export.py | 2 +- .../cvat/exchange-oracle/src/models/cvat.py | 2 +- .../exchange-oracle/src/schemas/exchange.py | 3 ++- .../cvat/exchange-oracle/src/services/cvat.py | 2 +- .../exchange-oracle/src/services/exchange.py | 3 ++- .../exchange-oracle/src/utils/assignments.py | 17 +---------------- .../tests/api/test_exchange_api.py | 3 ++- .../test_track_completed_escrows.py | 2 +- .../test_track_completed_projects.py | 3 ++- .../test_track_completed_tasks.py | 3 ++- .../cron/test_process_job_launcher_webhooks.py | 2 +- .../test_process_recording_oracle_webhooks.py | 2 +- .../test_process_reputation_oracle_webhooks.py | 2 +- .../tests/integration/services/test_cvat.py | 2 +- .../tests/integration/services/test_exchange.py | 3 ++- .../exchange-oracle/tests/utils/db_helper.py | 3 ++- .../exchange-oracle/tests/utils/setup_cvat.py | 3 ++- 27 files changed, 52 insertions(+), 49 deletions(-) create mode 100644 packages/examples/cvat/exchange-oracle/src/core/tasks/types.py diff --git a/packages/examples/cvat/exchange-oracle/src/core/manifest/v1.py b/packages/examples/cvat/exchange-oracle/src/core/manifest/v1.py index 5d6abafe62..1b774ebd5f 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/manifest/v1.py +++ b/packages/examples/cvat/exchange-oracle/src/core/manifest/v1.py @@ -4,7 +4,7 @@ from pydantic import AnyUrl, BaseModel, Field, model_validator from src.core.manifest.shared import BucketUrl, LabelInfo, LabelTypes -from src.core.types import TaskTypes +from src.core.tasks import TaskTypes class DataInfo(BaseModel): diff --git a/packages/examples/cvat/exchange-oracle/src/core/manifest/v2.py b/packages/examples/cvat/exchange-oracle/src/core/manifest/v2.py index 4d1e03449f..10f49fcc75 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/manifest/v2.py +++ b/packages/examples/cvat/exchange-oracle/src/core/manifest/v2.py @@ -4,7 +4,7 @@ from pydantic import AnyUrl, BaseModel, ConfigDict, Field, model_validator from src.core.manifest.shared import BucketUrl, LabelInfo, LabelTypes -from src.core.types import TaskTypes +from src.core.tasks import TaskTypes from src.utils.enums import BetterEnumMeta diff --git a/packages/examples/cvat/exchange-oracle/src/core/tasks/__init__.py b/packages/examples/cvat/exchange-oracle/src/core/tasks/__init__.py index e69de29bb2..2245fece0a 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/tasks/__init__.py +++ b/packages/examples/cvat/exchange-oracle/src/core/tasks/__init__.py @@ -0,0 +1,5 @@ +from src.core.tasks.types import TaskTypes + +__all__ = [ + "TaskTypes" +] diff --git a/packages/examples/cvat/exchange-oracle/src/core/tasks/cvat_formats.py b/packages/examples/cvat/exchange-oracle/src/core/tasks/cvat_formats.py index 2c102a0e5d..a2a465c891 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/tasks/cvat_formats.py +++ b/packages/examples/cvat/exchange-oracle/src/core/tasks/cvat_formats.py @@ -1,6 +1,6 @@ from __future__ import annotations -from src.core.types import TaskTypes +from src.core.tasks.types import TaskTypes DM_DATASET_FORMAT_MAPPING = { TaskTypes.image_label_binary: "cvat_images", diff --git a/packages/examples/cvat/exchange-oracle/src/core/tasks/types.py b/packages/examples/cvat/exchange-oracle/src/core/tasks/types.py new file mode 100644 index 0000000000..6912483e39 --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/core/tasks/types.py @@ -0,0 +1,13 @@ +from enum import Enum + +from src.utils.enums import BetterEnumMeta + + +class TaskTypes(str, Enum, metaclass=BetterEnumMeta): + image_label_binary = "image_label_binary" + image_points = "image_points" + image_boxes = "image_boxes" + image_boxes_from_points = "image_boxes_from_points" + image_skeletons_from_boxes = "image_skeletons_from_boxes" + image_polygons = "image_polygons" + audio_transcription = "audio_transcription" diff --git a/packages/examples/cvat/exchange-oracle/src/core/types.py b/packages/examples/cvat/exchange-oracle/src/core/types.py index 53d816b33c..c138465b89 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/types.py +++ b/packages/examples/cvat/exchange-oracle/src/core/types.py @@ -31,16 +31,6 @@ class JobStatuses(str, Enum, metaclass=BetterEnumMeta): completed = "completed" -class TaskTypes(str, Enum, metaclass=BetterEnumMeta): - image_label_binary = "image_label_binary" - image_points = "image_points" - image_boxes = "image_boxes" - image_boxes_from_points = "image_boxes_from_points" - image_skeletons_from_boxes = "image_skeletons_from_boxes" - image_polygons = "image_polygons" - audio_transcription = "audio_transcription" - - class OracleWebhookTypes(str, Enum, metaclass=BetterEnumMeta): exchange_oracle = "exchange_oracle" job_launcher = "job_launcher" diff --git a/packages/examples/cvat/exchange-oracle/src/endpoints/exchange.py b/packages/examples/cvat/exchange-oracle/src/endpoints/exchange.py index e75b14ddaf..af4ef571c7 100644 --- a/packages/examples/cvat/exchange-oracle/src/endpoints/exchange.py +++ b/packages/examples/cvat/exchange-oracle/src/endpoints/exchange.py @@ -14,7 +14,8 @@ import src.services.cvat as cvat_service import src.services.exchange as oracle_service from src.core.config import Config -from src.core.types import ProjectStatuses, TaskTypes +from src.core.tasks import TaskTypes +from src.core.types import ProjectStatuses from src.db import SessionLocal from src.db import engine as db_engine from src.endpoints.authentication import ( diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/completed_escrows.py b/packages/examples/cvat/exchange-oracle/src/handlers/completed_escrows.py index 55821087c2..7af359cfb6 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/completed_escrows.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/completed_escrows.py @@ -19,7 +19,8 @@ ExchangeOracleEvent_JobFinished, ) from src.core.storage import compose_results_bucket_filename -from src.core.types import EscrowValidationStatuses, OracleWebhookTypes, TaskTypes +from src.core.tasks import TaskTypes +from src.core.types import EscrowValidationStatuses, OracleWebhookTypes from src.db import SessionLocal from src.db.utils import ForUpdateParams from src.handlers.job_export import ( diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/factory.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/factory.py index 8cd0b3bc20..a88362a99a 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/factory.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/factory.py @@ -1,7 +1,7 @@ from __future__ import annotations from src.chain.escrow import get_escrow_manifest -from src.core.types import TaskTypes +from src.core.tasks import TaskTypes from src.handlers.job_creation.builders.audio.transcription import AudioTranscriptionTaskBuilder from src.handlers.job_creation.builders.vision.basic import ( PointsTaskBuilder, diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py index fb40f5ab19..4b5642288e 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py @@ -7,7 +7,7 @@ from datumaro.util.image import IMAGE_EXTENSIONS import src.cvat.api_calls as cvat_api -from src.core.types import TaskTypes +from src.core.tasks import TaskTypes from src.services.cloud import CloudProviders if TYPE_CHECKING: diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_export.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_export.py index e85ea9b79c..6f132e80c9 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_export.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_export.py @@ -15,8 +15,8 @@ from src.core.config import Config from src.core.manifest import TaskManifest from src.core.storage import compose_data_bucket_filename +from src.core.tasks import TaskTypes from src.core.tasks.cvat_formats import DM_DATASET_FORMAT_MAPPING -from src.core.types import TaskTypes from src.models.cvat import Image, Job from src.services.cloud import make_client as make_cloud_client from src.services.cloud.utils import BucketAccessInfo diff --git a/packages/examples/cvat/exchange-oracle/src/models/cvat.py b/packages/examples/cvat/exchange-oracle/src/models/cvat.py index dd0eb2d715..bebe3e571d 100644 --- a/packages/examples/cvat/exchange-oracle/src/models/cvat.py +++ b/packages/examples/cvat/exchange-oracle/src/models/cvat.py @@ -5,6 +5,7 @@ from sqlalchemy.orm import Mapped, relationship from sqlalchemy.sql import func +from src.core.tasks import TaskTypes from src.core.types import ( AssignmentStatuses, CvatWebhookStatuses, @@ -13,7 +14,6 @@ Networks, ProjectStatuses, TaskStatuses, - TaskTypes, ) from src.db import Base, BaseUUID, ChildOf from src.utils.time import utcnow diff --git a/packages/examples/cvat/exchange-oracle/src/schemas/exchange.py b/packages/examples/cvat/exchange-oracle/src/schemas/exchange.py index 73fde0d230..32ed3bd870 100644 --- a/packages/examples/cvat/exchange-oracle/src/schemas/exchange.py +++ b/packages/examples/cvat/exchange-oracle/src/schemas/exchange.py @@ -4,7 +4,8 @@ from pydantic import BaseModel, Field from strenum import StrEnum # added in python 3.11 -from src.core.types import Networks, TaskTypes +from src.core.tasks import TaskTypes +from src.core.types import Networks from src.utils.enums import BetterEnumMeta diff --git a/packages/examples/cvat/exchange-oracle/src/services/cvat.py b/packages/examples/cvat/exchange-oracle/src/services/cvat.py index 083b1e8efc..3c1b3ced80 100644 --- a/packages/examples/cvat/exchange-oracle/src/services/cvat.py +++ b/packages/examples/cvat/exchange-oracle/src/services/cvat.py @@ -15,6 +15,7 @@ from sqlalchemy.sql.functions import coalesce from src.core.config import Config +from src.core.tasks import TaskTypes from src.core.types import ( AssignmentStatuses, CvatWebhookStatuses, @@ -22,7 +23,6 @@ JobStatuses, ProjectStatuses, TaskStatuses, - TaskTypes, ) from src.db import Base, ChildOf from src.db import engine as db_engine diff --git a/packages/examples/cvat/exchange-oracle/src/services/exchange.py b/packages/examples/cvat/exchange-oracle/src/services/exchange.py index 5fdf0c854b..9582b47e07 100644 --- a/packages/examples/cvat/exchange-oracle/src/services/exchange.py +++ b/packages/examples/cvat/exchange-oracle/src/services/exchange.py @@ -4,7 +4,8 @@ import src.cvat.api_calls as cvat_api import src.services.cvat as cvat_service from src.chain.escrow import get_escrow_manifest -from src.core.types import JobStatuses, Networks, ProjectStatuses, TaskTypes +from src.core.tasks import TaskTypes +from src.core.types import JobStatuses, Networks, ProjectStatuses from src.db import SessionLocal from src.db.utils import ForUpdateParams from src.models.cvat import Job diff --git a/packages/examples/cvat/exchange-oracle/src/utils/assignments.py b/packages/examples/cvat/exchange-oracle/src/utils/assignments.py index 51c8fe9c7b..9aa1d8fac5 100644 --- a/packages/examples/cvat/exchange-oracle/src/utils/assignments.py +++ b/packages/examples/cvat/exchange-oracle/src/utils/assignments.py @@ -1,16 +1,10 @@ from urllib.parse import urljoin from src.core.config import Config -from src.core.manifest import TaskManifest, TaskTypes -from src.core.manifest import parse_manifest as _parse_manifest -from src.core.tasks import skeletons_from_boxes +from src.core.tasks import TaskTypes, skeletons_from_boxes from src.models.cvat import Project -def parse_manifest(manifest: dict) -> TaskManifest: - return _parse_manifest(manifest) - - def compose_assignment_url(task_id: int, job_id: int, *, project: Project) -> str: query_params = "" if project.job_type in [ @@ -34,12 +28,3 @@ def get_default_assignment_timeout(task_type: TaskTypes) -> int: timeout_seconds *= skeletons_from_boxes.DEFAULT_ASSIGNMENT_SIZE_MULTIPLIER return timeout_seconds - - -def get_default_assignment_size(manifest: TaskManifest) -> int: - job_size = manifest.annotation.job_size + manifest.validation.val_size - - if job_size == TaskTypes.image_skeletons_from_boxes: - job_size *= skeletons_from_boxes.DEFAULT_ASSIGNMENT_SIZE_MULTIPLIER - - return job_size diff --git a/packages/examples/cvat/exchange-oracle/tests/api/test_exchange_api.py b/packages/examples/cvat/exchange-oracle/tests/api/test_exchange_api.py index 71c812bc52..53a16ded0e 100644 --- a/packages/examples/cvat/exchange-oracle/tests/api/test_exchange_api.py +++ b/packages/examples/cvat/exchange-oracle/tests/api/test_exchange_api.py @@ -15,7 +15,8 @@ from sqlalchemy.orm import Session from src.core.config import Config -from src.core.types import AssignmentStatuses, JobStatuses, ProjectStatuses, TaskTypes +from src.core.tasks import TaskTypes +from src.core.types import AssignmentStatuses, JobStatuses, ProjectStatuses from src.models.cvat import Assignment, Job, Project, Task, User from src.schemas.exchange import AssignmentStatuses as APIAssignmentStatuses from src.schemas.exchange import JobStatuses as APIJobStatuses diff --git a/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_escrows.py b/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_escrows.py index 574ae5983c..13410d300c 100644 --- a/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_escrows.py +++ b/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_escrows.py @@ -13,6 +13,7 @@ import pytest from sqlalchemy import select +from src.core.tasks import TaskTypes from src.core.types import ( AssignmentStatuses, EscrowValidationStatuses, @@ -21,7 +22,6 @@ Networks, ProjectStatuses, TaskStatuses, - TaskTypes, ) from src.crons import track_completed_escrows from src.crons.cvat.state_trackers import track_escrow_validations diff --git a/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_projects.py b/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_projects.py index 9adbf904dd..e325722683 100644 --- a/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_projects.py +++ b/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_projects.py @@ -3,7 +3,8 @@ from sqlalchemy.sql import select -from src.core.types import Networks, ProjectStatuses, TaskStatuses, TaskTypes +from src.core.tasks import TaskTypes +from src.core.types import Networks, ProjectStatuses, TaskStatuses from src.crons.cvat.state_trackers import track_completed_projects from src.db import SessionLocal from src.models.cvat import Project, Task diff --git a/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_tasks.py b/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_tasks.py index 98c77a9f18..682e4b6e48 100644 --- a/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_tasks.py +++ b/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_tasks.py @@ -3,7 +3,8 @@ from sqlalchemy.sql import select -from src.core.types import JobStatuses, Networks, ProjectStatuses, TaskStatuses, TaskTypes +from src.core.tasks import TaskTypes +from src.core.types import JobStatuses, Networks, ProjectStatuses, TaskStatuses from src.crons.cvat.state_trackers import track_completed_tasks from src.db import SessionLocal from src.models.cvat import Job, Project, Task diff --git a/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_job_launcher_webhooks.py b/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_job_launcher_webhooks.py index bd999774ad..ba178240ba 100644 --- a/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_job_launcher_webhooks.py +++ b/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_job_launcher_webhooks.py @@ -7,6 +7,7 @@ from sqlalchemy.sql import select from src.core.storage import compose_data_bucket_prefix, compose_results_bucket_prefix +from src.core.tasks import TaskTypes from src.core.types import ( ExchangeOracleEventTypes, JobLauncherEventTypes, @@ -16,7 +17,6 @@ OracleWebhookTypes, ProjectStatuses, TaskStatuses, - TaskTypes, ) from src.crons.webhooks.job_launcher import ( process_incoming_job_launcher_webhooks, diff --git a/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_recording_oracle_webhooks.py b/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_recording_oracle_webhooks.py index 96c6f2931c..cf4571a383 100644 --- a/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_recording_oracle_webhooks.py +++ b/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_recording_oracle_webhooks.py @@ -5,6 +5,7 @@ from sqlalchemy.sql import select +from src.core.tasks import TaskTypes from src.core.types import ( AssignmentStatuses, EscrowValidationStatuses, @@ -16,7 +17,6 @@ ProjectStatuses, RecordingOracleEventTypes, TaskStatuses, - TaskTypes, ) from src.crons.webhooks.recording_oracle import ( process_incoming_recording_oracle_webhook_job_completed, diff --git a/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_reputation_oracle_webhooks.py b/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_reputation_oracle_webhooks.py index 380ed680b6..a1eff0585a 100644 --- a/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_reputation_oracle_webhooks.py +++ b/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_reputation_oracle_webhooks.py @@ -5,6 +5,7 @@ from sqlalchemy import select from src.core.storage import compose_data_bucket_prefix, compose_results_bucket_prefix +from src.core.tasks import TaskTypes from src.core.types import ( ExchangeOracleEventTypes, JobStatuses, @@ -14,7 +15,6 @@ ProjectStatuses, ReputationOracleEventTypes, TaskStatuses, - TaskTypes, ) from src.crons.webhooks.reputation_oracle import process_incoming_reputation_oracle_webhooks from src.cvat import api_calls diff --git a/packages/examples/cvat/exchange-oracle/tests/integration/services/test_cvat.py b/packages/examples/cvat/exchange-oracle/tests/integration/services/test_cvat.py index 65b07db6ae..5f43f03d69 100644 --- a/packages/examples/cvat/exchange-oracle/tests/integration/services/test_cvat.py +++ b/packages/examples/cvat/exchange-oracle/tests/integration/services/test_cvat.py @@ -6,13 +6,13 @@ from sqlalchemy.orm.exc import UnmappedInstanceError import src.services.cvat as cvat_service +from src.core.tasks import TaskTypes from src.core.types import ( AssignmentStatuses, JobStatuses, Networks, ProjectStatuses, TaskStatuses, - TaskTypes, ) from src.db import SessionLocal from src.models.cvat import ( diff --git a/packages/examples/cvat/exchange-oracle/tests/integration/services/test_exchange.py b/packages/examples/cvat/exchange-oracle/tests/integration/services/test_exchange.py index e2a52815ce..58d89d3ca3 100644 --- a/packages/examples/cvat/exchange-oracle/tests/integration/services/test_exchange.py +++ b/packages/examples/cvat/exchange-oracle/tests/integration/services/test_exchange.py @@ -8,7 +8,8 @@ from fastapi import HTTPException from pydantic import ValidationError -from src.core.types import AssignmentStatuses, JobStatuses, Networks, TaskTypes +from src.core.tasks import TaskTypes +from src.core.types import AssignmentStatuses, JobStatuses, Networks from src.db import SessionLocal from src.endpoints.serializers import serialize_job from src.models.cvat import Assignment, User diff --git a/packages/examples/cvat/exchange-oracle/tests/utils/db_helper.py b/packages/examples/cvat/exchange-oracle/tests/utils/db_helper.py index 3fd5e6afe1..57bd5b58b8 100644 --- a/packages/examples/cvat/exchange-oracle/tests/utils/db_helper.py +++ b/packages/examples/cvat/exchange-oracle/tests/utils/db_helper.py @@ -2,7 +2,8 @@ from sqlalchemy.orm import Session -from src.core.types import JobStatuses, Networks, ProjectStatuses, TaskStatuses, TaskTypes +from src.core.tasks import TaskTypes +from src.core.types import JobStatuses, Networks, ProjectStatuses, TaskStatuses from src.models.cvat import Job, Project, Task diff --git a/packages/examples/cvat/exchange-oracle/tests/utils/setup_cvat.py b/packages/examples/cvat/exchange-oracle/tests/utils/setup_cvat.py index 77f657f98b..ace837b61f 100644 --- a/packages/examples/cvat/exchange-oracle/tests/utils/setup_cvat.py +++ b/packages/examples/cvat/exchange-oracle/tests/utils/setup_cvat.py @@ -10,7 +10,8 @@ from sqlalchemy.sql import select from src.core.config import CvatConfig -from src.core.types import AssignmentStatuses, JobStatuses, ProjectStatuses, TaskStatuses, TaskTypes +from src.core.tasks import TaskTypes +from src.core.types import AssignmentStatuses, JobStatuses, ProjectStatuses, TaskStatuses from src.db import SessionLocal from src.models.cvat import Assignment, Job, Project, Task, User From e36e0b99d86b27eeb45de849a590ae1cc8a64ab7 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Thu, 2 Jul 2026 16:34:53 +0300 Subject: [PATCH 06/53] Make some task creation symbols public --- .../exchange-oracle/src/entrypoints/debug.py | 8 +- .../builders/audio/transcription.py | 4 +- .../job_creation/builders/vision/base.py | 6 +- .../job_creation/builders/vision/basic.py | 22 ++-- .../builders/vision/boxes_from_points.py | 108 +++++++++--------- .../builders/vision/skeletons_from_boxes.py | 100 ++++++++-------- .../src/handlers/job_creation/utils.py | 16 +-- .../src/services/cloud/types.py | 8 +- 8 files changed, 136 insertions(+), 136 deletions(-) diff --git a/packages/examples/cvat/exchange-oracle/src/entrypoints/debug.py b/packages/examples/cvat/exchange-oracle/src/entrypoints/debug.py index e5709d2a5b..d902e95e1a 100644 --- a/packages/examples/cvat/exchange-oracle/src/entrypoints/debug.py +++ b/packages/examples/cvat/exchange-oracle/src/entrypoints/debug.py @@ -27,7 +27,7 @@ @contextmanager def _mock_cvat_cloud_storage_params(logger: Logger) -> Generator[None, None, None]: from src.handlers.job_creation.utils import ( - _make_cvat_cloud_storage_params as original_make_cvat_cloud_storage_params, + make_cvat_cloud_storage_params as original_make_cvat_cloud_storage_params, ) def patched_make_cvat_cloud_storage_params(bucket_info: BucketAccessInfo) -> dict: @@ -52,9 +52,9 @@ def patched_make_cvat_cloud_storage_params(bucket_info: BucketAccessInfo) -> dic # The helper is looked up in each builder module that imports it, so patch all of them. patch_targets = [ - "src.handlers.job_creation.builders.vision.basic._make_cvat_cloud_storage_params", - "src.handlers.job_creation.builders.vision.boxes_from_points._make_cvat_cloud_storage_params", - "src.handlers.job_creation.builders.vision.skeletons_from_boxes._make_cvat_cloud_storage_params", + "src.handlers.job_creation.builders.vision.basic.make_cvat_cloud_storage_params", + "src.handlers.job_creation.builders.vision.boxes_from_points.make_cvat_cloud_storage_params", + "src.handlers.job_creation.builders.vision.skeletons_from_boxes.make_cvat_cloud_storage_params", ] with ExitStack() as stack: for target in patch_targets: diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py index d3e9fe4940..60d415cddb 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py @@ -1,9 +1,9 @@ from __future__ import annotations -from src.handlers.job_creation.builders.vision.base import _TaskBuilderBase +from src.handlers.job_creation.builders.vision.base import TaskBuilderBase -class AudioTranscriptionTaskBuilder(_TaskBuilderBase): +class AudioTranscriptionTaskBuilder(TaskBuilderBase): """ Handles task creation for the AUDIO_TRANSCRIPTION task type. diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/base.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/base.py index fd5c0dd97c..b6ea00c4b5 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/base.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/base.py @@ -24,11 +24,11 @@ import datumaro as dm - from src.core.manifest import TaskManifest + from src.core.manifest import ManifestBase -class _TaskBuilderBase(metaclass=ABCMeta): - def __init__(self, manifest: TaskManifest, escrow_address: str, chain_id: int) -> None: +class TaskBuilderBase(metaclass=ABCMeta): + def __init__(self, manifest: ManifestBase, escrow_address: str, chain_id: int) -> None: self.exit_stack = ExitStack() self.manifest = manifest self.escrow_address = escrow_address diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/basic.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/basic.py index 088c1520f2..d3701fdd1b 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/basic.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/basic.py @@ -22,24 +22,24 @@ from src.utils.logging import format_sequence if TYPE_CHECKING: - from src.core.manifest import TaskManifest + from src.core.manifest.v1 import JobManifest from src.core.tasks.cvat_formats import DM_GT_DATASET_FORMAT_MAPPING -from src.handlers.job_creation.builders.vision.base import _TaskBuilderBase +from src.handlers.job_creation.builders.vision.base import TaskBuilderBase from src.handlers.job_creation.exceptions import ( DatasetValidationError, TooFewSamples, ) from src.handlers.job_creation.utils import ( - _make_cvat_cloud_storage_params, - _MaybeUnset, - _unset, + MaybeUnset, filter_image_files, + make_cvat_cloud_storage_params, make_label_configuration, + unset, ) -class SimpleTaskBuilder(_TaskBuilderBase): +class SimpleTaskBuilder(TaskBuilderBase): """ Handles task creation for the IMAGE_BOXES task type """ @@ -88,7 +88,7 @@ def _parse_gt_dataset( return gt_dataset def _get_gt_filenames( - self, gt_dataset: dm.Dataset, data_filenames: list[str], *, manifest: TaskManifest + self, gt_dataset: dm.Dataset, data_filenames: list[str], *, manifest: JobManifest ) -> list[str]: gt_filenames = set(s.id + s.media.ext for s in gt_dataset) known_data_filenames = set(data_filenames) @@ -138,7 +138,7 @@ def build(self): self._upload_task_meta(gt_dataset) # Register cloud storage on CVAT to pass user dataset - cloud_storage = cvat_api.create_cloudstorage(**_make_cvat_cloud_storage_params(data_bucket)) + cloud_storage = cvat_api.create_cloudstorage(**make_cvat_cloud_storage_params(data_bucket)) # Create a project cvat_project = cvat_api.create_project( @@ -208,10 +208,10 @@ def build(self): class PointsTaskBuilder(SimpleTaskBuilder): - def __init__(self, manifest: TaskManifest, escrow_address: str, chain_id: int) -> None: + def __init__(self, manifest: JobManifest, escrow_address: str, chain_id: int) -> None: super().__init__(manifest=manifest, escrow_address=escrow_address, chain_id=chain_id) - self._mean_gt_bbox_radius_estimation: _MaybeUnset[float] = _unset + self._mean_gt_bbox_radius_estimation: MaybeUnset[float] = unset def _parse_gt_dataset(self, gt_file_data, *, add_prefix=None): gt_dataset = super()._parse_gt_dataset(gt_file_data, add_prefix=add_prefix) @@ -279,7 +279,7 @@ def _setup_gt_job_for_cvat_task( ) def _setup_quality_settings(self, task_id: int, **overrides) -> None: - assert self._mean_gt_bbox_radius_estimation is not _unset + assert self._mean_gt_bbox_radius_estimation is not unset values = { # We have at most 1 annotation per frame, so accuracy on each frame is either 0 or 1, diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/boxes_from_points.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/boxes_from_points.py index e7bcafa60c..ad1b1a5057 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/boxes_from_points.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/boxes_from_points.py @@ -34,10 +34,10 @@ if TYPE_CHECKING: from collections.abc import Sequence - from src.core.manifest import TaskManifest + from src.core.manifest.v1 import JobManifest from src.core.tasks.cvat_formats import DM_GT_DATASET_FORMAT_MAPPING -from src.handlers.job_creation.builders.vision.base import _TaskBuilderBase +from src.handlers.job_creation.builders.vision.base import TaskBuilderBase from src.handlers.job_creation.exceptions import ( DatasetValidationError, InvalidCategories, @@ -47,47 +47,47 @@ TooFewSamples, ) from src.handlers.job_creation.utils import ( - _ExcludedAnnotationsInfo, - _make_cvat_cloud_storage_params, - _MaybeUnset, - _unset, + ExcludedAnnotationsInfo, + MaybeUnset, filter_image_files, + make_cvat_cloud_storage_params, make_label_configuration, strip_bucket_prefix, + unset, ) -class BoxesFromPointsTaskBuilder(_TaskBuilderBase): - def __init__(self, manifest: TaskManifest, escrow_address: str, chain_id: int) -> None: +class BoxesFromPointsTaskBuilder(TaskBuilderBase): + def __init__(self, manifest: JobManifest, escrow_address: str, chain_id: int) -> None: super().__init__(manifest=manifest, escrow_address=escrow_address, chain_id=chain_id) - self._input_gt_data: _MaybeUnset[bytes] = _unset - self._input_points_data: _MaybeUnset[bytes] = _unset + self._input_gt_data: MaybeUnset[bytes] = unset + self._input_points_data: MaybeUnset[bytes] = unset - self._data_filenames: _MaybeUnset[Sequence[str]] = _unset - self._input_gt_dataset: _MaybeUnset[dm.Dataset] = _unset - self._gt_dataset: _MaybeUnset[dm.Dataset] = _unset - self._gt_roi_dataset: _MaybeUnset[dm.Dataset] = _unset - self._points_dataset: _MaybeUnset[dm.Dataset] = _unset + self._data_filenames: MaybeUnset[Sequence[str]] = unset + self._input_gt_dataset: MaybeUnset[dm.Dataset] = unset + self._gt_dataset: MaybeUnset[dm.Dataset] = unset + self._gt_roi_dataset: MaybeUnset[dm.Dataset] = unset + self._points_dataset: MaybeUnset[dm.Dataset] = unset - self._bbox_point_mapping: _MaybeUnset[boxes_from_points_task.BboxPointMapping] = _unset + self._bbox_point_mapping: MaybeUnset[boxes_from_points_task.BboxPointMapping] = unset "bbox_id -> point_id" - self._roi_size_estimations: _MaybeUnset[dict[int, tuple[float, float]]] = _unset + self._roi_size_estimations: MaybeUnset[dict[int, tuple[float, float]]] = unset "label_id -> (rel. w, rel. h)" - self._rois: _MaybeUnset[boxes_from_points_task.RoiInfos] = _unset - self._roi_filenames: _MaybeUnset[boxes_from_points_task.RoiFilenames] = _unset - self._roi_filenames_to_be_annotated: _MaybeUnset[Sequence[str]] = _unset - self._gt_roi_filenames: _MaybeUnset[Sequence[str]] = _unset + self._rois: MaybeUnset[boxes_from_points_task.RoiInfos] = unset + self._roi_filenames: MaybeUnset[boxes_from_points_task.RoiFilenames] = unset + self._roi_filenames_to_be_annotated: MaybeUnset[Sequence[str]] = unset + self._gt_roi_filenames: MaybeUnset[Sequence[str]] = unset - self._job_layout: _MaybeUnset[Sequence[Sequence[str]]] = _unset + self._job_layout: MaybeUnset[Sequence[Sequence[str]]] = unset "File lists per CVAT job" - self._label_configuration: _MaybeUnset[Sequence[dict]] = _unset + self._label_configuration: MaybeUnset[Sequence[dict]] = unset - self._excluded_points_info: _MaybeUnset[_ExcludedAnnotationsInfo] = _unset - self._excluded_gt_info: _MaybeUnset[_ExcludedAnnotationsInfo] = _unset + self._excluded_points_info: MaybeUnset[ExcludedAnnotationsInfo] = unset + self._excluded_gt_info: MaybeUnset[ExcludedAnnotationsInfo] = unset # Configuration / constants # TODO: consider WebP if produced files are too big @@ -164,7 +164,7 @@ def _parse_dataset(self, annotation_file_data: bytes, dataset_format: str) -> dm return dm.Dataset.import_from(annotation_filename, format=dataset_format) def _parse_gt(self): - assert self._input_gt_data is not _unset + assert self._input_gt_data is not unset self._input_gt_dataset = self._parse_dataset( self._input_gt_data, @@ -172,7 +172,7 @@ def _parse_gt(self): ) def _parse_points(self): - assert self._input_points_data is not _unset + assert self._input_points_data is not unset self._points_dataset = self._parse_dataset( self._input_points_data, dataset_format=self.points_format @@ -221,7 +221,7 @@ def _validate_gt_filenames(self): def _validate_gt_annotations(self): label_cat: dm.LabelCategories = self._input_gt_dataset.categories()[dm.AnnotationType.label] - excluded_gt_info = _ExcludedAnnotationsInfo() + excluded_gt_info = ExcludedAnnotationsInfo() excluded_samples = set() visited_ids = set() for gt_sample in self._input_gt_dataset: @@ -289,8 +289,8 @@ def _validate_gt_annotations(self): self._excluded_gt_info = excluded_gt_info def _validate_gt(self): - assert self._data_filenames is not _unset - assert self._input_gt_dataset is not _unset + assert self._data_filenames is not unset + assert self._input_gt_dataset is not unset self._validate_gt_filenames() self._validate_gt_labels() @@ -365,7 +365,7 @@ def _validate_skeleton(skeleton: dm.Skeleton, *, sample_bbox: dm.Bbox): label_cat: dm.LabelCategories = self._points_dataset.categories()[dm.AnnotationType.label] - excluded_points_info = _ExcludedAnnotationsInfo() + excluded_points_info = ExcludedAnnotationsInfo() excluded_samples = set() visited_ids = set() for sample in self._points_dataset: @@ -435,8 +435,8 @@ def _validate_skeleton(skeleton: dm.Skeleton, *, sample_bbox: dm.Bbox): self._excluded_points_info = excluded_points_info def _validate_points(self): - assert self._data_filenames is not _unset - assert self._points_dataset is not _unset + assert self._data_filenames is not unset + assert self._points_dataset is not unset self._validate_points_categories() self._validate_points_filenames() @@ -559,9 +559,9 @@ def _find_good_gt_boxes( return matched_boxes - assert self._data_filenames is not _unset - assert self._points_dataset is not _unset - assert self._input_gt_dataset is not _unset + assert self._data_filenames is not unset + assert self._points_dataset is not unset + assert self._input_gt_dataset is not unset assert [ label.name for label in self._input_gt_dataset.categories()[dm.AnnotationType.label] ] == [label.name for label in self.manifest.annotation.labels] @@ -582,7 +582,7 @@ def _find_good_gt_boxes( categories=self._input_gt_dataset.categories(), media_type=dm.Image ) - excluded_points_info = _ExcludedAnnotationsInfo() # local for the function + excluded_points_info = ExcludedAnnotationsInfo() # local for the function excluded_gt_info = self._excluded_gt_info gt_count_per_class = {} bbox_point_mapping = {} # bbox id -> point id @@ -657,7 +657,7 @@ def _find_good_gt_boxes( self._bbox_point_mapping = bbox_point_mapping def _estimate_roi_sizes(self): - assert self._gt_dataset is not _unset + assert self._gt_dataset is not unset assert [label.name for label in self._gt_dataset.categories()[dm.AnnotationType.label]] == [ label.name for label in self.manifest.annotation.labels ] @@ -719,9 +719,9 @@ def _estimate_roi_sizes(self): self._roi_size_estimations = roi_size_estimations_per_label def _prepare_roi_info(self): - assert self._gt_dataset is not _unset - assert self._roi_size_estimations is not _unset - assert self._points_dataset is not _unset + assert self._gt_dataset is not unset + assert self._roi_size_estimations is not unset + assert self._points_dataset is not unset rois: list[boxes_from_points_task.RoiInfo] = [] for sample in self._points_dataset: @@ -773,7 +773,7 @@ def _mangle_filenames(self): Mangle filenames in the dataset to make them less recognizable by annotators and hide private dataset info """ - assert self._rois is not _unset + assert self._rois is not unset # TODO: maybe add different names for the same GT images in # different jobs to make them even less recognizable @@ -782,9 +782,9 @@ def _mangle_filenames(self): } def _prepare_job_layout(self): - assert self._rois is not _unset - assert self._bbox_point_mapping is not _unset - assert self._input_gt_dataset is not _unset + assert self._rois is not unset + assert self._bbox_point_mapping is not unset + assert self._input_gt_dataset is not unset # This list can be different from what is selected for validation input_gt_filenames = set(sample.media.path for sample in self._input_gt_dataset) @@ -898,10 +898,10 @@ def _draw_roi_point( ) def _extract_and_upload_rois(self): - assert self._points_dataset is not _unset - assert self._rois is not _unset - assert self._data_filenames is not _unset - assert self._roi_filenames is not _unset + assert self._points_dataset is not unset + assert self._rois is not unset + assert self._data_filenames is not unset + assert self._roi_filenames is not unset src_bucket = BucketAccessInfo.parse_obj(self.manifest.data.data_url) src_prefix = src_bucket.path @@ -1019,16 +1019,16 @@ def _prepare_gt_roi_dataset(self): assert len(self._gt_roi_dataset) == len(self._gt_roi_filenames) def _create_on_cvat(self): - assert self._roi_filenames_to_be_annotated is not _unset - assert self._gt_roi_filenames is not _unset - assert self._label_configuration is not _unset - assert self._gt_roi_dataset is not _unset + assert self._roi_filenames_to_be_annotated is not unset + assert self._gt_roi_filenames is not unset + assert self._label_configuration is not unset + assert self._gt_roi_dataset is not unset oracle_bucket = self.oracle_data_bucket # Register cloud storage on CVAT to pass user dataset cvat_cloud_storage = cvat_api.create_cloudstorage( - **_make_cvat_cloud_storage_params(oracle_bucket) + **make_cvat_cloud_storage_params(oracle_bucket) ) # Create a project diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/skeletons_from_boxes.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/skeletons_from_boxes.py index 042429442b..bf3de7fb70 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/skeletons_from_boxes.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/skeletons_from_boxes.py @@ -36,10 +36,10 @@ from collections.abc import Sequence from pathlib import Path - from src.core.manifest import TaskManifest + from src.core.manifest.v1 import JobManifest from src.core.tasks.cvat_formats import DM_GT_DATASET_FORMAT_MAPPING -from src.handlers.job_creation.builders.vision.base import _TaskBuilderBase +from src.handlers.job_creation.builders.vision.base import TaskBuilderBase from src.handlers.job_creation.exceptions import ( DatasetValidationError, InvalidCoordinates, @@ -48,50 +48,50 @@ TooFewSamples, ) from src.handlers.job_creation.utils import ( - _ExcludedAnnotationsInfo, - _make_cvat_cloud_storage_params, - _MaybeUnset, - _unset, + ExcludedAnnotationsInfo, + MaybeUnset, filter_image_files, + make_cvat_cloud_storage_params, strip_bucket_prefix, + unset, ) -class SkeletonsFromBoxesTaskBuilder(_TaskBuilderBase): +class SkeletonsFromBoxesTaskBuilder(TaskBuilderBase): @dataclass class _TaskParams: label_id: int roi_ids: list[int] roi_gt_ids: list[int] - def __init__(self, manifest: TaskManifest, escrow_address: str, chain_id: int) -> None: + def __init__(self, manifest: JobManifest, escrow_address: str, chain_id: int) -> None: super().__init__(manifest=manifest, escrow_address=escrow_address, chain_id=chain_id) - self._input_gt_data: _MaybeUnset[bytes] = _unset - self._input_boxes_data: _MaybeUnset[bytes] = _unset + self._input_gt_data: MaybeUnset[bytes] = unset + self._input_boxes_data: MaybeUnset[bytes] = unset - self._data_filenames: _MaybeUnset[Sequence[str]] = _unset - self._input_gt_dataset: _MaybeUnset[dm.Dataset] = _unset - self._gt_dataset: _MaybeUnset[dm.Dataset] = _unset - self._boxes_dataset: _MaybeUnset[dm.Dataset] = _unset + self._data_filenames: MaybeUnset[Sequence[str]] = unset + self._input_gt_dataset: MaybeUnset[dm.Dataset] = unset + self._gt_dataset: MaybeUnset[dm.Dataset] = unset + self._boxes_dataset: MaybeUnset[dm.Dataset] = unset - self._skeleton_bbox_mapping: _MaybeUnset[skeletons_from_boxes_task.SkeletonBboxMapping] = ( - _unset + self._skeleton_bbox_mapping: MaybeUnset[skeletons_from_boxes_task.SkeletonBboxMapping] = ( + unset ) - self._roi_infos: _MaybeUnset[skeletons_from_boxes_task.RoiInfos] = _unset - self._roi_info_by_id: _MaybeUnset[dict[int, skeletons_from_boxes_task.RoiInfo]] = _unset + self._roi_infos: MaybeUnset[skeletons_from_boxes_task.RoiInfos] = unset + self._roi_info_by_id: MaybeUnset[dict[int, skeletons_from_boxes_task.RoiInfo]] = unset - self._gt_points_per_label: _MaybeUnset[ + self._gt_points_per_label: MaybeUnset[ dict[tuple[int, str], Sequence[tuple[int, dm.Points]]] - ] = _unset + ] = unset "(skeleton_label_id, point_label_name) to [(skeleton_id, point), ...]" - self._roi_filenames: _MaybeUnset[dict[int, str]] = _unset + self._roi_filenames: MaybeUnset[dict[int, str]] = unset - self._task_params: _MaybeUnset[list[self._TaskParams]] = _unset + self._task_params: MaybeUnset[list[self._TaskParams]] = unset - self._excluded_gt_info: _MaybeUnset[_ExcludedAnnotationsInfo] = _unset - self._excluded_boxes_info: _MaybeUnset[_ExcludedAnnotationsInfo] = _unset + self._excluded_gt_info: MaybeUnset[ExcludedAnnotationsInfo] = unset + self._excluded_boxes_info: MaybeUnset[ExcludedAnnotationsInfo] = unset # Configuration / constants self.job_size_mult = skeletons_from_boxes_task.DEFAULT_ASSIGNMENT_SIZE_MULTIPLIER @@ -170,7 +170,7 @@ def _parse_dataset(self, annotation_file_data: bytes, dataset_format: str) -> dm return dm.Dataset.import_from(annotation_filename, format=dataset_format) def _parse_gt(self): - assert self._input_gt_data is not _unset + assert self._input_gt_data is not unset self._input_gt_dataset = self._parse_dataset( self._input_gt_data, @@ -178,7 +178,7 @@ def _parse_gt(self): ) def _parse_boxes(self): - assert self._input_boxes_data is not _unset + assert self._input_boxes_data is not unset self._boxes_dataset = self._parse_dataset( self._input_boxes_data, dataset_format=self.boxes_format @@ -268,7 +268,7 @@ def _validate_skeleton(skeleton: dm.Skeleton, *, sample_bbox: dm.Bbox): label_cat: dm.LabelCategories = self._input_gt_dataset.categories()[dm.AnnotationType.label] - excluded_gt_info = _ExcludedAnnotationsInfo() + excluded_gt_info = ExcludedAnnotationsInfo() excluded_samples = set() visited_ids = set() for gt_sample in self._input_gt_dataset: @@ -333,8 +333,8 @@ def _validate_skeleton(skeleton: dm.Skeleton, *, sample_bbox: dm.Bbox): self._excluded_gt_info = excluded_gt_info def _validate_gt(self): - assert self._data_filenames is not _unset - assert self._input_gt_dataset is not _unset + assert self._data_filenames is not unset + assert self._input_gt_dataset is not unset self._validate_gt_filenames() self._validate_gt_labels() @@ -379,7 +379,7 @@ def _validate_boxes_annotations(self): # noqa: PLR0912 self._boxes_dataset.transform(InstanceSegmentsToBbox) self._boxes_dataset.init_cache() - excluded_boxes_info = _ExcludedAnnotationsInfo() + excluded_boxes_info = ExcludedAnnotationsInfo() label_cat: dm.LabelCategories = self._boxes_dataset.categories()[dm.AnnotationType.label] @@ -519,8 +519,8 @@ def _validate_boxes_annotations(self): # noqa: PLR0912 self._excluded_boxes_info = excluded_boxes_info def _validate_boxes(self): - assert self._data_filenames is not _unset - assert self._boxes_dataset is not _unset + assert self._data_filenames is not unset + assert self._boxes_dataset is not unset self._validate_boxes_categories() self._validate_boxes_filenames() @@ -698,9 +698,9 @@ def _find_good_gt_skeletons( return matched_skeletons - assert self._data_filenames is not _unset - assert self._boxes_dataset is not _unset - assert self._input_gt_dataset is not _unset + assert self._data_filenames is not unset + assert self._boxes_dataset is not unset + assert self._input_gt_dataset is not unset assert [ label.name for label in self._input_gt_dataset.categories()[dm.AnnotationType.label] @@ -723,7 +723,7 @@ def _find_good_gt_skeletons( categories=self._input_gt_dataset.categories(), media_type=dm.Image ) - excluded_boxes_info = _ExcludedAnnotationsInfo() # local for the function + excluded_boxes_info = ExcludedAnnotationsInfo() # local for the function excluded_gt_info = self._excluded_gt_info gt_count_per_class = {} skeleton_bbox_mapping = {} # skeleton id -> bbox id @@ -808,8 +808,8 @@ def _find_good_gt_skeletons( self._skeleton_bbox_mapping = skeleton_bbox_mapping def _prepare_roi_infos(self): - assert self._gt_dataset is not _unset - assert self._boxes_dataset is not _unset + assert self._gt_dataset is not unset + assert self._boxes_dataset is not unset rois: list[skeletons_from_boxes_task.RoiInfo] = [] for sample in self._boxes_dataset: @@ -857,7 +857,7 @@ def _mangle_filenames(self): Mangle filenames in the dataset to make them less recognizable by annotators and hide private dataset info """ - assert self._roi_infos is not _unset + assert self._roi_infos is not unset # TODO: maybe add different names for the same GT images in # different jobs to make them even less recognizable @@ -878,9 +878,9 @@ def _job_val_frames_count(self) -> int: return super()._job_val_frames_count * self.job_size_mult def _prepare_task_params(self): - assert self._roi_infos is not _unset - assert self._skeleton_bbox_mapping is not _unset - assert self._input_gt_dataset is not _unset + assert self._roi_infos is not unset + assert self._skeleton_bbox_mapping is not unset + assert self._input_gt_dataset is not unset # This list can be different from what is selected for validation input_gt_filenames = set(sample.media.path for sample in self._input_gt_dataset) @@ -927,7 +927,7 @@ def _prepare_job_labels(self): self._point_labels[(skeleton_label.name, point_name)] = point_name def _prepare_gt_points_mapping(self): - assert self._gt_dataset is not _unset + assert self._gt_dataset is not unset self._gt_points_per_label = {} @@ -1060,8 +1060,8 @@ def _draw_roi_point(self, roi_image: np.ndarray, point: tuple[float, float]) -> ) def _extract_and_upload_rois(self): - assert self._roi_filenames is not _unset - assert self._roi_infos is not _unset + assert self._roi_filenames is not unset + assert self._roi_infos is not unset src_bucket = BucketAccessInfo.parse_obj(self.manifest.data.data_url) src_prefix = src_bucket.path @@ -1158,7 +1158,7 @@ def _prepare_gt_dataset_for_skeleton_point( point_label_name: str, skeleton_label_id: int, ) -> dm.Dataset: - assert self._gt_points_per_label is not _unset + assert self._gt_points_per_label is not unset # Change annotations to Points for validation in CVAT, # as annotators will use this annotation type @@ -1222,9 +1222,9 @@ def _setup_quality_settings(self, task_id: int, **overrides) -> None: super()._setup_quality_settings(task_id, **values) def _create_on_cvat(self): - assert self._task_params is not _unset - assert self._point_labels is not _unset - assert self._gt_points_per_label is not _unset + assert self._task_params is not unset + assert self._point_labels is not unset + assert self._gt_points_per_label is not unset def _task_params_label_key(ts): return ts.label_id @@ -1251,7 +1251,7 @@ def _task_params_label_key(ts): # Register cloud storage on CVAT to pass user dataset cvat_cloud_storage = cvat_api.create_cloudstorage( - **_make_cvat_cloud_storage_params(oracle_bucket) + **make_cvat_cloud_storage_params(oracle_bucket) ) segment_size = self._task_segment_size diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py index 4b5642288e..2d53143ea2 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py @@ -32,21 +32,21 @@ def __bool__(self) -> bool: return False -_unset = _Undefined() +unset = _Undefined() -_MaybeUnset = T | _Undefined +MaybeUnset = T | _Undefined @dataclass -class _ExcludedAnnotationInfo: +class ExcludedAnnotationInfo: message: str sample_id: str = field(kw_only=True) sample_subset: str = field(kw_only=True) @dataclass -class _ExcludedAnnotationsInfo: - messages: list[_ExcludedAnnotationInfo] = field(default_factory=list) +class ExcludedAnnotationsInfo: + messages: list[ExcludedAnnotationInfo] = field(default_factory=list) excluded_count: int = 0 "The number of excluded annotations. Can be different from len(messages)" @@ -55,7 +55,7 @@ class _ExcludedAnnotationsInfo: def add_message(self, message: str, *, sample_id: str, sample_subset: str): self.messages.append( - _ExcludedAnnotationInfo( + ExcludedAnnotationInfo( message=message, sample_id=sample_id, sample_subset=sample_subset ) ) @@ -74,7 +74,7 @@ def strip_bucket_prefix(data_filenames: list[str], prefix: str) -> list[str]: return [os.path.relpath(fn, prefix) for fn in data_filenames] -def make_label_configuration(manifest: TaskManifest) -> list[dict]: +def make_label_configuration(manifest: JobManifest) -> list[dict]: return [ { "name": label.name, @@ -84,7 +84,7 @@ def make_label_configuration(manifest: TaskManifest) -> list[dict]: ] -def _make_cvat_cloud_storage_params(bucket_info: BucketAccessInfo) -> dict: +def make_cvat_cloud_storage_params(bucket_info: BucketAccessInfo) -> dict: CLOUD_PROVIDER_TO_CVAT_CLOUD_PROVIDER = { CloudProviders.aws: "AWS_S3_BUCKET", CloudProviders.gcs: "GOOGLE_CLOUD_STORAGE", diff --git a/packages/examples/cvat/exchange-oracle/src/services/cloud/types.py b/packages/examples/cvat/exchange-oracle/src/services/cloud/types.py index 41ca984fa0..d6c9c2994d 100644 --- a/packages/examples/cvat/exchange-oracle/src/services/cloud/types.py +++ b/packages/examples/cvat/exchange-oracle/src/services/cloud/types.py @@ -10,8 +10,8 @@ import pydantic from httpx import URL -from src.core import manifest from src.core.config import Config, StorageConfig +from src.core.manifest import shared from src.services.cloud.gcs import DEFAULT_GCS_HOST from src.services.cloud.s3 import DEFAULT_S3_HOST from src.utils.enums import BetterEnumMeta @@ -174,14 +174,14 @@ def from_storage_config(cls, config: type[StorageConfig]) -> BucketAccessInfo: ) @classmethod - def from_bucket_url(cls, bucket_url: manifest.BucketUrl) -> BucketAccessInfo: + def from_bucket_url(cls, bucket_url: shared.BucketUrl) -> BucketAccessInfo: return cls._from_dict(bucket_url.model_dump()) @classmethod def parse_obj( - cls, data: str | type[StorageConfig] | manifest.BucketUrl | pydantic.AnyUrl + cls, data: str | type[StorageConfig] | shared.BucketUrl | pydantic.AnyUrl ) -> BucketAccessInfo: - if isinstance(data, manifest.BucketUrlBase): + if isinstance(data, shared.BucketUrlBase): return cls.from_bucket_url(data) elif isinstance(data, str): return cls.from_url(data) From 7fb34a72b91bc60ecd9bc78072a15f70c69f8ea6 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Thu, 2 Jul 2026 16:35:32 +0300 Subject: [PATCH 07/53] Update manifest imports for compatibility --- .../cvat/exchange-oracle/src/handlers/job_creation/utils.py | 2 +- .../cvat/exchange-oracle/src/handlers/job_export.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py index 2d53143ea2..fa3ccd6564 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py @@ -11,7 +11,7 @@ from src.services.cloud import CloudProviders if TYPE_CHECKING: - from src.core.manifest import TaskManifest + from src.core.manifest.v1 import JobManifest from src.services.cloud.utils import BucketAccessInfo LABEL_TYPE_MAPPING = { diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_export.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_export.py index 6f132e80c9..9abeae929a 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_export.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_export.py @@ -13,7 +13,7 @@ import src.utils.annotations as annotation_utils from src.core.annotation_meta import ANNOTATION_RESULTS_METAFILE_NAME, AnnotationMeta, JobMeta from src.core.config import Config -from src.core.manifest import TaskManifest +from src.core.manifest.v1 import JobManifest from src.core.storage import compose_data_bucket_filename from src.core.tasks import TaskTypes from src.core.tasks.cvat_formats import DM_DATASET_FORMAT_MAPPING @@ -75,7 +75,7 @@ def __init__( annotations: Sequence[FileDescriptor], merged_annotation: FileDescriptor, *, - manifest: TaskManifest, + manifest: JobManifest, project_images: list[Image], ) -> None: self.escrow_address = escrow_address @@ -594,7 +594,7 @@ def postprocess_annotations( annotations: Sequence[FileDescriptor], merged_annotation: FileDescriptor, *, - manifest: TaskManifest, + manifest: JobManifest, project_images: list[Image], ) -> None: """ From f2c2bebacf5054fa672be357463a19b3d60fffac Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Thu, 2 Jul 2026 16:56:17 +0300 Subject: [PATCH 08/53] Refactor simple task creation --- .../src/handlers/completed_escrows.py | 2 +- .../job_creation/builders/vision/basic.py | 58 ++++++++++++------- .../builders/vision/boxes_from_points.py | 4 +- .../src/handlers/job_creation/factory.py | 19 +++--- .../src/handlers/job_creation/utils.py | 2 +- .../exchange-oracle/src/services/exchange.py | 3 +- 6 files changed, 53 insertions(+), 35 deletions(-) diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/completed_escrows.py b/packages/examples/cvat/exchange-oracle/src/handlers/completed_escrows.py index 7af359cfb6..663ecb2c4f 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/completed_escrows.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/completed_escrows.py @@ -14,6 +14,7 @@ from src.chain.escrow import get_escrow_manifest, validate_escrow from src.core.annotation_meta import ANNOTATION_RESULTS_METAFILE_NAME, RESULTING_ANNOTATIONS_FILE from src.core.config import CronConfig, StorageConfig +from src.core.manifest import parse_manifest from src.core.oracle_events import ( ExchangeOracleEvent_EscrowRecorded, ExchangeOracleEvent_JobFinished, @@ -31,7 +32,6 @@ ) from src.models.cvat import Job, Project from src.services.cloud.types import BucketAccessInfo -from src.utils.assignments import parse_manifest def _download_with_retries( diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/basic.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/basic.py index d3701fdd1b..2ff83b47da 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/basic.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/basic.py @@ -2,6 +2,7 @@ import math import os +from abc import abstractmethod from tempfile import TemporaryDirectory from typing import TYPE_CHECKING @@ -34,34 +35,18 @@ MaybeUnset, filter_image_files, make_cvat_cloud_storage_params, - make_label_configuration, + make_cvat_label_configuration, unset, ) class SimpleTaskBuilder(TaskBuilderBase): """ - Handles task creation for the IMAGE_BOXES task type + Base builder for simple image tasks with a single annotation type and no extra processing. """ - def _upload_task_meta(self, gt_dataset: dm.Dataset): - layout = simple_task.TaskMetaLayout() - serializer = simple_task.TaskMetaSerializer() - - file_list = [] - file_list.append( - ( - serializer.serialize_gt_annotations(gt_dataset), - layout.GT_FILENAME, - ) - ) - - storage_client = self._make_cloud_storage_client(self._oracle_data_bucket) - for file_data, filename in file_list: - storage_client.create_file( - compose_data_bucket_filename(self.escrow_address, self.chain_id, filename), - file_data, - ) + @abstractmethod + def _upload_task_meta(self, gt_dataset: dm.Dataset) -> None: ... def _parse_gt_dataset( self, gt_file_data: bytes, *, add_prefix: str | None = None @@ -110,6 +95,9 @@ def _get_gt_filenames( return list(matched_gt_filenames) + def _prepare_cvat_label_configuration(self) -> list[dict]: + return make_cvat_label_configuration(self.manifest) + def build(self): manifest = self.manifest escrow_address = self.escrow_address @@ -133,7 +121,7 @@ def build(self): # Create task configuration gt_filenames = self._get_gt_filenames(gt_dataset, data_filenames, manifest=manifest) data_to_be_annotated = [f for f in data_filenames if f not in set(gt_filenames)] - label_configuration = make_label_configuration(manifest) + label_configuration = self._prepare_cvat_label_configuration() self._upload_task_meta(gt_dataset) @@ -207,6 +195,32 @@ def build(self): db_service.touch(session, Project, [project_id]) +class LabelBinaryTaskBuilder(TaskBuilderBase): + def build(self): + raise NotImplementedError + + +class BoxesTaskBuilder(SimpleTaskBuilder): + def _upload_task_meta(self, gt_dataset: dm.Dataset): + layout = simple_task.TaskMetaLayout() + serializer = simple_task.TaskMetaSerializer() + + file_list = [] + file_list.append( + ( + serializer.serialize_gt_annotations(gt_dataset), + layout.GT_FILENAME, + ) + ) + + storage_client = self._make_cloud_storage_client(self._oracle_data_bucket) + for file_data, filename in file_list: + storage_client.create_file( + compose_data_bucket_filename(self.escrow_address, self.chain_id, filename), + file_data, + ) + + class PointsTaskBuilder(SimpleTaskBuilder): def __init__(self, manifest: JobManifest, escrow_address: str, chain_id: int) -> None: super().__init__(manifest=manifest, escrow_address=escrow_address, chain_id=chain_id) @@ -321,7 +335,7 @@ def build(self): return super().build() -class PolygonTaskBuilder(SimpleTaskBuilder): +class PolygonTaskBuilder(BoxesTaskBuilder): def _setup_quality_settings(self, task_id: int, **overrides) -> None: values = {"iou_threshold": Config.cvat_config.iou_threshold, **overrides} super()._setup_quality_settings(task_id, **values) diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/boxes_from_points.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/boxes_from_points.py index ad1b1a5057..b1dac57322 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/boxes_from_points.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/boxes_from_points.py @@ -51,7 +51,7 @@ MaybeUnset, filter_image_files, make_cvat_cloud_storage_params, - make_label_configuration, + make_cvat_label_configuration, strip_bucket_prefix, unset, ) @@ -804,7 +804,7 @@ def _prepare_job_layout(self): ] def _prepare_label_configuration(self): - self._label_configuration = make_label_configuration(self.manifest) + self._label_configuration = make_cvat_label_configuration(self.manifest) def _upload_task_meta(self): layout = boxes_from_points_task.TaskMetaLayout() diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/factory.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/factory.py index a88362a99a..6cdc6a47b1 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/factory.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/factory.py @@ -1,19 +1,20 @@ from __future__ import annotations from src.chain.escrow import get_escrow_manifest +from src.core.manifest import parse_manifest from src.core.tasks import TaskTypes from src.handlers.job_creation.builders.audio.transcription import AudioTranscriptionTaskBuilder from src.handlers.job_creation.builders.vision.basic import ( + BoxesTaskBuilder, + LabelBinaryTaskBuilder, PointsTaskBuilder, PolygonTaskBuilder, - SimpleTaskBuilder, ) from src.handlers.job_creation.builders.vision.boxes_from_points import BoxesFromPointsTaskBuilder from src.handlers.job_creation.builders.vision.skeletons_from_boxes import ( SkeletonsFromBoxesTaskBuilder, ) from src.log import ROOT_LOGGER_NAME -from src.utils.assignments import parse_manifest from src.utils.logging import get_function_logger module_logger = f"{ROOT_LOGGER_NAME}.cron.cvat" @@ -24,17 +25,19 @@ def create_task(escrow_address: str, chain_id: int) -> None: manifest = parse_manifest(get_escrow_manifest(chain_id, escrow_address)) - if manifest.annotation.type in [TaskTypes.image_boxes, TaskTypes.image_label_binary]: - builder_type = SimpleTaskBuilder + if manifest.annotation.type is TaskTypes.image_boxes: + builder_type = BoxesTaskBuilder + elif manifest.annotation.type is TaskTypes.image_label_binary: + builder_type = LabelBinaryTaskBuilder elif manifest.annotation.type is TaskTypes.image_polygons: builder_type = PolygonTaskBuilder - elif manifest.annotation.type in [TaskTypes.image_points]: + elif manifest.annotation.type is TaskTypes.image_points: builder_type = PointsTaskBuilder - elif manifest.annotation.type in [TaskTypes.image_boxes_from_points]: + elif manifest.annotation.type is TaskTypes.image_boxes_from_points: builder_type = BoxesFromPointsTaskBuilder - elif manifest.annotation.type in [TaskTypes.image_skeletons_from_boxes]: + elif manifest.annotation.type is TaskTypes.image_skeletons_from_boxes: builder_type = SkeletonsFromBoxesTaskBuilder - elif manifest.annotation.type in [TaskTypes.audio_transcription]: + elif manifest.annotation.type is TaskTypes.audio_transcription: builder_type = AudioTranscriptionTaskBuilder else: raise Exception(f"Unsupported task type {manifest.annotation.type}") diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py index fa3ccd6564..24b5607db4 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py @@ -74,7 +74,7 @@ def strip_bucket_prefix(data_filenames: list[str], prefix: str) -> list[str]: return [os.path.relpath(fn, prefix) for fn in data_filenames] -def make_label_configuration(manifest: JobManifest) -> list[dict]: +def make_cvat_label_configuration(manifest: JobManifest) -> list[dict]: return [ { "name": label.name, diff --git a/packages/examples/cvat/exchange-oracle/src/services/exchange.py b/packages/examples/cvat/exchange-oracle/src/services/exchange.py index 9582b47e07..70d57a1320 100644 --- a/packages/examples/cvat/exchange-oracle/src/services/exchange.py +++ b/packages/examples/cvat/exchange-oracle/src/services/exchange.py @@ -4,12 +4,13 @@ import src.cvat.api_calls as cvat_api import src.services.cvat as cvat_service from src.chain.escrow import get_escrow_manifest +from src.core.manifest import parse_manifest from src.core.tasks import TaskTypes from src.core.types import JobStatuses, Networks, ProjectStatuses from src.db import SessionLocal from src.db.utils import ForUpdateParams from src.models.cvat import Job -from src.utils.assignments import get_default_assignment_timeout, parse_manifest +from src.utils.assignments import get_default_assignment_timeout from src.utils.requests import get_or_404 from src.utils.time import utcnow From eab8bbafee7282a2877d4178f1e32da7a40c2a56 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Thu, 2 Jul 2026 17:11:04 +0300 Subject: [PATCH 09/53] Factor out task configuration from complex task builders --- .../builders/vision/boxes_from_points.py | 132 +++++++------- .../builders/vision/skeletons_from_boxes.py | 171 +++++++++--------- 2 files changed, 157 insertions(+), 146 deletions(-) diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/boxes_from_points.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/boxes_from_points.py index b1dac57322..6de9910762 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/boxes_from_points.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/boxes_from_points.py @@ -4,6 +4,7 @@ import os import uuid from concurrent.futures import Future, ThreadPoolExecutor +from dataclasses import dataclass, field from itertools import groupby from math import ceil from pathlib import Path @@ -57,6 +58,51 @@ ) +@dataclass +class BoxesFromPointsConfig: + roi_file_ext: str = ".png" # supposed to be lossless and reasonably compressing + "File extension for RoI images, with leading dot (.) included" + + roi_size_mult: float = 1.1 + "Additional point ROI size multiplier" + + min_roi_size: tuple[int, int] = field( + default_factory=lambda: ( + Config.core_config.min_roi_size_w, + Config.core_config.min_roi_size_h, + ) + ) + "Minimum absolute ROI size, (w, h)" + + points_format: str = "coco_person_keypoints" + + embed_point_in_roi_image: bool = True + "Put a small point into the extracted RoI images for the original point" + + embedded_point_radius: int = 15 + min_embedded_point_radius_percent: float = 0.005 + max_embedded_point_radius_percent: float = 0.01 + embedded_point_color: tuple[int, int, int] = (0, 255, 255) + roi_background_color: tuple[int, int, int] = (245, 240, 242) # BGR - CVAT background color + + min_class_samples_for_roi_estimation: int = 25 + + max_class_roi_image_side_threshold: float = 0.5 + """ + The maximum allowed percent of the image for the estimated class RoI, + before the default RoI is used. Too big RoI estimations reduce the overall + prediction quality, making them unreliable. + """ + + max_discarded_threshold: float = 0.5 + """ + The maximum allowed percent of discarded + GT boxes, points, or samples for successful job launch + """ + + # TODO: consider adding an absolute number of minimum GT RoIs + + class BoxesFromPointsTaskBuilder(TaskBuilderBase): def __init__(self, manifest: JobManifest, escrow_address: str, chain_id: int) -> None: super().__init__(manifest=manifest, escrow_address=escrow_address, chain_id=chain_id) @@ -89,52 +135,7 @@ def __init__(self, manifest: JobManifest, escrow_address: str, chain_id: int) -> self._excluded_points_info: MaybeUnset[ExcludedAnnotationsInfo] = unset self._excluded_gt_info: MaybeUnset[ExcludedAnnotationsInfo] = unset - # Configuration / constants - # TODO: consider WebP if produced files are too big - self.roi_file_ext = ".png" # supposed to be lossless and reasonably compressing - "File extension for RoI images, with leading dot (.) included" - - self.list_display_threshold = 5 - "The maximum number of rendered list items in a message" - - self.roi_size_mult = 1.1 - "Additional point ROI size multiplier" - - self.min_roi_size = ( - Config.core_config.min_roi_size_w, - Config.core_config.min_roi_size_h, - ) - "Minimum absolute ROI size, (w, h)" - - self.points_format = "coco_person_keypoints" - - self.embed_point_in_roi_image = True - "Put a small point into the extracted RoI images for the original point" - - self.embedded_point_radius = 15 - self.min_embedded_point_radius_percent = 0.005 - self.max_embedded_point_radius_percent = 0.01 - self.embedded_point_color = (0, 255, 255) - self.roi_background_color = (245, 240, 242) # BGR - CVAT background color - - self.oracle_data_bucket = BucketAccessInfo.parse_obj(Config.storage_config) - - self.min_class_samples_for_roi_estimation = 25 - - self.max_class_roi_image_side_threshold = 0.5 - """ - The maximum allowed percent of the image for the estimated class RoI, - before the default RoI is used. Too big RoI estimations reduce the overall - prediction quality, making them unreliable. - """ - - self.max_discarded_threshold = 0.5 - """ - The maximum allowed percent of discarded - GT boxes, points, or samples for successful job launch - """ - - # TODO: probably, need to also add an absolute number of minimum GT RoIs + self.config = BoxesFromPointsConfig() def _download_input_data(self): data_bucket = BucketAccessInfo.parse_obj(self.manifest.data.data_url) @@ -175,7 +176,7 @@ def _parse_points(self): assert self._input_points_data is not unset self._points_dataset = self._parse_dataset( - self._input_points_data, dataset_format=self.points_format + self._input_points_data, dataset_format=self.config.points_format ) def _validate_gt_labels(self): @@ -276,7 +277,7 @@ def _validate_gt_annotations(self): ) if excluded_gt_info.excluded_count > ceil( - excluded_gt_info.total_count * self.max_discarded_threshold + excluded_gt_info.total_count * self.config.max_discarded_threshold ): raise TooFewSamples( "Too many GT boxes discarded, canceling job creation. Errors: {}".format( @@ -422,7 +423,7 @@ def _validate_skeleton(skeleton: dm.Skeleton, *, sample_bbox: dm.Bbox): ) if excluded_points_info.excluded_count > ceil( - excluded_points_info.total_count * self.max_discarded_threshold + excluded_points_info.total_count * self.config.max_discarded_threshold ): raise TooFewSamples( "Too many points discarded, canceling job creation. Errors: {}".format( @@ -620,7 +621,7 @@ def _find_good_gt_boxes( ) if excluded_gt_info.excluded_count > ceil( - excluded_gt_info.total_count * self.max_discarded_threshold + excluded_gt_info.total_count * self.config.max_discarded_threshold ): raise DatasetValidationError( "Too many GT boxes discarded ({} out of {}). " @@ -683,16 +684,16 @@ def _estimate_roi_sizes(self): roi_size_estimations_per_label = {} # label id -> (w, h) default_roi_size = (2, 2) # 2 will yield just the image size after halving for label_id, label_sizes in bbox_sizes_per_label.items(): - if len(label_sizes) < self.min_class_samples_for_roi_estimation: + if len(label_sizes) < self.config.min_class_samples_for_roi_estimation: estimated_size = default_roi_size classes_with_default_roi[label_id] = "too few GT provided" else: max_bbox = np.max(label_sizes, axis=0) - if np.any(max_bbox > self.max_class_roi_image_side_threshold): + if np.any(max_bbox > self.config.max_class_roi_image_side_threshold): estimated_size = default_roi_size classes_with_default_roi[label_id] = "estimated RoI is unreliable" else: - estimated_size = 2 * max_bbox * self.roi_size_mult + estimated_size = 2 * max_bbox * self.config.roi_size_mult roi_size_estimations_per_label[label_id] = estimated_size @@ -739,8 +740,8 @@ def _prepare_roi_info(self): roi_est_w, roi_est_h = self._roi_size_estimations[point_label_id] roi_est_w *= image_w roi_est_h *= image_h - roi_est_w = max(roi_est_w, self.min_roi_size[0]) - roi_est_h = max(roi_est_h, self.min_roi_size[1]) + roi_est_w = max(roi_est_w, self.config.min_roi_size[0]) + roi_est_h = max(roi_est_h, self.config.min_roi_size[1]) roi_left = max(0, original_point_x - int(roi_est_w / 2)) roi_top = max(0, original_point_y - int(roi_est_h / 2)) @@ -778,7 +779,7 @@ def _mangle_filenames(self): # TODO: maybe add different names for the same GT images in # different jobs to make them even less recognizable self._roi_filenames = { - roi.point_id: str(uuid.uuid4()) + self.roi_file_ext for roi in self._rois + roi.point_id: str(uuid.uuid4()) + self.config.roi_file_ext for roi in self._rois } def _prepare_job_layout(self): @@ -829,7 +830,7 @@ def _upload_task_meta(self): (serializer.serialize_roi_filenames(self._roi_filenames), layout.ROI_FILENAMES_FILENAME) ) - storage_client = self._make_cloud_storage_client(self.oracle_data_bucket) + storage_client = self._make_cloud_storage_client(self._oracle_data_bucket) for file_data, filename in file_list: storage_client.create_file( compose_data_bucket_filename(self.escrow_address, self.chain_id, filename), @@ -853,7 +854,7 @@ def _extract_roi( # Coords can be outside the original image # In this case a border should be added to RoI, so that the image was centered on bbox wrapped_roi_pixels = np.zeros((roi_info.roi_h, roi_info.roi_w, 3), dtype=np.float32) - wrapped_roi_pixels[:, :] = self.roi_background_color + wrapped_roi_pixels[:, :] = self.config.roi_background_color dst_y = max(-roi_info.roi_y, 0) dst_x = max(-roi_info.roi_x, 0) @@ -876,8 +877,11 @@ def _draw_roi_point( roi_r = (roi_info.roi_w**2 + roi_info.roi_h**2) ** 0.5 / 2 point_size = int( min( - self.max_embedded_point_radius_percent * roi_r, - max(self.embedded_point_radius, self.min_embedded_point_radius_percent * roi_r), + self.config.max_embedded_point_radius_percent * roi_r, + max( + self.config.embedded_point_radius, + self.config.min_embedded_point_radius_percent * roi_r, + ), ) ) @@ -893,7 +897,7 @@ def _draw_roi_point( roi_pixels, center, point_size, - self.embedded_point_color, + self.config.embedded_point_color, cv2.FILLED, ) @@ -905,7 +909,7 @@ def _extract_and_upload_rois(self): src_bucket = BucketAccessInfo.parse_obj(self.manifest.data.data_url) src_prefix = src_bucket.path - dst_bucket = self.oracle_data_bucket + dst_bucket = self._oracle_data_bucket src_client = self._make_cloud_storage_client(src_bucket) dst_client = self._make_cloud_storage_client(dst_bucket) @@ -942,7 +946,7 @@ def process_file(filename: str, image_pixels: np.ndarray): for roi_info in image_roi_infos: roi_pixels = self._extract_roi(image_pixels, roi_info) - if self.embed_point_in_roi_image: + if self.config.embed_point_in_roi_image: roi_pixels = self._draw_roi_point(roi_pixels, roi_info) roi_filename = self._roi_filenames[roi_info.point_id] @@ -1024,7 +1028,7 @@ def _create_on_cvat(self): assert self._label_configuration is not unset assert self._gt_roi_dataset is not unset - oracle_bucket = self.oracle_data_bucket + oracle_bucket = self._oracle_data_bucket # Register cloud storage on CVAT to pass user dataset cvat_cloud_storage = cvat_api.create_cloudstorage( diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/skeletons_from_boxes.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/skeletons_from_boxes.py index bf3de7fb70..b7f9c9e155 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/skeletons_from_boxes.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/skeletons_from_boxes.py @@ -5,7 +5,7 @@ import random import uuid from concurrent.futures import Future, ThreadPoolExecutor -from dataclasses import dataclass +from dataclasses import dataclass, field from itertools import chain, groupby from math import ceil from queue import Queue @@ -57,13 +57,62 @@ ) -class SkeletonsFromBoxesTaskBuilder(TaskBuilderBase): - @dataclass - class _TaskParams: - label_id: int - roi_ids: list[int] - roi_gt_ids: list[int] +@dataclass +class _TaskParams: + label_id: int + roi_ids: list[int] + roi_gt_ids: list[int] + + +@dataclass +class SkeletonsFromBoxesConfig: + job_size_mult: int = skeletons_from_boxes_task.DEFAULT_ASSIGNMENT_SIZE_MULTIPLIER + "Job size multiplier" + + roi_file_ext: str = ".png" # supposed to be lossless and reasonably compressing + "File extension for RoI images, with leading dot (.) included" + + roi_size_mult: float = 1.1 + "Additional point ROI size multiplier" + + min_roi_size: tuple[int, int] = field( + default_factory=lambda: ( + Config.core_config.min_roi_size_w, + Config.core_config.min_roi_size_h, + ) + ) + "Minimum absolute ROI size, (w, h)" + + boxes_format: str = "coco_person_keypoints" + + embed_bbox_in_roi_image: bool = True + "Put a bbox into the extracted skeleton RoI images" + embed_tile_border: bool = True + + embedded_point_radius: int = 15 + min_embedded_point_radius_percent: float = 0.005 + max_embedded_point_radius_percent: float = 0.01 + embedded_point_color: tuple[int, int, int] = (0, 255, 255) + + roi_embedded_bbox_color: tuple[int, int, int] = (0, 255, 255) # BGR + roi_background_color: tuple[int, int, int] = (245, 240, 242) # BGR - CVAT background color + + min_label_gt_samples: int = 2 # TODO: find good threshold + + max_discarded_threshold: float = 0.5 + """ + The maximum allowed percent of discarded + GT annotations or samples for successful job launch + """ + + gt_id_attribute: str = "object_id" + "An additional way to match GT skeletons with input boxes" + + # TODO: consider adding an absolute number of minimum GT RoIs + + +class SkeletonsFromBoxesTaskBuilder(TaskBuilderBase): def __init__(self, manifest: JobManifest, escrow_address: str, chain_id: int) -> None: super().__init__(manifest=manifest, escrow_address=escrow_address, chain_id=chain_id) @@ -88,60 +137,12 @@ def __init__(self, manifest: JobManifest, escrow_address: str, chain_id: int) -> self._roi_filenames: MaybeUnset[dict[int, str]] = unset - self._task_params: MaybeUnset[list[self._TaskParams]] = unset + self._task_params: MaybeUnset[list[_TaskParams]] = unset self._excluded_gt_info: MaybeUnset[ExcludedAnnotationsInfo] = unset self._excluded_boxes_info: MaybeUnset[ExcludedAnnotationsInfo] = unset - # Configuration / constants - self.job_size_mult = skeletons_from_boxes_task.DEFAULT_ASSIGNMENT_SIZE_MULTIPLIER - "Job size multiplier" - - # TODO: consider WebP if produced files are too big - self.roi_file_ext = ".png" # supposed to be lossless and reasonably compressing - "File extension for RoI images, with leading dot (.) included" - - self.list_display_threshold = 5 - "The maximum number of rendered list items in a message" - - self.roi_size_mult = 1.1 - "Additional point ROI size multiplier" - - self.min_roi_size = ( - Config.core_config.min_roi_size_w, - Config.core_config.min_roi_size_h, - ) - "Minimum absolute ROI size, (w, h)" - - self.boxes_format = "coco_person_keypoints" - - self.embed_bbox_in_roi_image = True - "Put a bbox into the extracted skeleton RoI images" - - self.embed_tile_border = True - - self.embedded_point_radius = 15 - self.min_embedded_point_radius_percent = 0.005 - self.max_embedded_point_radius_percent = 0.01 - self.embedded_point_color = (0, 255, 255) - - self.roi_embedded_bbox_color = (0, 255, 255) # BGR - self.roi_background_color = (245, 240, 242) # BGR - CVAT background color - - self.oracle_data_bucket = BucketAccessInfo.parse_obj(Config.storage_config) - - self.min_label_gt_samples = 2 # TODO: find good threshold - - self.max_discarded_threshold = 0.5 - """ - The maximum allowed percent of discarded - GT annotations or samples for successful job launch - """ - - self.gt_id_attribute = "object_id" - "An additional way to match GT skeletons with input boxes" - - # TODO: probably, need to also add an absolute number of minimum GT RoIs per class + self.config = SkeletonsFromBoxesConfig() def _download_input_data(self): data_bucket = BucketAccessInfo.parse_obj(self.manifest.data.data_url) @@ -181,7 +182,7 @@ def _parse_boxes(self): assert self._input_boxes_data is not unset self._boxes_dataset = self._parse_dataset( - self._input_boxes_data, dataset_format=self.boxes_format + self._input_boxes_data, dataset_format=self.config.boxes_format ) def _validate_gt_labels(self): @@ -320,7 +321,7 @@ def _validate_skeleton(skeleton: dm.Skeleton, *, sample_bbox: dm.Bbox): ) if excluded_gt_info.excluded_count > ceil( - excluded_gt_info.total_count * self.max_discarded_threshold + excluded_gt_info.total_count * self.config.max_discarded_threshold ): raise TooFewSamples( "Too many GT skeletons discarded, canceling job creation. Errors: {}".format( @@ -493,7 +494,7 @@ def _validate_boxes_annotations(self): # noqa: PLR0912 ) if excluded_boxes_info.excluded_count > ceil( - excluded_boxes_info.total_count * self.max_discarded_threshold + excluded_boxes_info.total_count * self.config.max_discarded_threshold ): raise TooFewSamples( "Too many boxes discarded, canceling job creation. Errors: {}".format( @@ -586,8 +587,10 @@ def _find_unambiguous_matches( ) and ( # a way to customize matching if the default method is too rough - not (bbox_id := input_bbox.attributes.get(self.gt_id_attribute)) - or not (skeleton_id := gt_skeleton.attributes.get(self.gt_id_attribute)) + not (bbox_id := input_bbox.attributes.get(self.config.gt_id_attribute)) + or not ( + skeleton_id := gt_skeleton.attributes.get(self.config.gt_id_attribute) + ) or bbox_id == skeleton_id ) for gt_skeleton in gt_skeletons @@ -770,7 +773,7 @@ def _find_good_gt_skeletons( ) if excluded_gt_info.excluded_count > ceil( - self.max_discarded_threshold * excluded_gt_info.total_count + self.config.max_discarded_threshold * excluded_gt_info.total_count ): raise DatasetValidationError( "Too many GT skeletons discarded ({} out of {}). " @@ -795,7 +798,7 @@ def _find_good_gt_skeletons( labels_with_few_gt = [ gt_label_cat[label_id] for label_id, label_count in gt_count_per_class.items() - if label_count < self.min_label_gt_samples + if label_count < self.config.min_label_gt_samples ] if labels_with_few_gt: raise DatasetValidationError( @@ -822,10 +825,10 @@ def _prepare_roi_infos(self): original_bbox_cx = int(bbox.x + bbox.w / 2) original_bbox_cy = int(bbox.y + bbox.h / 2) - roi_w = ceil(bbox.w * self.roi_size_mult) - roi_h = ceil(bbox.h * self.roi_size_mult) - roi_w = max(roi_w, self.min_roi_size[0]) - roi_h = max(roi_h, self.min_roi_size[1]) + roi_w = ceil(bbox.w * self.config.roi_size_mult) + roi_h = ceil(bbox.h * self.config.roi_size_mult) + roi_w = max(roi_w, self.config.min_roi_size[0]) + roi_h = max(roi_h, self.config.min_roi_size[1]) roi_x = original_bbox_cx - int(roi_w / 2) roi_y = original_bbox_cy - int(roi_h / 2) @@ -862,7 +865,8 @@ def _mangle_filenames(self): # TODO: maybe add different names for the same GT images in # different jobs to make them even less recognizable self._roi_filenames = { - roi_info.bbox_id: str(uuid.uuid4()) + self.roi_file_ext for roi_info in self._roi_infos + roi_info.bbox_id: str(uuid.uuid4()) + self.config.roi_file_ext + for roi_info in self._roi_infos } @property @@ -871,11 +875,11 @@ def _task_segment_size(self) -> int: # is supposed to be simple and the assignment is expected # to take little time with the default job size. # Then, we add a percent of job tiles for validation, keeping the requested ratio. - return super()._task_segment_size * self.job_size_mult + return super()._task_segment_size * self.config.job_size_mult @property def _job_val_frames_count(self) -> int: - return super()._job_val_frames_count * self.job_size_mult + return super()._job_val_frames_count * self.config.job_size_mult def _prepare_task_params(self): assert self._roi_infos is not unset @@ -888,7 +892,7 @@ def _prepare_task_params(self): sample.attributes["id"]: sample.media.path for sample in self._boxes_dataset } - task_params: list[self._TaskParams] = [] + task_params: list[_TaskParams] = [] segment_size = self._task_segment_size for label_id, _ in enumerate(self.manifest.annotation.labels): label_gt_roi_ids = set( @@ -908,7 +912,7 @@ def _prepare_task_params(self): task_params.extend( [ - self._TaskParams( + _TaskParams( label_id=label_id, roi_ids=task_data_roi_ids, roi_gt_ids=label_gt_roi_ids ) for task_data_roi_ids in take_by( @@ -982,7 +986,7 @@ def _upload_task_meta(self): (serializer.serialize_point_labels(self._point_labels), layout.POINT_LABELS_FILENAME) ) - storage_client = self._make_cloud_storage_client(self.oracle_data_bucket) + storage_client = self._make_cloud_storage_client(self._oracle_data_bucket) for file_data, filename in file_list: storage_client.create_file( compose_data_bucket_filename(self.escrow_address, self.chain_id, filename), @@ -1006,7 +1010,7 @@ def _extract_roi( # Coords can be outside the original image # In this case a border should be added to RoI, so that the image was centered on bbox wrapped_roi_pixels = np.zeros((roi_info.roi_h, roi_info.roi_w, 3), dtype=np.float32) - wrapped_roi_pixels[:, :] = self.roi_background_color + wrapped_roi_pixels[:, :] = self.config.roi_background_color dst_y = max(-roi_info.roi_y, 0) dst_x = max(-roi_info.roi_x, 0) @@ -1028,7 +1032,7 @@ def _draw_roi_bbox(self, roi_image: np.ndarray, bbox: dm.Bbox) -> np.ndarray: roi_image, tuple(map(int, (roi_cx - bbox.w / 2, roi_cy - bbox.h / 2))), tuple(map(int, (roi_cx + bbox.w / 2, roi_cy + bbox.h / 2))), - self.roi_embedded_bbox_color, + self.config.roi_embedded_bbox_color, 1, cv2.LINE_4, ) @@ -1037,8 +1041,11 @@ def _draw_roi_point(self, roi_image: np.ndarray, point: tuple[float, float]) -> roi_r = (roi_image.shape[0] ** 2 + roi_image.shape[1] ** 2) ** 0.5 / 2 radius = int( min( - self.max_embedded_point_radius_percent * roi_r, - max(self.embedded_point_radius, self.min_embedded_point_radius_percent * roi_r), + self.config.max_embedded_point_radius_percent * roi_r, + max( + self.config.embedded_point_radius, + self.config.min_embedded_point_radius_percent * roi_r, + ), ) ) @@ -1054,7 +1061,7 @@ def _draw_roi_point(self, roi_image: np.ndarray, point: tuple[float, float]) -> roi_image, tuple(map(int, (point[0], point[1]))), radius, - self.embedded_point_color, + self.config.embedded_point_color, -1, cv2.LINE_4, ) @@ -1065,7 +1072,7 @@ def _extract_and_upload_rois(self): src_bucket = BucketAccessInfo.parse_obj(self.manifest.data.data_url) src_prefix = src_bucket.path - dst_bucket = self.oracle_data_bucket + dst_bucket = self._oracle_data_bucket src_client = self._make_cloud_storage_client(src_bucket) dst_client = self._make_cloud_storage_client(dst_bucket) @@ -1110,7 +1117,7 @@ def process_file(filename: str, image_pixels: np.ndarray): for roi_info in image_roi_infos: roi_pixels = self._extract_roi(image_pixels, roi_info) - if self.embed_bbox_in_roi_image: + if self.config.embed_bbox_in_roi_image: roi_pixels = self._draw_roi_bbox(roi_pixels, bbox_by_id[roi_info.bbox_id]) roi_pixels = self._draw_roi_point( roi_pixels, (roi_info.point_x, roi_info.point_y) @@ -1247,7 +1254,7 @@ def _task_params_label_key(ts): for skeleton_label_id, skeleton_label in enumerate(self.manifest.annotation.labels) } - oracle_bucket = self.oracle_data_bucket + oracle_bucket = self._oracle_data_bucket # Register cloud storage on CVAT to pass user dataset cvat_cloud_storage = cvat_api.create_cloudstorage( From 7ceaca10ca9fbb35271a4f03c954fc202ea67d5d Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Thu, 2 Jul 2026 19:50:20 +0300 Subject: [PATCH 10/53] Define audio task spec --- .../src/core/tasks/__init__.py | 4 +- .../tasks/audio_transcription/__init__.py | 0 .../core/tasks/audio_transcription/spec.py | 131 ++++++++++++++++++ .../exchange-oracle/src/core/tasks/errors.py | 2 + 4 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/__init__.py create mode 100644 packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py create mode 100644 packages/examples/cvat/exchange-oracle/src/core/tasks/errors.py diff --git a/packages/examples/cvat/exchange-oracle/src/core/tasks/__init__.py b/packages/examples/cvat/exchange-oracle/src/core/tasks/__init__.py index 2245fece0a..3f24f51e5c 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/tasks/__init__.py +++ b/packages/examples/cvat/exchange-oracle/src/core/tasks/__init__.py @@ -1,5 +1,7 @@ +from src.core.tasks import errors from src.core.tasks.types import TaskTypes __all__ = [ - "TaskTypes" + "TaskTypes", + "errors", ] diff --git a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/__init__.py b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py new file mode 100644 index 0000000000..29d60e5899 --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py @@ -0,0 +1,131 @@ +from __future__ import annotations + +from datetime import timedelta +from enum import Enum +from typing import TYPE_CHECKING + +from pydantic import AnyUrl, BaseModel, Field + +from src.core.manifest.shared import BucketUrl # noqa: TC001 # runtime-needed by pydantic +from src.core.tasks.errors import UnsupportedManifestError +from src.utils.enums import BetterEnumMeta + +if TYPE_CHECKING: + from src.core.manifest.v2 import JobManifest + + +class NormalizerPreset(str, Enum, metaclass=BetterEnumMeta): + basic = "basic" + + +class TranscriptionTaskData(BaseModel): + media_url: AnyUrl | BucketUrl + regions_url: AnyUrl | BucketUrl + gt_url: AnyUrl | BucketUrl + + +class TranscriptionTaskValidation(BaseModel): + target_metric: str + target_score: float + + +class TranscriptionDetails(BaseModel): + assignment_time_limit: timedelta | None = None + "Maximum time to complete an assignment" + + validation_overhead: float = 0.2 + "Honeypot share of the assignment duration" + + normalizer: NormalizerPreset | None = NormalizerPreset.basic + "Text normalization for transcriptions" + + min_composition: tuple[int, int] = (2, 2) + "(gt, ds): minimum number of honeypot and DS regions per assignment" + + roi_min_duration: timedelta = timedelta(seconds=5) + "Minimum presented region duration. Smaller segments are bundled together" + + roi_max_duration: timedelta = timedelta(seconds=120) + "Maximum presented region duration. Used for input validation" + + roi_join_pause: timedelta = timedelta(milliseconds=700) + "Silence inserted between concatenated regions in an assignment clip" + + standard_assignment_duration: timedelta = timedelta(seconds=60) + "Target audio duration per assignment" + + # TODO: add honeypot rotation later + # max_validation_gt: float = 0.05 + # "Maximum share of GT regions used as honeypots. The rest is used for honeypot rotation" + + max_discarded_gt: float = 0.5 + "Maximum share of GT regions that may be discarded before job creation fails" + + sample_rate: int = 48000 + "Sample rate of the generated assignment clips, Hz" + + transcription_attr_name: str = "transcription" + "The name of the output attribute for transcriptions in CVAT tasks and annotations" + + +class TranscriptionTaskSpecification(BaseModel): + data: TranscriptionTaskData + + labels: list[str] + shared_attributes: list = Field(default_factory=list) + + validation: TranscriptionTaskValidation + + details: TranscriptionDetails = Field(default_factory=TranscriptionDetails) + + +def parse_audio_manifest(manifest: JobManifest) -> TranscriptionTaskSpecification: + """ + Produce the task specification from a manifest, encapsulating manifest-version differences. + + Only v2 manifests are supported. + """ + + version = getattr(manifest, "version", 1) + if version != 2: + raise UnsupportedManifestError( + f"Audio transcription requires a v2 manifest, got version {version}" + ) + + annotation = manifest.annotation + validation = annotation.validation + manifest_details = annotation.details + + # Only pass the details fields the manifest provides; pydantic fills the rest with defaults + # and coerces raw values (e.g. normalizer str -> NormalizerPreset). + details_fields: dict = {} + if manifest_details is not None: + if manifest_details.max_segment_duration is not None: + details_fields["roi_max_duration"] = timedelta( + seconds=manifest_details.max_segment_duration + ) + if manifest_details.validation_overhead is not None: + details_fields["validation_overhead"] = manifest_details.validation_overhead / 100.0 + if manifest_details.min_composition is not None: + details_fields["min_composition"] = ( + manifest_details.min_composition.gt, + manifest_details.min_composition.ds, + ) + details_fields["normalizer"] = manifest_details.normalizer + if manifest_details.max_time is not None: + details_fields["assignment_time_limit"] = timedelta(seconds=manifest_details.max_time) + + return TranscriptionTaskSpecification( + data=TranscriptionTaskData( + media_url=manifest.data.media_url, + regions_url=manifest.data.regions_url, + gt_url=manifest.data.gt_url, + ), + labels=[label.name for label in annotation.labels], + shared_attributes=list(annotation.shared_attributes), + validation=TranscriptionTaskValidation( + target_metric=validation.target_metric.value, + target_score=validation.target_score, + ), + details=TranscriptionDetails(**details_fields), + ) diff --git a/packages/examples/cvat/exchange-oracle/src/core/tasks/errors.py b/packages/examples/cvat/exchange-oracle/src/core/tasks/errors.py new file mode 100644 index 0000000000..68ddebabb1 --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/core/tasks/errors.py @@ -0,0 +1,2 @@ +class UnsupportedManifestError(Exception): + pass \ No newline at end of file From e566857dd98526c3911c09ba4b06d08dc7cad147 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Fri, 3 Jul 2026 19:49:36 +0300 Subject: [PATCH 11/53] Implement audio transcription task builder Add the audio_transcription task builder end to end: parse the input regions/GT TSVs, select ground-truth honeypots and bundle DS regions, mix them into assignments, cut and concatenate per-assignment clips via ffmpeg, upload clips + input copies + region/assignment meta to the oracle bucket, and create one CVAT task per assignment. - core/tasks/audio_transcription: task spec, domain meta types, and meta (de)serialization - utils/audio: ffmpeg cut_and_concat / probe_duration helpers - debug: route the audio builder through the dev cloud-storage host patch - optionally dump per-region DS/GT cuts when DEBUG is enabled Co-Authored-By: Claude Opus 4.8 (1M context) --- .../core/tasks/audio_transcription/meta.py | 181 +++++ .../core/tasks/audio_transcription/spec.py | 3 + .../exchange-oracle/src/cvat/api_calls.py | 9 +- .../exchange-oracle/src/entrypoints/debug.py | 1 + .../builders/audio/transcription.py | 738 +++++++++++++++++- .../builders/vision/boxes_from_points.py | 2 +- .../builders/vision/skeletons_from_boxes.py | 2 +- .../src/handlers/job_creation/exceptions.py | 26 + .../src/handlers/job_creation/utils.py | 25 - .../cvat/exchange-oracle/src/utils/audio.py | 122 +++ 10 files changed, 1076 insertions(+), 33 deletions(-) create mode 100644 packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py create mode 100644 packages/examples/cvat/exchange-oracle/src/utils/audio.py diff --git a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py new file mode 100644 index 0000000000..815bdec524 --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py @@ -0,0 +1,181 @@ +"""Domain types and meta persistence for audio transcription tasks. + +Regions are mixed across input files into assignments, so every region carries its own source +provenance. The persisted mappings (regions + assignments) let each region be located back in its +source media after mixing. +""" + +from __future__ import annotations + +from enum import Enum +from typing import TYPE_CHECKING + +from attrs import Factory, frozen +from datumaro.util import dump_json, parse_json + +from src.utils.enums import BetterEnumMeta + +if TYPE_CHECKING: + from datetime import timedelta + + +@frozen(kw_only=True) +class InputRegion: + """A region to annotate, parsed from the input regions TSV.""" + + filename: str + start: timedelta + stop: timedelta + label: str | None = None + row_idx: int + + @property + def id(self) -> str: + return f"{self.filename}:{self.row_idx}" + + +@frozen(kw_only=True) +class InputGtRegion: + """A ground-truth region, parsed from the input GT TSV.""" + + filename: str + start: timedelta + stop: timedelta + label: str | None = None + text: str = "" + row_idx: int + + @property + def id(self) -> str: + return f"{self.filename}:{self.row_idx}" + + +class RegionKind(str, Enum, metaclass=BetterEnumMeta): + gt = "gt" # ground-truth honeypot region + ds = "ds" # regular dataset region to annotate + + +@frozen(kw_only=True) +class PresentedRegion: + "A bundle of consecutive input ROIs from one source file, presented as a single region." + + source_filename: str + start: float # bundle span (file-time seconds): first ROI start .. last ROI stop + stop: float + kind: RegionKind + input_ids: list[str] = Factory(list) + + @property + def duration(self) -> float: + return self.stop - self.start + + +@frozen(kw_only=True) +class PlacedRegion: + "A presented region placed in an assignment" + + index: int # 0-based position within the assignment clip + source_filename: str + file_start: float + file_stop: float + clip_start: float + clip_stop: float + kind: RegionKind = RegionKind.ds + input_ids: list[str] = Factory(list) # ids of the input ROIs this bundle was built from + + +@frozen(kw_only=True) +class Assignment: + """One CVAT task: a single concatenated clip built from ``placed`` regions.""" + + id: str + clip_filename: str + clip_duration: float + placed: list[PlacedRegion] = Factory(list) + + +def _region_to_dict(r: PresentedRegion) -> dict: + return { + "source_filename": r.source_filename, + "start": r.start, + "stop": r.stop, + "kind": r.kind.value, + "input_ids": list(r.input_ids), + } + + +def _region_from_dict(d: dict) -> PresentedRegion: + return PresentedRegion( + source_filename=d["source_filename"], + start=d["start"], + stop=d["stop"], + kind=RegionKind(d["kind"]), + input_ids=list(d.get("input_ids", [])), + ) + + +def _placed_to_dict(r: PlacedRegion) -> dict: + return { + "index": r.index, + "source_filename": r.source_filename, + "file_start": r.file_start, + "file_stop": r.file_stop, + "clip_start": r.clip_start, + "clip_stop": r.clip_stop, + "kind": r.kind.value, + "input_ids": list(r.input_ids), + } + + +def _placed_from_dict(d: dict) -> PlacedRegion: + return PlacedRegion( + index=d["index"], + source_filename=d["source_filename"], + file_start=d["file_start"], + file_stop=d["file_stop"], + clip_start=d["clip_start"], + clip_stop=d["clip_stop"], + kind=RegionKind(d["kind"]), + input_ids=list(d.get("input_ids", [])), + ) + + +class TaskMetaLayout: + GT_FILENAME = "gt.tsv" + REGIONS_TSV_FILENAME = "regions.tsv" + REGIONS_FILENAME = "regions.json" + ASSIGNMENTS_FILENAME = "assignments.json" + DEBUG_DS_CUTS_DIR = "debug/ds_cuts" # per-region DS clips (debug only) + DEBUG_GT_CUTS_DIR = "debug/gt_cuts" # per-region GT clips (debug only) + + +class TaskMetaSerializer: + def serialize_regions(self, regions: list[PresentedRegion]) -> bytes: + return dump_json([_region_to_dict(r) for r in regions]) + + def parse_regions(self, data: bytes) -> list[PresentedRegion]: + return [_region_from_dict(d) for d in parse_json(data)] + + def serialize_assignments(self, assignments: list[Assignment]) -> bytes: + return dump_json( + [ + { + "id": a.id, + "clip_filename": a.clip_filename, + "clip_dur": a.clip_duration, + "placed": [_placed_to_dict(p) for p in a.placed], + } + for a in assignments + ] + ) + + def parse_assignments(self, data: bytes) -> list[Assignment]: + return [ + Assignment( + id=d["id"], + clip_filename=d["clip_filename"], + clip_duration=d["clip_dur"], + placed=[_placed_from_dict(p) for p in d.get("placed", [])], + ) + for d in parse_json(data) + ] diff --git a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py index 29d60e5899..c9888e5670 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py +++ b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py @@ -67,6 +67,9 @@ class TranscriptionDetails(BaseModel): transcription_attr_name: str = "transcription" "The name of the output attribute for transcriptions in CVAT tasks and annotations" + random_seed: int = 0 + "Seed for deterministic honeypot selection and assignment mixing" + class TranscriptionTaskSpecification(BaseModel): data: TranscriptionTaskData diff --git a/packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py b/packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py index 1d4103b111..42903bbb78 100644 --- a/packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py +++ b/packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py @@ -50,6 +50,7 @@ class LabelType(str, Enum, metaclass=BetterEnumMeta): points = "points" rectangle = "rectangle" polygon = "polygon" + interval = "interval" class WebhookEventType(str, Enum, metaclass=BetterEnumMeta): @@ -340,15 +341,19 @@ def create_task( project_id: int, name: str, *, - segment_size: int, + segment_size: int | None = None, ) -> models.TaskRead: logger = logging.getLogger("app") with get_api_client() as api_client: + kwargs = {} + if segment_size is not None: + kwargs["segment_size"] = segment_size + task_write_request = models.TaskWriteRequest( name=name, project_id=project_id, overlap=0, - segment_size=segment_size, + **kwargs, ) try: (task_info, _) = api_client.tasks_api.create(task_write_request) diff --git a/packages/examples/cvat/exchange-oracle/src/entrypoints/debug.py b/packages/examples/cvat/exchange-oracle/src/entrypoints/debug.py index d902e95e1a..8e62f9ef9d 100644 --- a/packages/examples/cvat/exchange-oracle/src/entrypoints/debug.py +++ b/packages/examples/cvat/exchange-oracle/src/entrypoints/debug.py @@ -55,6 +55,7 @@ def patched_make_cvat_cloud_storage_params(bucket_info: BucketAccessInfo) -> dic "src.handlers.job_creation.builders.vision.basic.make_cvat_cloud_storage_params", "src.handlers.job_creation.builders.vision.boxes_from_points.make_cvat_cloud_storage_params", "src.handlers.job_creation.builders.vision.skeletons_from_boxes.make_cvat_cloud_storage_params", + "src.handlers.job_creation.builders.audio.transcription.make_cvat_cloud_storage_params", ] with ExitStack() as stack: for target in patch_targets: diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py index 60d415cddb..230e213042 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py @@ -1,14 +1,744 @@ from __future__ import annotations +import csv +import io +import random +import time +import uuid +from collections import Counter +from datetime import timedelta +from math import ceil +from pathlib import Path, PurePosixPath +from tempfile import TemporaryDirectory +from typing import TYPE_CHECKING + +import src.cvat.api_calls as cvat_api +import src.services.cvat as db_service +from src.core.config import Config +from src.core.storage import compose_data_bucket_filename +from src.core.tasks import TaskTypes +from src.core.tasks.audio_transcription.meta import ( + Assignment, + InputGtRegion, + InputRegion, + PlacedRegion, + PresentedRegion, + RegionKind, + TaskMetaLayout, + TaskMetaSerializer, +) +from src.core.tasks.audio_transcription.spec import parse_audio_manifest +from src.core.types import TaskStatuses +from src.db import SessionLocal from src.handlers.job_creation.builders.vision.base import TaskBuilderBase +from src.handlers.job_creation.exceptions import ( + DatasetValidationError, + ExcludedAnnotationsInfo, + TooFewSamples, +) +from src.handlers.job_creation.utils import ( + MaybeUnset, + make_cvat_cloud_storage_params, + unset, +) +from src.models.cvat import Project +from src.services.cloud.utils import BucketAccessInfo +from src.utils.audio import RegionCut, cut_and_concat, probe_duration +from src.utils.logging import format_sequence +if TYPE_CHECKING: + from src.core.manifest import ManifestBase -class AudioTranscriptionTaskBuilder(TaskBuilderBase): + +# --------------------------------------------------------------------------- # +# Assignment construction +# --------------------------------------------------------------------------- # + + +def _bundle_regions( + regions: list[InputRegion], *, min_duration: float +) -> list[tuple[float, float, list[InputRegion]]]: + """ + Bundle consecutive input regions (sorted by start) into groups >= min_duration; + a region already >= min_duration stays alone. Returns (start_s, stop_s, members) per bundle. + """ + + def span(group: list[InputRegion]) -> float: + return group[-1].stop.total_seconds() - group[0].start.total_seconds() + + out: list[list[InputRegion]] = [] + bundle: list[InputRegion] | None = None + for region in regions: + if bundle is None: + bundle = [region] + elif span(bundle) < min_duration: + bundle.append(region) + else: + out.append(bundle) + bundle = [region] + + if bundle is not None: + if out and span(bundle) < min_duration: + out[-1].extend(bundle) + else: + out.append(bundle) + + return [(g[0].start.total_seconds(), g[-1].stop.total_seconds(), g) for g in out] + + +def select_honeypots( + regions: list[InputGtRegion], + *, + source_filename: str, + val_fraction: float, + min_duration: float, + random_seed: int = 0, +) -> list[PresentedRegion]: + """ + Bundle GT cues into >= min_duration groups, then pick ~val_fraction of the bundles (by count) + as honeypots. A bundle spans its first cue start to its last cue stop (including inter-cue + gaps); we never pad into unannotated media, so a bundle may stay shorter than min_duration when + the source GT does not provide enough context. """ - Handles task creation for the AUDIO_TRANSCRIPTION task type. - Stub: audio transcription task creation is not implemented yet. + regions = sorted(regions, key=lambda r: r.start) + bundles = _bundle_regions(regions, min_duration=min_duration) + + rng = random.Random(random_seed) # noqa: S311 # not cryptographic + idxs = list(range(len(bundles))) + rng.shuffle(idxs) + n_select = ceil(val_fraction * len(bundles)) + chosen = sorted(idxs[:n_select]) + + honeypots: list[PresentedRegion] = [] + for bundle_idx in chosen: + s, e, members = bundles[bundle_idx] + honeypots.append( + PresentedRegion( + source_filename=source_filename, + start=s, + stop=e, + kind=RegionKind.gt, + input_ids=[m.id for m in members], + ) + ) + return honeypots + + +def plan_assignments( + ds_regions: list[PresentedRegion], + honeypots: list[PresentedRegion], + *, + composition: tuple[int, int], + validation_overhead: float, + max_audio_duration: float, + random_seed: int = 0, +) -> list[list[PresentedRegion]]: """ + Mix DS and honeypots into assignments. Returns ordered region lists. + """ + + # 1. shuffle DS + rng = random.Random(random_seed) # noqa: S311 # not cryptographic + ds_order = list(range(len(ds_regions))) + rng.shuffle(ds_order) + + # 2. Reservoir-pack each assignment up to the DS budget (>= K_min), inject least-used honeypots + # up to the validation overhead (>= H_min), then shuffle region order. Deterministic. + ds_budget = max_audio_duration * (1.0 - validation_overhead) + hp_target = validation_overhead * max_audio_duration + h_min, k_min = composition + + hp_use: Counter = Counter() + assignments: list[list[PresentedRegion]] = [] + di = 0 + while di < len(ds_order): + chosen: list[PresentedRegion] = [] + ds_dur = 0.0 + while di < len(ds_order): + r = ds_regions[ds_order[di]] + d = r.duration + if len(chosen) >= k_min and ds_dur + d > ds_budget: + break + chosen.append(r) + ds_dur += d + di += 1 + if len(chosen) < k_min: + break # DS stream exhausted - can't form a full assignment + + hp_dur = 0.0 + # least-used honeypots first, randomized within each use-count tier + hp_ranked = sorted(range(len(honeypots)), key=lambda i: (hp_use[i], rng.random())) + for n_hp, i in enumerate(hp_ranked): + if (hp_dur >= hp_target and n_hp >= h_min) or n_hp >= len(honeypots): + break + chosen.append(honeypots[i]) + hp_dur += honeypots[i].duration + hp_use[i] += 1 + + rng.shuffle(chosen) + assignments.append(chosen) + + return assignments + + +def place_regions( + regions: list[PresentedRegion], *, pause_s: float +) -> tuple[list[PlacedRegion], float]: + """Clip-time placement of concatenated regions (region durations + inter-region pauses).""" + placed: list[PlacedRegion] = [] + t = 0.0 + n = len(regions) + for i, region in enumerate(regions): + dur = region.duration + placed.append( + PlacedRegion( + index=i, + source_filename=region.source_filename, + file_start=region.start, + file_stop=region.stop, + clip_start=t, + clip_stop=t + dur, + kind=region.kind, + input_ids=region.input_ids, + ) + ) + t += dur + if i < n - 1: + t += pause_s + return placed, t + + +def _overlaps(a0: float, a1: float, b0: float, b1: float) -> bool: + return a0 < b1 and b0 < a1 + + +# --------------------------------------------------------------------------- # +# Builder +# --------------------------------------------------------------------------- # + + +class AudioTranscriptionTaskBuilder(TaskBuilderBase): + def __init__(self, manifest: ManifestBase, escrow_address: str, chain_id: int) -> None: + super().__init__(manifest=manifest, escrow_address=escrow_address, chain_id=chain_id) + + self.spec = parse_audio_manifest(self.manifest) + + self._media_dir: MaybeUnset[Path] = unset + self._media_paths: MaybeUnset[dict[str, Path]] = unset # source_filename -> local path + self._regions_by_file: MaybeUnset[dict[str, list[InputRegion]]] = unset + self._gt_by_file: MaybeUnset[dict[str, list[InputGtRegion]]] = unset + self._regions_tsv_data: MaybeUnset[bytes] = unset # raw input regions TSV + self._gt_tsv_data: MaybeUnset[bytes] = unset # raw input GT TSV + self._ds_regions: MaybeUnset[list[PresentedRegion]] = unset + self._honeypots: MaybeUnset[list[PresentedRegion]] = unset + self._assignments: MaybeUnset[list[Assignment]] = unset + self._excluded_gt_info: MaybeUnset[ExcludedAnnotationsInfo] = unset + + self._meta_serializer = TaskMetaSerializer() + self._meta_layout = TaskMetaLayout() + + # -- input ------------------------------------------------------------- # + + def _download_input_data(self) -> None: + media_bucket = BucketAccessInfo.parse_obj(self.spec.data.media_url) + regions_bucket = BucketAccessInfo.parse_obj(self.spec.data.regions_url) + gt_bucket = BucketAccessInfo.parse_obj(self.spec.data.gt_url) + + media_client = self._make_cloud_storage_client(media_bucket) + regions_client = self._make_cloud_storage_client(regions_bucket) + gt_client = self._make_cloud_storage_client(gt_bucket) + + regions_tsv_data = regions_client.download_file(regions_bucket.path) + gt_tsv_data = gt_client.download_file(gt_bucket.path) + regions_by_file = _parse_regions_tsv(regions_tsv_data) + gt_by_file = _parse_gt_tsv(gt_tsv_data) + + # download only the referenced media files + referenced = set(regions_by_file) | set(gt_by_file) + media_dir = Path(self.exit_stack.enter_context(TemporaryDirectory())) + media_paths: dict[str, Path] = {} + for filename in sorted(referenced): + local_path = media_dir / PurePosixPath(filename) + local_path.parent.mkdir(parents=True, exist_ok=True) + + data = media_client.download_file(_bucket_key(media_bucket.path, filename)) + local_path.write_bytes(data) + media_paths[filename] = local_path + + self._media_dir = media_dir + self._media_paths = media_paths + self._regions_by_file = regions_by_file + self._gt_by_file = gt_by_file + self._regions_tsv_data = regions_tsv_data + self._gt_tsv_data = gt_tsv_data + + # -- region pools ------------------------------------------------------ # + + def _prepare_rois(self) -> None: + self._build_gt_regions() + self._build_ds_regions() + + def _build_gt_regions(self) -> None: + assert self._gt_by_file is not unset + assert self._media_paths is not unset + + excluded_gt = ExcludedAnnotationsInfo() + honeypots: list[PresentedRegion] = [] + + for filename in sorted(self._gt_by_file): + file_duration = probe_duration(self._media_paths[filename]) + valid_regions = _validate_gt_regions( + self._gt_by_file[filename], + filename, + file_duration, + self.spec.details.roi_max_duration.total_seconds(), + excluded_gt, + ) + if not valid_regions: + continue + + honeypots.extend( + select_honeypots( + valid_regions, + source_filename=filename, + val_fraction=1.0, # use all GT as honeypots (rotation is a future feature) + min_duration=self.spec.details.roi_min_duration.total_seconds(), + random_seed=self.spec.details.random_seed, + ) + ) + + if excluded_gt.excluded_count: + self.logger.warning( + "Some GT regions were excluded due to errors found: \n{}".format( + format_sequence([m.message for m in excluded_gt.messages], separator="\n") + ) + ) + if excluded_gt.excluded_count > ceil( + excluded_gt.total_count * self.spec.details.max_discarded_gt + ): + raise TooFewSamples( + "Too many GT regions discarded, canceling job creation. Errors: {}".format( + format_sequence([m.message for m in excluded_gt.messages]) + ) + ) + + h_min = self.spec.details.min_composition[0] + if len(honeypots) < h_min: + raise TooFewSamples( + f"Too few ground-truth honeypots after filtering ({len(honeypots)}), " + f"at least {h_min} required." + ) + + self._honeypots = honeypots + self._excluded_gt_info = excluded_gt + + # -- dataset regions --------------------------------------------------- # + + def _build_ds_regions(self) -> None: + assert self._regions_by_file is not unset + assert self._gt_by_file is not unset + + # Regions and ground truth are should not normally overlap. + # Keep all DS regions regardless and only warn if overlaps are found. + gt_spans_by_file: dict[str, list[tuple[float, float]]] = {} + for filename, gt_regions in self._gt_by_file.items(): + gt_spans_by_file[filename] = [ + (gt.start.total_seconds(), gt.stop.total_seconds()) for gt in gt_regions + ] + + ds_regions: list[PresentedRegion] = [] + overlap_count = 0 + for filename in sorted(self._regions_by_file): + gt_spans = gt_spans_by_file.get(filename, []) + rows = sorted(self._regions_by_file[filename], key=lambda r: r.start) + overlap_count += sum( + any( + _overlaps(r.start.total_seconds(), r.stop.total_seconds(), gs, ge) + for gs, ge in gt_spans + ) + for r in rows + ) + bundled = _bundle_regions( + rows, + min_duration=self.spec.details.roi_min_duration.total_seconds(), + ) + for start, stop, members in bundled: + ds_regions.append( + PresentedRegion( + source_filename=filename, + start=start, + stop=stop, + kind=RegionKind.ds, + input_ids=[m.id for m in members], + ) + ) + + if overlap_count: + self.logger.warning( + f"{overlap_count} input region(s) overlap ground-truth regions; " + "regions and ground truth are expected to be disjoint." + ) + + self._ds_regions = ds_regions + + # -- assignments ------------------------------------------------------- # + + def _prepare_assignments(self) -> None: + assert self._ds_regions is not unset + assert self._honeypots is not unset + assert self._media_dir is not unset + assert self._media_paths is not unset + + region_lists = plan_assignments( + self._ds_regions, + self._honeypots, + composition=self.spec.details.min_composition, + validation_overhead=self.spec.details.validation_overhead, + max_audio_duration=self.spec.details.standard_assignment_duration.total_seconds(), + random_seed=self.spec.details.random_seed, + ) + if not region_lists: + raise DatasetValidationError( + "No assignments could be formed from the provided regions and ground truth" + ) + + pause = self.spec.details.roi_join_pause + pause_ms = round(pause.total_seconds() * 1000) + assignments: list[Assignment] = [] + for regions in region_lists: + assignment_id = uuid.uuid4().hex + clip_filename = f"{assignment_id}.wav" + clip_path = self._media_dir / clip_filename + + cuts = [ + RegionCut(media=self._media_paths[r.source_filename], start=r.start, stop=r.stop) + for r in regions + ] + cut_and_concat( + cuts, clip_path, pause_ms=pause_ms, sample_rate=self.spec.details.sample_rate + ) + + placed, clip_duration = place_regions(regions, pause_s=pause.total_seconds()) + assignments.append( + Assignment( + id=assignment_id, + clip_filename=clip_filename, + clip_duration=clip_duration, + placed=placed, + ) + ) + self._assignments = assignments + + # -- persistence ------------------------------------------------------- # + + def _upload_clips_and_meta(self) -> None: + assert self._assignments is not unset + assert self._media_dir is not unset + assert self._media_paths is not unset + assert self._ds_regions is not unset + assert self._honeypots is not unset + assert self._regions_tsv_data is not unset + assert self._gt_tsv_data is not unset + + storage_client = self._make_cloud_storage_client(self._oracle_data_bucket) + + for assignment in self._assignments: + clip_path = self._media_dir / assignment.clip_filename + storage_client.create_file( + compose_data_bucket_filename( + self.escrow_address, self.chain_id, assignment.clip_filename + ), + clip_path.read_bytes(), + ) + + # copies of the raw inputs, for provenance / downstream scoring + storage_client.create_file( + compose_data_bucket_filename( + self.escrow_address, self.chain_id, self._meta_layout.REGIONS_TSV_FILENAME + ), + self._regions_tsv_data, + ) + storage_client.create_file( + compose_data_bucket_filename( + self.escrow_address, self.chain_id, self._meta_layout.GT_FILENAME + ), + self._gt_tsv_data, + ) + + pool = list(self._ds_regions) + list(self._honeypots) + storage_client.create_file( + compose_data_bucket_filename( + self.escrow_address, self.chain_id, self._meta_layout.REGIONS_FILENAME + ), + self._meta_serializer.serialize_regions(pool), + ) + storage_client.create_file( + compose_data_bucket_filename( + self.escrow_address, self.chain_id, self._meta_layout.ASSIGNMENTS_FILENAME + ), + self._meta_serializer.serialize_assignments(self._assignments), + ) + + if Config.debug: + self._upload_debug_cuts(storage_client) + + def _upload_debug_cuts(self, storage_client) -> None: + """Upload each DS/GT region as its own clip into debug dirs, for inspection.""" + assert self._media_dir is not unset + assert self._media_paths is not unset + assert self._ds_regions is not unset + assert self._honeypots is not unset + + pairs = ( + (self._meta_layout.DEBUG_DS_CUTS_DIR, self._ds_regions), + (self._meta_layout.DEBUG_GT_CUTS_DIR, self._honeypots), + ) + for subdir, regions in pairs: + for i, region in enumerate(regions): + stem = PurePosixPath(region.source_filename).stem + cut_name = f"{i:04d}_{stem}_{region.start:.2f}-{region.stop:.2f}.wav" + cut_path = self._media_dir / cut_name + cut_and_concat( + [ + RegionCut( + media=self._media_paths[region.source_filename], + start=region.start, + stop=region.stop, + ) + ], + cut_path, + pause_ms=0, + sample_rate=self.spec.details.sample_rate, + ) + storage_client.create_file( + compose_data_bucket_filename( + self.escrow_address, self.chain_id, f"{subdir}/{cut_name}" + ), + cut_path.read_bytes(), + ) + + # -- CVAT + DB --------------------------------------------------------- # + + def _create_on_cvat(self) -> None: + assert self._assignments is not unset + + escrow_address = self.escrow_address + chain_id = self.chain_id + + label_configuration = _make_cvat_audio_label_configuration( + self.spec.labels, + transcription_attr_name=self.spec.details.transcription_attr_name, + shared_attributes=self.spec.shared_attributes, + ) + + cvat_project = cvat_api.create_project(escrow_address, labels=label_configuration) + cvat_webhook = cvat_api.create_cvat_webhook(cvat_project.id) + cloud_storage = cvat_api.create_cloudstorage( + **make_cvat_cloud_storage_params(self._oracle_data_bucket) + ) + + with SessionLocal.begin() as session: + self.logger.info( + "Task creation for escrow '%s': will create %s assignments", + escrow_address, + len(self._assignments), + ) + db_service.create_escrow_creation( + session, + escrow_address=escrow_address, + chain_id=chain_id, + total_jobs=len(self._assignments), + ) + + project_id = db_service.create_project( + session, + cvat_project.id, + cloud_storage.id, + TaskTypes.audio_transcription, + escrow_address, + chain_id, + self._oracle_data_bucket.to_url(), + cvat_webhook_id=cvat_webhook.id, + ) + db_service.get_project_by_id(session, project_id, for_update=True) # lock the row + + for assignment in self._assignments: + clip_key = compose_data_bucket_filename( + escrow_address, chain_id, assignment.clip_filename + ) + cvat_task = cvat_api.create_task(cvat_project.id, escrow_address) + task_id = db_service.create_task( + session, cvat_task.id, cvat_project.id, TaskStatuses[cvat_task.status] + ) + db_service.get_task_by_id(session, task_id, for_update=True) # lock the row + + cvat_api.put_task_data(cvat_task.id, cloud_storage.id, filenames=[clip_key]) + db_service.create_data_upload(session, cvat_task.id) + + db_service.touch(session, Project, [project_id]) + + # -- orchestration ----------------------------------------------------- # def build(self) -> None: - raise NotImplementedError("Audio transcription task creation is not implemented yet") + self._download_input_data() + + self._prepare_rois() + self._prepare_assignments() + + self._upload_clips_and_meta() + self._create_on_cvat() + + +# --------------------------------------------------------------------------- # +# TSV parsing +# --------------------------------------------------------------------------- # + + +def _parse_time(v: str) -> timedelta: + for fmt_string in ("%H:%M:%S.%f", "%H:%M:%S"): + try: + parsed_time = time.strptime(v, fmt_string) + if fmt_string.endswith(".%f"): + # time.strptime drops sub-second precision, so recover it from the raw string. + frac = v.split(".", maxsplit=1)[-1] + microseconds = int(frac.ljust(6, "0")[:6]) + else: + microseconds = 0 + except ValueError: + continue + else: + return timedelta( + hours=parsed_time.tm_hour, + minutes=parsed_time.tm_min, + seconds=parsed_time.tm_sec, + microseconds=microseconds, + ) + + raise ValueError(f"Failed to parse timestamp '{v}'") + + +def _read_tsv(data: bytes) -> list[dict[str, str]]: + reader = csv.DictReader(io.StringIO(data.decode("utf-8")), delimiter="\t") + return list(reader) + + +def _parse_regions_tsv(data: bytes) -> dict[str, list[InputRegion]]: + by_file: dict[str, list[InputRegion]] = {} + for row_idx, row in enumerate(_read_tsv(data)): + filename = row["filename"].strip() + by_file.setdefault(filename, []).append( + InputRegion( + filename=filename, + row_idx=row_idx, + start=_parse_time(row["start"]), + stop=_parse_time(row["stop"]), + label=(row.get("label") or "").strip() or None, + ) + ) + return by_file + + +def _parse_gt_tsv(data: bytes) -> dict[str, list[InputGtRegion]]: + by_file: dict[str, list[InputGtRegion]] = {} + for row_idx, row in enumerate(_read_tsv(data)): + filename = row["filename"].strip() + by_file.setdefault(filename, []).append( + InputGtRegion( + filename=filename, + row_idx=row_idx, + start=_parse_time(row["start"]), + stop=_parse_time(row["stop"]), + label=(row.get("label") or "").strip() or None, + text=(row.get("text") or "").strip(), + ) + ) + return by_file + + +# --------------------------------------------------------------------------- # +# Helpers +# --------------------------------------------------------------------------- # + +def _validate_gt_regions( + regions: list[InputGtRegion], + filename: str, + file_duration: float, + max_duration: float, + excluded: ExcludedAnnotationsInfo, +) -> list[InputGtRegion]: + valid: list[InputGtRegion] = [] + for region in regions: + start = region.start.total_seconds() + stop = region.stop.total_seconds() + + excluded.total_count += 1 + + if not (0 <= start < stop <= file_duration + 1e-6): + excluded.excluded_count += 1 + excluded.add_message( + f"GT region '{region.row_idx}' ({region.label}) [{start:.3f}, {stop:.3f}] " + "- invalid timestamps", + sample_id=filename, + sample_subset="", + ) + continue + + if stop - start > max_duration: + excluded.excluded_count += 1 + excluded.add_message( + f"GT region '{region.row_idx}' ({region.label}) [{start:.3f}, {stop:.3f}] " + f"- longer than the maximum region duration ({max_duration:.3f}s)", + sample_id=filename, + sample_subset="", + ) + continue + + valid.append(region) + + return valid + + +def _bucket_key(prefix: str, filename: str) -> str: + """Join a bucket prefix and a filename into a POSIX-style object key.""" + return str(PurePosixPath(prefix, filename)) + +def _make_cvat_audio_label_configuration( + labels: list[str], + *, + transcription_attr_name: str, + shared_attributes: list | None = None, +) -> list[dict]: + """Interval labels (one per speaker) each carrying a mutable transcription text attribute, + plus any shared attributes. Used for audio_transcription tasks.""" + shared_attributes = shared_attributes or [] + + def _shared_attr_spec(attr) -> dict: + return { + "name": attr.name, + "mutable": True, + "input_type": attr.type or "text", + "values": list(attr.values), + "default_value": attr.default_value, + } + + label_config = [] + for name in labels: + attributes = [ + { + "name": transcription_attr_name, + "mutable": True, + "input_type": "text", + "values": [], + "default_value": "", + }, + *(_shared_attr_spec(a) for a in shared_attributes), + ] + label_config.append( + { + "name": name, + "type": cvat_api.LabelType.interval.value, + "attributes": attributes, + } + ) + return label_config diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/boxes_from_points.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/boxes_from_points.py index 6de9910762..7dba049790 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/boxes_from_points.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/boxes_from_points.py @@ -41,6 +41,7 @@ from src.handlers.job_creation.builders.vision.base import TaskBuilderBase from src.handlers.job_creation.exceptions import ( DatasetValidationError, + ExcludedAnnotationsInfo, InvalidCategories, InvalidCoordinates, InvalidImageInfo, @@ -48,7 +49,6 @@ TooFewSamples, ) from src.handlers.job_creation.utils import ( - ExcludedAnnotationsInfo, MaybeUnset, filter_image_files, make_cvat_cloud_storage_params, diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/skeletons_from_boxes.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/skeletons_from_boxes.py index b7f9c9e155..31558143d6 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/skeletons_from_boxes.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/skeletons_from_boxes.py @@ -42,13 +42,13 @@ from src.handlers.job_creation.builders.vision.base import TaskBuilderBase from src.handlers.job_creation.exceptions import ( DatasetValidationError, + ExcludedAnnotationsInfo, InvalidCoordinates, InvalidImageInfo, MismatchingAnnotations, TooFewSamples, ) from src.handlers.job_creation.utils import ( - ExcludedAnnotationsInfo, MaybeUnset, filter_image_files, make_cvat_cloud_storage_params, diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/exceptions.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/exceptions.py index bf737ef20f..c1c5708ffb 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/exceptions.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/exceptions.py @@ -1,5 +1,7 @@ from __future__ import annotations +from dataclasses import dataclass, field + class DatasetValidationError(Exception): pass @@ -27,3 +29,27 @@ class InvalidCoordinates(DatasetValidationError): class InvisibleSkeletonError(DatasetValidationError): pass + + +@dataclass +class ExcludedAnnotationInfo: + message: str + sample_id: str = field(kw_only=True) + sample_subset: str = field(kw_only=True) + + +@dataclass +class ExcludedAnnotationsInfo: + messages: list[ExcludedAnnotationInfo] = field(default_factory=list) + + excluded_count: int = 0 + "The number of excluded annotations. Can be different from len(messages)" + + total_count: int = 0 + + def add_message(self, message: str, *, sample_id: str, sample_subset: str): + self.messages.append( + ExcludedAnnotationInfo( + message=message, sample_id=sample_id, sample_subset=sample_subset + ) + ) diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py index 24b5607db4..b9b957d580 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py @@ -1,7 +1,6 @@ from __future__ import annotations import os -from dataclasses import dataclass, field from typing import TYPE_CHECKING, TypeVar from datumaro.util.image import IMAGE_EXTENSIONS @@ -37,30 +36,6 @@ def __bool__(self) -> bool: MaybeUnset = T | _Undefined -@dataclass -class ExcludedAnnotationInfo: - message: str - sample_id: str = field(kw_only=True) - sample_subset: str = field(kw_only=True) - - -@dataclass -class ExcludedAnnotationsInfo: - messages: list[ExcludedAnnotationInfo] = field(default_factory=list) - - excluded_count: int = 0 - "The number of excluded annotations. Can be different from len(messages)" - - total_count: int = 0 - - def add_message(self, message: str, *, sample_id: str, sample_subset: str): - self.messages.append( - ExcludedAnnotationInfo( - message=message, sample_id=sample_id, sample_subset=sample_subset - ) - ) - - def is_image(path: str) -> bool: trunk, ext = os.path.splitext(os.path.basename(path)) return trunk and ext.lower() in IMAGE_EXTENSIONS diff --git a/packages/examples/cvat/exchange-oracle/src/utils/audio.py b/packages/examples/cvat/exchange-oracle/src/utils/audio.py new file mode 100644 index 0000000000..e627269f26 --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/utils/audio.py @@ -0,0 +1,122 @@ +"""ffmpeg/ffprobe helpers for audio task creation. + +ffmpeg and ffprobe must be available on PATH (system dependency). +Ported from the asr-qa-research prototype (experiments/pipeline_test/oracle.py::cut_clip). +""" + +from __future__ import annotations + +import subprocess +from dataclasses import dataclass +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pathlib import Path + + +class FfmpegError(RuntimeError): + pass + + +@dataclass(frozen=True) +class RegionCut: + """A slice [start, stop) (seconds) to take from a source media file.""" + + media: Path + start: float + stop: float + + @property + def duration(self) -> float: + return self.stop - self.start + + +def probe_duration(path: Path) -> float: + """Return media duration in seconds""" + + cmd = [ + "ffprobe", + "-v", + "error", + "-show_entries", + "format=duration", + "-of", + "csv=p=0", + str(path), + ] + result = subprocess.run(cmd, capture_output=True, text=True, check=False) # noqa: S603 + if result.returncode != 0: + raise FfmpegError(f"ffprobe failed for {path}:\n{result.stderr[-800:]}") + + return float(result.stdout.strip()) + + +def cut_and_concat( + regions: list[RegionCut], + out_path: Path, + *, + pause_ms: int, + sample_rate: int, +) -> None: + """ + Trim each region from its own media and concatenate them into a single mono clip, + separated by ``pause_ms`` of silence. Output is ``sample_rate`` Hz mono WAV. + + Assignments mix regions from multiple source files, so each region is trimmed from its + own input (distinct medias map to distinct ffmpeg input indices). + """ + if not regions: + raise ValueError("cut_and_concat requires at least one region") + + out_path.parent.mkdir(parents=True, exist_ok=True) + pause_s = pause_ms / 1000.0 + + # distinct source files -> ffmpeg input indices + medias: list[Path] = [] + idx_of: dict[str, int] = {} + for region in regions: + key = str(region.media) + if key not in idx_of: + idx_of[key] = len(medias) + medias.append(region.media) + + filter_parts: list[str] = [] + segment_labels: list[str] = [] + n = len(regions) + for i, region in enumerate(regions): + inp = idx_of[str(region.media)] + filter_parts.append( + f"[{inp}:a]atrim={region.start:.3f}:{region.stop:.3f},asetpts=PTS-STARTPTS," + f"aresample={sample_rate},aformat=channel_layouts=mono[s{i}]" + ) + segment_labels.append(f"[s{i}]") + if i < n - 1: + filter_parts.append(f"aevalsrc=0:d={pause_s}:s={sample_rate}[g{i}]") + segment_labels.append(f"[g{i}]") + + filter_parts.append("".join(segment_labels) + f"concat=n={len(segment_labels)}:v=0:a=1[out]") + + inputs: list[str] = [] + for media in medias: + inputs += ["-i", str(media)] + + cmd = [ + "ffmpeg", + "-y", + "-hide_banner", + "-v", + "error", + *inputs, + "-filter_complex", + ";".join(filter_parts), + "-map", + "[out]", + "-ar", + str(sample_rate), + "-ac", + "1", + str(out_path), + ] + result = subprocess.run(cmd, capture_output=True, text=True, check=False) # noqa: S603 + if result.returncode != 0: + raise FfmpegError(f"ffmpeg cut/concat failed for {out_path}:\n{result.stderr[-800:]}") From 6c2cc7736e11c4fa46e544fb2e87db1a9d554d95 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Fri, 3 Jul 2026 19:49:38 +0300 Subject: [PATCH 12/53] Bump cvat-sdk to 2.69.0 Match the CVAT server version used by the local deployment; the older 2.37.0 client rejected newer server responses. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/examples/cvat/exchange-oracle/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/examples/cvat/exchange-oracle/pyproject.toml b/packages/examples/cvat/exchange-oracle/pyproject.toml index 428e3a87f4..7d2970cda7 100644 --- a/packages/examples/cvat/exchange-oracle/pyproject.toml +++ b/packages/examples/cvat/exchange-oracle/pyproject.toml @@ -15,7 +15,7 @@ psycopg2 = "^2.9.6" sqlalchemy-utils = "^0.41.1" alembic = "^1.11.1" httpx = "^0.24.1" -cvat-sdk = "2.37.0" +cvat-sdk = "2.69.0" sqlalchemy = "^2.0.16" apscheduler = "^3.10.1" xmltodict = "^0.13.0" From e066ecf874dc079cbc5f5e35abcb6aab7a4b77de Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Fri, 3 Jul 2026 21:12:32 +0300 Subject: [PATCH 13/53] Refine audio task builder: bundling caps, clips subdir, debug dumps - Bound region bundling: cap the join gap at min_duration and the merged span at max_duration, so bundles no longer bridge long silences over sparse input (kept assignment durations near the target). - Store assignment clips under a clips/ subdir on the oracle bucket; reference the full key in task creation (CVAT requires it). - Upload copies of the input regions/GT TSVs alongside the meta. - Optionally dump per-region DS/GT cuts into debug dirs when DEBUG is set. - Add a configurable random_seed to the task spec, forwarded to honeypot selection and assignment mixing. - Forward the annotation user_guide from the manifest to the CVAT project. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../core/tasks/audio_transcription/meta.py | 1 + .../core/tasks/audio_transcription/spec.py | 2 + .../builders/audio/transcription.py | 43 +++++++++++++------ 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py index 815bdec524..801684b683 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py +++ b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py @@ -141,6 +141,7 @@ def _placed_from_dict(d: dict) -> PlacedRegion: class TaskMetaLayout: + CLIPS_DIR = "clips" # assignment clips subdir on the oracle bucket GT_FILENAME = "gt.tsv" REGIONS_TSV_FILENAME = "regions.tsv" REGIONS_FILENAME = "regions.json" diff --git a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py index c9888e5670..f80bfc3344 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py +++ b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py @@ -76,6 +76,7 @@ class TranscriptionTaskSpecification(BaseModel): labels: list[str] shared_attributes: list = Field(default_factory=list) + user_guide: str = "" validation: TranscriptionTaskValidation @@ -126,6 +127,7 @@ def parse_audio_manifest(manifest: JobManifest) -> TranscriptionTaskSpecificatio ), labels=[label.name for label in annotation.labels], shared_attributes=list(annotation.shared_attributes), + user_guide=annotation.user_guide_url, validation=TranscriptionTaskValidation( target_metric=validation.target_metric.value, target_score=validation.target_score, diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py index 230e213042..83c1ce47bd 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py @@ -56,32 +56,39 @@ def _bundle_regions( - regions: list[InputRegion], *, min_duration: float + regions: list[InputRegion], *, min_duration: float, max_duration: float ) -> list[tuple[float, float, list[InputRegion]]]: """ Bundle consecutive input regions (sorted by start) into groups >= min_duration; a region already >= min_duration stays alone. Returns (start_s, stop_s, members) per bundle. + + A bundle's span (first ROI start .. last ROI stop) includes the gaps between its members, so + two limits keep bundles from ballooning over sparse input: + - max_duration caps the presented span (a longer bundle would fail input validation anyway); + - the join gap is capped at min_duration, so we never bridge a long silence between ROIs. """ def span(group: list[InputRegion]) -> float: return group[-1].stop.total_seconds() - group[0].start.total_seconds() + def joinable(group: list[InputRegion], region: InputRegion) -> bool: + gap = region.start.total_seconds() - group[-1].stop.total_seconds() + reach = region.stop.total_seconds() - group[0].start.total_seconds() + return span(group) < min_duration and gap <= min_duration and reach <= max_duration + out: list[list[InputRegion]] = [] bundle: list[InputRegion] | None = None for region in regions: if bundle is None: bundle = [region] - elif span(bundle) < min_duration: + elif joinable(bundle, region): bundle.append(region) else: out.append(bundle) bundle = [region] - if bundle is not None: - if out and span(bundle) < min_duration: - out[-1].extend(bundle) - else: - out.append(bundle) + if bundle is not None: # trailing bundle may stay shorter than min_duration + out.append(bundle) return [(g[0].start.total_seconds(), g[-1].stop.total_seconds(), g) for g in out] @@ -92,6 +99,7 @@ def select_honeypots( source_filename: str, val_fraction: float, min_duration: float, + max_duration: float, random_seed: int = 0, ) -> list[PresentedRegion]: """ @@ -102,7 +110,7 @@ def select_honeypots( """ regions = sorted(regions, key=lambda r: r.start) - bundles = _bundle_regions(regions, min_duration=min_duration) + bundles = _bundle_regions(regions, min_duration=min_duration, max_duration=max_duration) rng = random.Random(random_seed) # noqa: S311 # not cryptographic idxs = list(range(len(bundles))) @@ -304,6 +312,7 @@ def _build_gt_regions(self) -> None: source_filename=filename, val_fraction=1.0, # use all GT as honeypots (rotation is a future feature) min_duration=self.spec.details.roi_min_duration.total_seconds(), + max_duration=self.spec.details.roi_max_duration.total_seconds(), random_seed=self.spec.details.random_seed, ) ) @@ -362,6 +371,7 @@ def _build_ds_regions(self) -> None: bundled = _bundle_regions( rows, min_duration=self.spec.details.roi_min_duration.total_seconds(), + max_duration=self.spec.details.roi_max_duration.total_seconds(), ) for start, stop, members in bundled: ds_regions.append( @@ -447,7 +457,9 @@ def _upload_clips_and_meta(self) -> None: clip_path = self._media_dir / assignment.clip_filename storage_client.create_file( compose_data_bucket_filename( - self.escrow_address, self.chain_id, assignment.clip_filename + self.escrow_address, + self.chain_id, + f"{self._meta_layout.CLIPS_DIR}/{assignment.clip_filename}", ), clip_path.read_bytes(), ) @@ -481,10 +493,9 @@ def _upload_clips_and_meta(self) -> None: ) if Config.debug: - self._upload_debug_cuts(storage_client) + self._upload_roi_cuts(storage_client) - def _upload_debug_cuts(self, storage_client) -> None: - """Upload each DS/GT region as its own clip into debug dirs, for inspection.""" + def _upload_roi_cuts(self, storage_client) -> None: assert self._media_dir is not unset assert self._media_paths is not unset assert self._ds_regions is not unset @@ -532,7 +543,9 @@ def _create_on_cvat(self) -> None: shared_attributes=self.spec.shared_attributes, ) - cvat_project = cvat_api.create_project(escrow_address, labels=label_configuration) + cvat_project = cvat_api.create_project( + escrow_address, labels=label_configuration, user_guide=self.spec.user_guide + ) cvat_webhook = cvat_api.create_cvat_webhook(cvat_project.id) cloud_storage = cvat_api.create_cloudstorage( **make_cvat_cloud_storage_params(self._oracle_data_bucket) @@ -565,7 +578,9 @@ def _create_on_cvat(self) -> None: for assignment in self._assignments: clip_key = compose_data_bucket_filename( - escrow_address, chain_id, assignment.clip_filename + escrow_address, + chain_id, + f"{self._meta_layout.CLIPS_DIR}/{assignment.clip_filename}", ) cvat_task = cvat_api.create_task(cvat_project.id, escrow_address) task_id = db_service.create_task( From 928522ca4c50d1f56cfafbbfb3cd4926a17260d9 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Mon, 6 Jul 2026 14:16:03 +0300 Subject: [PATCH 14/53] Fix manifest parsing --- .../exchange-oracle/src/core/manifest/v2.py | 39 ++++++--------- .../src/handlers/job_creation/factory.py | 48 ++++++++++++------- 2 files changed, 45 insertions(+), 42 deletions(-) diff --git a/packages/examples/cvat/exchange-oracle/src/core/manifest/v2.py b/packages/examples/cvat/exchange-oracle/src/core/manifest/v2.py index 10f49fcc75..76ac94fade 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/manifest/v2.py +++ b/packages/examples/cvat/exchange-oracle/src/core/manifest/v2.py @@ -116,52 +116,43 @@ class AnnotationInfo(BaseModel): details: ImageJobDetails | AudioJobDetails | None = None "Task-specific details" - @model_validator(mode="before") - @classmethod - def validate_label_type(cls, values: dict[str, Any]) -> dict[str, Any]: - if not isinstance(values, dict): - raise NotImplementedError - - default_label_type = LabelTypes.plain - if values["type"] == TaskTypes.image_skeletons_from_boxes: - default_label_type = LabelTypes.skeleton - - # Add default value for labels, if none provided. - # pydantic can't do this for tagged unions - try: - labels = values["labels"] - for label_info in labels: - label_info["type"] = label_info.get("type", default_label_type) - except KeyError: - pass - - return values - class JobManifest(BaseModel): version: Literal[2] - task_type: TaskTypes + job_type: TaskTypes data: DataInfo annotation: AnnotationInfo @model_validator(mode="before") @classmethod - def validate_task_details(cls, values: dict[str, Any]) -> dict[str, Any]: + def parse_task_specific_params(cls, values: dict[str, Any]) -> dict[str, Any]: if not isinstance(values, dict): raise NotImplementedError + job_type = values.get("job_type") + annotation = values.get("annotation") if not isinstance(annotation, dict): raise NotImplementedError + # Default label types (pydantic can't do this for tagged unions). + default_label_type = ( + LabelTypes.skeleton + if job_type == TaskTypes.image_skeletons_from_boxes + else LabelTypes.plain + ) + for label_info in annotation.get("labels", []): + if isinstance(label_info, dict): + label_info.setdefault("type", default_label_type) + if details := annotation.get("details"): if not isinstance(details, dict): raise NotImplementedError details_cls = ( AudioJobDetails - if values.get("job_type") == TaskTypes.audio_transcription + if job_type == TaskTypes.audio_transcription else ImageJobDetails ) annotation["details"] = details_cls.model_validate(details) diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/factory.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/factory.py index 6cdc6a47b1..3b35d4161e 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/factory.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/factory.py @@ -1,7 +1,9 @@ from __future__ import annotations +from typing import cast + from src.chain.escrow import get_escrow_manifest -from src.core.manifest import parse_manifest +from src.core import manifest as manifest_utils from src.core.tasks import TaskTypes from src.handlers.job_creation.builders.audio.transcription import AudioTranscriptionTaskBuilder from src.handlers.job_creation.builders.vision.basic import ( @@ -23,24 +25,34 @@ def create_task(escrow_address: str, chain_id: int) -> None: logger = get_function_logger(module_logger) - manifest = parse_manifest(get_escrow_manifest(chain_id, escrow_address)) - - if manifest.annotation.type is TaskTypes.image_boxes: - builder_type = BoxesTaskBuilder - elif manifest.annotation.type is TaskTypes.image_label_binary: - builder_type = LabelBinaryTaskBuilder - elif manifest.annotation.type is TaskTypes.image_polygons: - builder_type = PolygonTaskBuilder - elif manifest.annotation.type is TaskTypes.image_points: - builder_type = PointsTaskBuilder - elif manifest.annotation.type is TaskTypes.image_boxes_from_points: - builder_type = BoxesFromPointsTaskBuilder - elif manifest.annotation.type is TaskTypes.image_skeletons_from_boxes: - builder_type = SkeletonsFromBoxesTaskBuilder - elif manifest.annotation.type is TaskTypes.audio_transcription: - builder_type = AudioTranscriptionTaskBuilder + manifest = manifest_utils.parse_manifest(get_escrow_manifest(chain_id, escrow_address)) + + if manifest.version == 1: + manifest = cast("manifest_utils.v1.JobManifest", manifest) + task_type = manifest.annotation.type + elif manifest.version == 2: + manifest = cast("manifest_utils.v2.JobManifest", manifest) + task_type = manifest.job_type else: - raise Exception(f"Unsupported task type {manifest.annotation.type}") + raise NotImplementedError(f"Unknown manifest version '{manifest.version}'") + + match task_type: + case TaskTypes.image_boxes: + builder_type = BoxesTaskBuilder + case TaskTypes.image_label_binary: + builder_type = LabelBinaryTaskBuilder + case TaskTypes.image_polygons: + builder_type = PolygonTaskBuilder + case TaskTypes.image_points: + builder_type = PointsTaskBuilder + case TaskTypes.image_boxes_from_points: + builder_type = BoxesFromPointsTaskBuilder + case TaskTypes.image_skeletons_from_boxes: + builder_type = SkeletonsFromBoxesTaskBuilder + case TaskTypes.audio_transcription: + builder_type = AudioTranscriptionTaskBuilder + case _: + raise Exception(f"Unsupported task type {task_type}") with builder_type(manifest, escrow_address, chain_id) as task_builder: task_builder.set_logger(logger) From d084d7a503802a8523ecb33027577ee16572114f Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Mon, 6 Jul 2026 14:25:18 +0300 Subject: [PATCH 15/53] Create test assets directory --- .../tests/api/test_exchange_api.py | 28 +++++++++--------- .../datasets/gt_annotations_coco.zip | Bin .../annotations/instances_default.json | 0 .../tests/{utils => assets}/datasets/img1.jpg | Bin .../{utils => assets}/datasets/img10.jpg | Bin .../{utils => assets}/datasets/img11.jpg | Bin .../{utils => assets}/datasets/img12.jpg | Bin .../{utils => assets}/datasets/img13.jpg | Bin .../tests/{utils => assets}/datasets/img2.jpg | Bin .../tests/{utils => assets}/datasets/img3.jpg | Bin .../tests/{utils => assets}/datasets/img4.jpg | Bin .../tests/{utils => assets}/datasets/img5.jpg | Bin .../tests/{utils => assets}/datasets/img6.jpg | Bin .../tests/{utils => assets}/datasets/img7.jpg | Bin .../tests/{utils => assets}/datasets/img8.jpg | Bin .../tests/{utils => assets}/datasets/img9.jpg | Bin .../{utils => assets/manifests}/manifest.json | 0 .../test_track_completed_escrows.py | 8 ++--- .../test_process_job_launcher_webhooks.py | 4 +-- .../integration/services/test_exchange.py | 24 +++++++-------- 20 files changed, 32 insertions(+), 32 deletions(-) rename packages/examples/cvat/exchange-oracle/tests/{utils => assets}/datasets/gt_annotations_coco.zip (100%) rename packages/examples/cvat/exchange-oracle/tests/{utils => assets}/datasets/gt_annotations_coco/annotations/instances_default.json (100%) rename packages/examples/cvat/exchange-oracle/tests/{utils => assets}/datasets/img1.jpg (100%) rename packages/examples/cvat/exchange-oracle/tests/{utils => assets}/datasets/img10.jpg (100%) rename packages/examples/cvat/exchange-oracle/tests/{utils => assets}/datasets/img11.jpg (100%) rename packages/examples/cvat/exchange-oracle/tests/{utils => assets}/datasets/img12.jpg (100%) rename packages/examples/cvat/exchange-oracle/tests/{utils => assets}/datasets/img13.jpg (100%) rename packages/examples/cvat/exchange-oracle/tests/{utils => assets}/datasets/img2.jpg (100%) rename packages/examples/cvat/exchange-oracle/tests/{utils => assets}/datasets/img3.jpg (100%) rename packages/examples/cvat/exchange-oracle/tests/{utils => assets}/datasets/img4.jpg (100%) rename packages/examples/cvat/exchange-oracle/tests/{utils => assets}/datasets/img5.jpg (100%) rename packages/examples/cvat/exchange-oracle/tests/{utils => assets}/datasets/img6.jpg (100%) rename packages/examples/cvat/exchange-oracle/tests/{utils => assets}/datasets/img7.jpg (100%) rename packages/examples/cvat/exchange-oracle/tests/{utils => assets}/datasets/img8.jpg (100%) rename packages/examples/cvat/exchange-oracle/tests/{utils => assets}/datasets/img9.jpg (100%) rename packages/examples/cvat/exchange-oracle/tests/{utils => assets/manifests}/manifest.json (100%) diff --git a/packages/examples/cvat/exchange-oracle/tests/api/test_exchange_api.py b/packages/examples/cvat/exchange-oracle/tests/api/test_exchange_api.py index 53a16ded0e..b3c552b888 100644 --- a/packages/examples/cvat/exchange-oracle/tests/api/test_exchange_api.py +++ b/packages/examples/cvat/exchange-oracle/tests/api/test_exchange_api.py @@ -175,7 +175,7 @@ def validate_result( session.commit() with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, patch( "src.endpoints.serializers.get_escrow_fund_token_symbol" @@ -240,7 +240,7 @@ def test_can_list_jobs_200_without_escrows_in_hidden_states( session.commit() with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, patch( "src.endpoints.serializers.get_escrow_fund_token_symbol" @@ -298,7 +298,7 @@ def test_can_list_jobs_200_with_only_one_entry_per_escrow_address_if_several_pro session.commit() with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, patch( "src.endpoints.serializers.get_escrow_fund_token_symbol" @@ -338,7 +338,7 @@ def test_can_list_jobs_200_with_fields(client: TestClient, session: Session) -> session.commit() with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, patch( "src.endpoints.serializers.get_escrow_fund_token_symbol" @@ -421,7 +421,7 @@ def test_can_list_jobs_200_with_sorting(client: TestClient, session: Session) -> } == {(obj.__class__.__name__, obj.id): True for obj in cvat_jobs + cvat_tasks + cvat_projects} with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, patch( "src.endpoints.serializers.get_escrow_fund_token_symbol" @@ -541,7 +541,7 @@ def test_can_list_jobs_200_with_filters(client: TestClient, session: Session): post_init_time = utcnow() + timedelta(seconds=1) with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, patch( "src.endpoints.serializers.get_escrow_fund_token_symbol" @@ -632,7 +632,7 @@ def test_can_list_jobs_200_can_show_only_active_jobs_with_free_assignments( session.commit() with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, patch( "src.endpoints.serializers.get_escrow_fund_token_symbol" @@ -681,7 +681,7 @@ def test_can_list_jobs_200_check_values(client: TestClient, session: Session) -> session.commit() with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, patch( "src.endpoints.serializers.get_escrow_fund_token_symbol" @@ -740,7 +740,7 @@ def test_can_list_jobs_200_without_address(client: TestClient, session: Session) session.commit() with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, patch( "src.endpoints.serializers.get_escrow_fund_token_symbol" @@ -875,7 +875,7 @@ def test_can_create_assignment_200(client: TestClient, session: Session) -> None session.commit() with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.endpoints.serializers.get_escrow_manifest") as mock_serializer_get_manifest, patch("src.services.exchange.get_escrow_manifest") as mock_exchange_get_manifest, patch( @@ -980,7 +980,7 @@ def test_cannot_create_assignment_400_when_has_unfinished_assignments( session.commit() with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.services.exchange.get_escrow_manifest") as mock_get_manifest, ): manifest = json.load(data) @@ -1075,7 +1075,7 @@ def test_can_list_assignments_200(client: TestClient, session: Session) -> None: post_init_time = utcnow() + timedelta(seconds=1) with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, patch( "src.endpoints.serializers.get_escrow_fund_token_symbol" @@ -1156,7 +1156,7 @@ def test_can_list_assignments_200_with_sorting(client: TestClient, session: Sess session.commit() with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, patch( "src.endpoints.serializers.get_escrow_fund_token_symbol" @@ -1513,7 +1513,7 @@ def test_can_list_jobs_200_check_updated_at(client: TestClient, session: Session session.commit() with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.endpoints.serializers.get_escrow_manifest") as mock_serializer_get_manifest, patch("src.services.exchange.get_escrow_manifest") as mock_exchange_get_manifest, patch( diff --git a/packages/examples/cvat/exchange-oracle/tests/utils/datasets/gt_annotations_coco.zip b/packages/examples/cvat/exchange-oracle/tests/assets/datasets/gt_annotations_coco.zip similarity index 100% rename from packages/examples/cvat/exchange-oracle/tests/utils/datasets/gt_annotations_coco.zip rename to packages/examples/cvat/exchange-oracle/tests/assets/datasets/gt_annotations_coco.zip diff --git a/packages/examples/cvat/exchange-oracle/tests/utils/datasets/gt_annotations_coco/annotations/instances_default.json b/packages/examples/cvat/exchange-oracle/tests/assets/datasets/gt_annotations_coco/annotations/instances_default.json similarity index 100% rename from packages/examples/cvat/exchange-oracle/tests/utils/datasets/gt_annotations_coco/annotations/instances_default.json rename to packages/examples/cvat/exchange-oracle/tests/assets/datasets/gt_annotations_coco/annotations/instances_default.json diff --git a/packages/examples/cvat/exchange-oracle/tests/utils/datasets/img1.jpg b/packages/examples/cvat/exchange-oracle/tests/assets/datasets/img1.jpg similarity index 100% rename from packages/examples/cvat/exchange-oracle/tests/utils/datasets/img1.jpg rename to packages/examples/cvat/exchange-oracle/tests/assets/datasets/img1.jpg diff --git a/packages/examples/cvat/exchange-oracle/tests/utils/datasets/img10.jpg b/packages/examples/cvat/exchange-oracle/tests/assets/datasets/img10.jpg similarity index 100% rename from packages/examples/cvat/exchange-oracle/tests/utils/datasets/img10.jpg rename to packages/examples/cvat/exchange-oracle/tests/assets/datasets/img10.jpg diff --git a/packages/examples/cvat/exchange-oracle/tests/utils/datasets/img11.jpg b/packages/examples/cvat/exchange-oracle/tests/assets/datasets/img11.jpg similarity index 100% rename from packages/examples/cvat/exchange-oracle/tests/utils/datasets/img11.jpg rename to packages/examples/cvat/exchange-oracle/tests/assets/datasets/img11.jpg diff --git a/packages/examples/cvat/exchange-oracle/tests/utils/datasets/img12.jpg b/packages/examples/cvat/exchange-oracle/tests/assets/datasets/img12.jpg similarity index 100% rename from packages/examples/cvat/exchange-oracle/tests/utils/datasets/img12.jpg rename to packages/examples/cvat/exchange-oracle/tests/assets/datasets/img12.jpg diff --git a/packages/examples/cvat/exchange-oracle/tests/utils/datasets/img13.jpg b/packages/examples/cvat/exchange-oracle/tests/assets/datasets/img13.jpg similarity index 100% rename from packages/examples/cvat/exchange-oracle/tests/utils/datasets/img13.jpg rename to packages/examples/cvat/exchange-oracle/tests/assets/datasets/img13.jpg diff --git a/packages/examples/cvat/exchange-oracle/tests/utils/datasets/img2.jpg b/packages/examples/cvat/exchange-oracle/tests/assets/datasets/img2.jpg similarity index 100% rename from packages/examples/cvat/exchange-oracle/tests/utils/datasets/img2.jpg rename to packages/examples/cvat/exchange-oracle/tests/assets/datasets/img2.jpg diff --git a/packages/examples/cvat/exchange-oracle/tests/utils/datasets/img3.jpg b/packages/examples/cvat/exchange-oracle/tests/assets/datasets/img3.jpg similarity index 100% rename from packages/examples/cvat/exchange-oracle/tests/utils/datasets/img3.jpg rename to packages/examples/cvat/exchange-oracle/tests/assets/datasets/img3.jpg diff --git a/packages/examples/cvat/exchange-oracle/tests/utils/datasets/img4.jpg b/packages/examples/cvat/exchange-oracle/tests/assets/datasets/img4.jpg similarity index 100% rename from packages/examples/cvat/exchange-oracle/tests/utils/datasets/img4.jpg rename to packages/examples/cvat/exchange-oracle/tests/assets/datasets/img4.jpg diff --git a/packages/examples/cvat/exchange-oracle/tests/utils/datasets/img5.jpg b/packages/examples/cvat/exchange-oracle/tests/assets/datasets/img5.jpg similarity index 100% rename from packages/examples/cvat/exchange-oracle/tests/utils/datasets/img5.jpg rename to packages/examples/cvat/exchange-oracle/tests/assets/datasets/img5.jpg diff --git a/packages/examples/cvat/exchange-oracle/tests/utils/datasets/img6.jpg b/packages/examples/cvat/exchange-oracle/tests/assets/datasets/img6.jpg similarity index 100% rename from packages/examples/cvat/exchange-oracle/tests/utils/datasets/img6.jpg rename to packages/examples/cvat/exchange-oracle/tests/assets/datasets/img6.jpg diff --git a/packages/examples/cvat/exchange-oracle/tests/utils/datasets/img7.jpg b/packages/examples/cvat/exchange-oracle/tests/assets/datasets/img7.jpg similarity index 100% rename from packages/examples/cvat/exchange-oracle/tests/utils/datasets/img7.jpg rename to packages/examples/cvat/exchange-oracle/tests/assets/datasets/img7.jpg diff --git a/packages/examples/cvat/exchange-oracle/tests/utils/datasets/img8.jpg b/packages/examples/cvat/exchange-oracle/tests/assets/datasets/img8.jpg similarity index 100% rename from packages/examples/cvat/exchange-oracle/tests/utils/datasets/img8.jpg rename to packages/examples/cvat/exchange-oracle/tests/assets/datasets/img8.jpg diff --git a/packages/examples/cvat/exchange-oracle/tests/utils/datasets/img9.jpg b/packages/examples/cvat/exchange-oracle/tests/assets/datasets/img9.jpg similarity index 100% rename from packages/examples/cvat/exchange-oracle/tests/utils/datasets/img9.jpg rename to packages/examples/cvat/exchange-oracle/tests/assets/datasets/img9.jpg diff --git a/packages/examples/cvat/exchange-oracle/tests/utils/manifest.json b/packages/examples/cvat/exchange-oracle/tests/assets/manifests/manifest.json similarity index 100% rename from packages/examples/cvat/exchange-oracle/tests/utils/manifest.json rename to packages/examples/cvat/exchange-oracle/tests/assets/manifests/manifest.json diff --git a/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_escrows.py b/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_escrows.py index 13410d300c..b086425289 100644 --- a/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_escrows.py +++ b/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_escrows.py @@ -572,7 +572,7 @@ def test_can_export_escrow(self): self.session.commit() with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.handlers.completed_escrows.get_escrow_manifest") as mock_get_manifest, patch("src.handlers.completed_escrows.validate_escrow"), patch("src.handlers.completed_escrows.cvat_api") as mock_cvat_api, @@ -700,7 +700,7 @@ def test_can_export_escrow_error_getting_annotations(self): self.session.commit() with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.handlers.completed_escrows.get_escrow_manifest") as mock_get_manifest, patch("src.handlers.completed_escrows.validate_escrow"), patch( @@ -823,7 +823,7 @@ def test_can_export_escrow_error_uploading_files(self): self.session.commit() with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.handlers.completed_escrows.get_escrow_manifest") as mock_get_manifest, patch("src.handlers.completed_escrows.validate_escrow"), patch("src.handlers.completed_escrows.cvat_api") as mock_cvat_api, @@ -948,7 +948,7 @@ def test_can_export_multiple_projects_per_escrow(self): self.session.commit() with ( - open("tests/utils/manifest.json") as manifest_data, + open("tests/assets/manifests/manifest.json") as manifest_data, patch("src.handlers.completed_escrows.get_escrow_manifest") as mock_get_manifest, patch("src.handlers.completed_escrows.validate_escrow"), patch("src.handlers.completed_escrows.cvat_api") as mock_cvat_api, diff --git a/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_job_launcher_webhooks.py b/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_job_launcher_webhooks.py index ba178240ba..17857e0081 100644 --- a/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_job_launcher_webhooks.py +++ b/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_job_launcher_webhooks.py @@ -62,7 +62,7 @@ def test_process_incoming_job_launcher_webhooks_escrow_created_type(self): self.session.commit() with ( patch("src.chain.escrow.get_escrow") as mock_escrow, - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.handlers.job_creation.factory.get_escrow_manifest") as mock_get_manifest, patch("src.handlers.job_creation.builders.vision.basic.cvat_api") as mock_cvat_api, patch( @@ -248,7 +248,7 @@ def test_process_incoming_job_launcher_webhooks_escrow_created_type_remove_when_ self.session.commit() with ( patch("src.chain.escrow.get_escrow") as mock_escrow, - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.handlers.job_creation.factory.get_escrow_manifest") as mock_get_manifest, patch("src.handlers.job_creation.builders.vision.basic.cvat_api") as mock_cvat_api, patch( diff --git a/packages/examples/cvat/exchange-oracle/tests/integration/services/test_exchange.py b/packages/examples/cvat/exchange-oracle/tests/integration/services/test_exchange.py index 58d89d3ca3..6d2b191df7 100644 --- a/packages/examples/cvat/exchange-oracle/tests/integration/services/test_exchange.py +++ b/packages/examples/cvat/exchange-oracle/tests/integration/services/test_exchange.py @@ -40,7 +40,7 @@ def test_serialize_job(self): self.session.commit() with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, patch( "src.endpoints.serializers.get_escrow_fund_token_symbol" @@ -99,7 +99,7 @@ def test_create_assignment(self): self.session.commit() with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.services.exchange.get_escrow_manifest") as mock_get_manifest, patch("src.services.exchange.cvat_api"), ): @@ -154,7 +154,7 @@ def test_create_assignment_many_jobs_1_completed(self): self.session.commit() with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.services.exchange.get_escrow_manifest") as mock_get_manifest, patch("src.services.exchange.cvat_api"), ): @@ -193,7 +193,7 @@ def test_create_assignment_invalid_project(self): self.session.commit() with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.services.exchange.get_escrow_manifest") as mock_get_manifest, ): manifest = json.load(data) @@ -212,7 +212,7 @@ def test_create_assignment_no_required_qualifications(self): self.session.commit() with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.services.exchange.get_escrow_manifest") as mock_get_manifest, ): manifest = json.load(data) @@ -240,7 +240,7 @@ def test_create_assignment_with_required_qualifications(self): self.session.commit() with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.services.exchange.get_escrow_manifest") as mock_get_manifest, patch("src.services.exchange.cvat_api"), ): @@ -293,7 +293,7 @@ def test_create_assignment_unfinished_assignment(self): self.session.commit() with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.services.exchange.get_escrow_manifest") as mock_get_manifest, patch("src.services.exchange.cvat_api"), ): @@ -331,7 +331,7 @@ def test_create_assignment_has_expired_assignment_and_available_jobs(self): self.session.commit() with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.services.exchange.get_escrow_manifest") as mock_get_manifest, patch("src.services.exchange.cvat_api"), ): @@ -381,7 +381,7 @@ def test_create_assignment_no_available_jobs_completed_assignment(self): self.session.commit() with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.services.exchange.get_escrow_manifest") as mock_get_manifest, patch("src.services.exchange.cvat_api"), ): @@ -426,7 +426,7 @@ def test_create_assignment_no_available_jobs_active_foreign_assignment(self): self.session.commit() with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.services.exchange.get_escrow_manifest") as mock_get_manifest, patch("src.services.exchange.cvat_api"), ): @@ -464,7 +464,7 @@ def test_create_assignment_wont_reassign_job_to_previous_user(self): self.session.commit() with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.services.exchange.get_escrow_manifest") as mock_get_manifest, patch("src.services.exchange.cvat_api"), ): @@ -511,7 +511,7 @@ def test_create_assignment_can_assign_job_to_new_user(self): self.session.commit() with ( - open("tests/utils/manifest.json") as data, + open("tests/assets/manifests/manifest.json") as data, patch("src.services.exchange.get_escrow_manifest") as mock_get_manifest, patch("src.services.exchange.cvat_api"), ): From ab44088876a367748c27d3f054332af73835321b Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Mon, 6 Jul 2026 15:23:30 +0300 Subject: [PATCH 16/53] Update cuts location --- .../src/core/tasks/audio_transcription/meta.py | 4 ++-- .../src/handlers/job_creation/builders/audio/transcription.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py index 801684b683..c4c5b18b23 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py +++ b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py @@ -146,8 +146,8 @@ class TaskMetaLayout: REGIONS_TSV_FILENAME = "regions.tsv" REGIONS_FILENAME = "regions.json" ASSIGNMENTS_FILENAME = "assignments.json" - DEBUG_DS_CUTS_DIR = "debug/ds_cuts" # per-region DS clips (debug only) - DEBUG_GT_CUTS_DIR = "debug/gt_cuts" # per-region GT clips (debug only) + DS_CUTS_DIR = "ds_cuts" + GT_CUTS_DIR = "gt_cuts" class TaskMetaSerializer: diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py index 83c1ce47bd..753c859d1a 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py @@ -502,8 +502,8 @@ def _upload_roi_cuts(self, storage_client) -> None: assert self._honeypots is not unset pairs = ( - (self._meta_layout.DEBUG_DS_CUTS_DIR, self._ds_regions), - (self._meta_layout.DEBUG_GT_CUTS_DIR, self._honeypots), + (self._meta_layout.DS_CUTS_DIR, self._ds_regions), + (self._meta_layout.GT_CUTS_DIR, self._honeypots), ) for subdir, regions in pairs: for i, region in enumerate(regions): From 4fec9c9eb424b862c22be81f8b074fe2fd5ddc45 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Mon, 6 Jul 2026 15:42:56 +0300 Subject: [PATCH 17/53] Rename completed_escrows to job_completion --- .../src/crons/cvat/state_trackers.py | 2 +- .../src/crons/webhooks/recording_oracle.py | 2 +- ...completed_escrows.py => job_completion.py} | 0 .../test_track_completed_escrows.py | 48 +++++++++---------- 4 files changed, 26 insertions(+), 26 deletions(-) rename packages/examples/cvat/exchange-oracle/src/handlers/{completed_escrows.py => job_completion.py} (100%) diff --git a/packages/examples/cvat/exchange-oracle/src/crons/cvat/state_trackers.py b/packages/examples/cvat/exchange-oracle/src/crons/cvat/state_trackers.py index f841397b45..00a000bb31 100644 --- a/packages/examples/cvat/exchange-oracle/src/crons/cvat/state_trackers.py +++ b/packages/examples/cvat/exchange-oracle/src/crons/cvat/state_trackers.py @@ -14,8 +14,8 @@ from src.db import SessionLocal from src.db import errors as db_errors from src.db.utils import ForUpdateParams -from src.handlers.completed_escrows import handle_escrows_validations from src.handlers.cvat_events import cvat_webhook_handler +from src.handlers.job_completion import handle_escrows_validations from src.utils.logging import format_sequence from src.utils.time import utcnow diff --git a/packages/examples/cvat/exchange-oracle/src/crons/webhooks/recording_oracle.py b/packages/examples/cvat/exchange-oracle/src/crons/webhooks/recording_oracle.py index bf527a9ef0..6fe64a0a33 100644 --- a/packages/examples/cvat/exchange-oracle/src/crons/webhooks/recording_oracle.py +++ b/packages/examples/cvat/exchange-oracle/src/crons/webhooks/recording_oracle.py @@ -19,7 +19,7 @@ from src.crons._cron_job import cron_job from src.crons.webhooks._common import handle_webhook, process_outgoing_webhooks from src.db.utils import ForUpdateParams -from src.handlers.completed_escrows import handle_escrow_export +from src.handlers.job_completion import handle_escrow_export from src.models.webhook import Webhook diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/completed_escrows.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_completion.py similarity index 100% rename from packages/examples/cvat/exchange-oracle/src/handlers/completed_escrows.py rename to packages/examples/cvat/exchange-oracle/src/handlers/job_completion.py diff --git a/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_escrows.py b/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_escrows.py index b086425289..9cc86c5d64 100644 --- a/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_escrows.py +++ b/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_escrows.py @@ -26,7 +26,7 @@ from src.crons import track_completed_escrows from src.crons.cvat.state_trackers import track_escrow_validations from src.db import SessionLocal -from src.handlers.completed_escrows import handle_escrow_export +from src.handlers.job_completion import handle_escrow_export from src.models.cvat import ( Assignment, EscrowCreation, @@ -263,8 +263,8 @@ def test_can_request_validation(self): self.session.commit() with ( - patch("src.handlers.completed_escrows.validate_escrow"), - patch("src.handlers.completed_escrows.cloud_service") as mock_cloud_service, + patch("src.handlers.job_completion.validate_escrow"), + patch("src.handlers.job_completion.cloud_service") as mock_cloud_service, ): mock_storage_client = Mock() mock_storage_client.create_file = Mock() @@ -373,8 +373,8 @@ def test_request_validation_error_in_file_uploading_increases_attempts(self): self.session.commit() with ( - patch("src.handlers.completed_escrows.validate_escrow"), - patch("src.handlers.completed_escrows.cloud_service") as mock_cloud_service, + patch("src.handlers.job_completion.validate_escrow"), + patch("src.handlers.job_completion.cloud_service") as mock_cloud_service, patch("src.services.cloud.make_client"), ): mock_cloud_service.make_client.return_value.create_file.side_effect = _TestException() @@ -466,8 +466,8 @@ def test_can_request_validation_multiple_projects_per_escrow_all_completed(self) self.session.commit() with ( - patch("src.handlers.completed_escrows.validate_escrow"), - patch("src.handlers.completed_escrows.cloud_service") as mock_cloud_service, + patch("src.handlers.job_completion.validate_escrow"), + patch("src.handlers.job_completion.cloud_service") as mock_cloud_service, ): mock_storage_client = Mock() mock_storage_client.create_file = Mock() @@ -573,10 +573,10 @@ def test_can_export_escrow(self): with ( open("tests/assets/manifests/manifest.json") as data, - patch("src.handlers.completed_escrows.get_escrow_manifest") as mock_get_manifest, - patch("src.handlers.completed_escrows.validate_escrow"), - patch("src.handlers.completed_escrows.cvat_api") as mock_cvat_api, - patch("src.handlers.completed_escrows.cloud_service") as mock_cloud_service, + patch("src.handlers.job_completion.get_escrow_manifest") as mock_get_manifest, + patch("src.handlers.job_completion.validate_escrow"), + patch("src.handlers.job_completion.cvat_api") as mock_cvat_api, + patch("src.handlers.job_completion.cloud_service") as mock_cloud_service, ): manifest = json.load(data) mock_get_manifest.return_value = manifest @@ -701,12 +701,12 @@ def test_can_export_escrow_error_getting_annotations(self): with ( open("tests/assets/manifests/manifest.json") as data, - patch("src.handlers.completed_escrows.get_escrow_manifest") as mock_get_manifest, - patch("src.handlers.completed_escrows.validate_escrow"), + patch("src.handlers.job_completion.get_escrow_manifest") as mock_get_manifest, + patch("src.handlers.job_completion.validate_escrow"), patch( - "src.handlers.completed_escrows.cvat_api.request_job_annotations" + "src.handlers.job_completion.cvat_api.request_job_annotations" ) as mock_request_job_annotations, - patch("src.handlers.completed_escrows.cloud_service") as mock_cloud_service, + patch("src.handlers.job_completion.cloud_service") as mock_cloud_service, ): manifest = json.load(data) mock_get_manifest.return_value = manifest @@ -824,10 +824,10 @@ def test_can_export_escrow_error_uploading_files(self): with ( open("tests/assets/manifests/manifest.json") as data, - patch("src.handlers.completed_escrows.get_escrow_manifest") as mock_get_manifest, - patch("src.handlers.completed_escrows.validate_escrow"), - patch("src.handlers.completed_escrows.cvat_api") as mock_cvat_api, - patch("src.handlers.completed_escrows.cloud_service") as mock_cloud_service, + patch("src.handlers.job_completion.get_escrow_manifest") as mock_get_manifest, + patch("src.handlers.job_completion.validate_escrow"), + patch("src.handlers.job_completion.cvat_api") as mock_cvat_api, + patch("src.handlers.job_completion.cloud_service") as mock_cloud_service, patch("src.services.cloud.make_client"), ): manifest = json.load(data) @@ -949,12 +949,12 @@ def test_can_export_multiple_projects_per_escrow(self): with ( open("tests/assets/manifests/manifest.json") as manifest_data, - patch("src.handlers.completed_escrows.get_escrow_manifest") as mock_get_manifest, - patch("src.handlers.completed_escrows.validate_escrow"), - patch("src.handlers.completed_escrows.cvat_api") as mock_cvat_api, - patch("src.handlers.completed_escrows.cloud_service") as mock_cloud_service, + patch("src.handlers.job_completion.get_escrow_manifest") as mock_get_manifest, + patch("src.handlers.job_completion.validate_escrow"), + patch("src.handlers.job_completion.cvat_api") as mock_cvat_api, + patch("src.handlers.job_completion.cloud_service") as mock_cloud_service, patch( - "src.handlers.completed_escrows.postprocess_annotations" + "src.handlers.job_completion.postprocess_annotations" ) as mock_postprocess_annotations, ): manifest = json.load(manifest_data) From 56b353046c7a0b35b2096efdfdbf6b0dd16f34a2 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Mon, 6 Jul 2026 18:16:51 +0300 Subject: [PATCH 18/53] Refactor job completion handlers, add audio task completion handler --- .../src/core/manifest/__init__.py | 17 +- .../src/crons/webhooks/recording_oracle.py | 2 +- .../exchange-oracle/src/cvat/api_calls.py | 26 +- .../exchange-oracle/src/entrypoints/debug.py | 2 + .../src/handlers/job_completion.py | 348 ------------------ .../src/handlers/job_completion/__init__.py | 3 + .../src/handlers/job_completion/factory.py | 43 +++ .../src/handlers/job_completion/handlers.py | 75 ++++ .../job_completion/validators/__init__.py | 0 .../job_completion/validators/audio.py | 58 +++ .../job_completion/validators/base.py | 81 ++++ .../src/handlers/job_creation/factory.py | 16 +- .../src/handlers/job_export/__init__.py | 3 + .../src/handlers/job_export/downloading.py | 123 +++++++ .../handlers/job_export/exporters/__init__.py | 0 .../handlers/job_export/exporters/audio.py | 23 ++ .../src/handlers/job_export/exporters/base.py | 67 ++++ .../exporters/vision.py} | 269 ++++++++------ .../src/handlers/job_export/factory.py | 43 +++ .../src/handlers/job_export/handlers.py | 33 ++ .../src/handlers/job_export/results.py | 82 +++++ .../test_track_completed_escrows.py | 62 ++-- 22 files changed, 858 insertions(+), 518 deletions(-) delete mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_completion.py create mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_completion/__init__.py create mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_completion/factory.py create mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_completion/handlers.py create mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_completion/validators/__init__.py create mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_completion/validators/audio.py create mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_completion/validators/base.py create mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_export/__init__.py create mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_export/downloading.py create mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/__init__.py create mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/audio.py create mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/base.py rename packages/examples/cvat/exchange-oracle/src/handlers/{job_export.py => job_export/exporters/vision.py} (73%) create mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_export/factory.py create mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_export/handlers.py create mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_export/results.py diff --git a/packages/examples/cvat/exchange-oracle/src/core/manifest/__init__.py b/packages/examples/cvat/exchange-oracle/src/core/manifest/__init__.py index d19a7550a7..5d91f29f36 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/manifest/__init__.py +++ b/packages/examples/cvat/exchange-oracle/src/core/manifest/__init__.py @@ -1,10 +1,16 @@ -from typing import Any +from __future__ import annotations + +from typing import TYPE_CHECKING, Any from src.core.manifest import v1, v2 from src.core.manifest.base import ManifestBase +if TYPE_CHECKING: + from src.core.tasks import TaskTypes + __all__ = [ "ManifestBase", + "get_manifest_task_type", "parse_manifest", "v1", "v2", @@ -21,3 +27,12 @@ def parse_manifest(manifest: Any) -> ManifestBase: return v2.JobManifest.model_validate(manifest) else: return v1.JobManifest.model_validate(manifest) + + +def get_manifest_task_type(manifest: ManifestBase) -> TaskTypes: + if isinstance(manifest, v1.JobManifest): + return manifest.annotation.type + if isinstance(manifest, v2.JobManifest): + return manifest.job_type + + raise NotImplementedError(f"Unknown manifest version '{manifest.version}'") diff --git a/packages/examples/cvat/exchange-oracle/src/crons/webhooks/recording_oracle.py b/packages/examples/cvat/exchange-oracle/src/crons/webhooks/recording_oracle.py index 6fe64a0a33..1985119256 100644 --- a/packages/examples/cvat/exchange-oracle/src/crons/webhooks/recording_oracle.py +++ b/packages/examples/cvat/exchange-oracle/src/crons/webhooks/recording_oracle.py @@ -19,7 +19,7 @@ from src.crons._cron_job import cron_job from src.crons.webhooks._common import handle_webhook, process_outgoing_webhooks from src.db.utils import ForUpdateParams -from src.handlers.job_completion import handle_escrow_export +from src.handlers.job_export import handle_escrow_export from src.models.webhook import Webhook diff --git a/packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py b/packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py index 42903bbb78..bfacd03134 100644 --- a/packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py +++ b/packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py @@ -92,6 +92,7 @@ def _get_annotations( *, attempt_interval: int = 5, timeout: int | None = _NOTSET, + expect_zip: bool = True, ) -> io.RawIOBase: """ Downloads annotations. @@ -133,9 +134,12 @@ def _get_annotations( request_auths=list(api_client.configuration.auth_settings().values()), body="", ) + response = api_client.rest_client.GET(request_info.result_url, headers=headers) file_buffer = io.BytesIO(response.data) - assert zipfile.is_zipfile(file_buffer) + if expect_zip: + assert zipfile.is_zipfile(file_buffer) + file_buffer.seek(0) return file_buffer @@ -524,19 +528,26 @@ def request_job_annotations(cvat_id: int, format_name: str) -> str: raise -def get_job_annotations(request_id: str, *, timeout: int | None = _NOTSET) -> io.RawIOBase: +def get_job_annotations( + request_id: str, *, timeout: int | None = _NOTSET, expect_zip: bool = True +) -> io.RawIOBase: """ Downloads annotations. The dataset preparation can take some time (e.g. 10 min), so it must be used like this: request_id = request_job_annotations(...): get_job_annotations(request_id, ...) + + Some export formats (e.g. "Generic TSV 1.0") return a plain file instead of a zip archive; + pass ``expect_zip=False`` for those. """ logger = logging.getLogger("app") with get_api_client() as api_client: try: - return _get_annotations(api_client, request_id=request_id, timeout=timeout) + return _get_annotations( + api_client, request_id=request_id, timeout=timeout, expect_zip=expect_zip + ) except exceptions.ApiException as e: logger.exception(f"Exception when calling JobsApi.retrieve_annotations: {e}\n") raise @@ -613,17 +624,12 @@ def clear_job_annotations(job_id: int) -> None: with get_api_client() as api_client: try: - api_client.jobs_api.update_annotations( - id=job_id, - job_annotations_update_request=models.JobAnnotationsUpdateRequest( - tags=[], shapes=[], tracks=[] - ), - ) + api_client.jobs_api.destroy_annotations(id=job_id) except exceptions.ApiException as e: if e.status == 404: return - logger.exception(f"Exception when calling JobsApi.partial_update_annotations(): {e}\n") + logger.exception(f"Exception when calling JobsApi.destroy_annotations(): {e}\n") raise diff --git a/packages/examples/cvat/exchange-oracle/src/entrypoints/debug.py b/packages/examples/cvat/exchange-oracle/src/entrypoints/debug.py index 8e62f9ef9d..0c996dfbc2 100644 --- a/packages/examples/cvat/exchange-oracle/src/entrypoints/debug.py +++ b/packages/examples/cvat/exchange-oracle/src/entrypoints/debug.py @@ -228,6 +228,8 @@ def decode_plain_json_token(self, token) -> dict[str, Any]: token_data["wallet_address"] = None token_data["email"] = "" + token_data.setdefault("qualifications", []) + logger.info(f"DEV: Decoded plain JSON auth token: {token_data}") return token_data except (ValueError, TypeError): diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_completion.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_completion.py deleted file mode 100644 index 663ecb2c4f..0000000000 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_completion.py +++ /dev/null @@ -1,348 +0,0 @@ -import io -import logging -from collections.abc import Callable, Sequence -from functools import partial -from typing import Any - -from datumaro.util import take_by -from sqlalchemy.orm import Session - -import src.cvat.api_calls as cvat_api -import src.services.cloud as cloud_service -import src.services.cvat as cvat_service -import src.services.webhook as oracle_db_service -from src.chain.escrow import get_escrow_manifest, validate_escrow -from src.core.annotation_meta import ANNOTATION_RESULTS_METAFILE_NAME, RESULTING_ANNOTATIONS_FILE -from src.core.config import CronConfig, StorageConfig -from src.core.manifest import parse_manifest -from src.core.oracle_events import ( - ExchangeOracleEvent_EscrowRecorded, - ExchangeOracleEvent_JobFinished, -) -from src.core.storage import compose_results_bucket_filename -from src.core.tasks import TaskTypes -from src.core.types import EscrowValidationStatuses, OracleWebhookTypes -from src.db import SessionLocal -from src.db.utils import ForUpdateParams -from src.handlers.job_export import ( - CVAT_EXPORT_FORMAT_MAPPING, - FileDescriptor, - postprocess_annotations, - prepare_annotation_metafile, -) -from src.models.cvat import Job, Project -from src.services.cloud.types import BucketAccessInfo - - -def _download_with_retries( - logger: logging.Logger, - download_callback: Callable[[], io.RawIOBase], - retry_callback: Callable[[], Any], - *, - max_attempts: int | None = None, -) -> io.RawIOBase: - """ - Sometimes CVAT downloading can fail with the 500 error. - This function tries to repeat the export in such cases. - """ - - if max_attempts is None: - max_attempts = CronConfig.track_completed_escrows_max_downloading_retries - - attempt = 0 - while attempt < max_attempts: - try: - return download_callback() - except cvat_api.exceptions.ApiException as e: - if 500 <= e.status < 600 and attempt + 1 < max_attempts: - attempt += 1 - logger.info(f"Retrying downloading, attempt #{attempt} of {max_attempts}...") - retry_callback() - else: - raise - return None - - -def _export_escrow_annotations( - logger: logging.Logger, - chain_id: int, - escrow_address: str, - escrow_projects: Sequence[Project], - session: Session, -) -> None: - manifest = parse_manifest(get_escrow_manifest(chain_id, escrow_address)) - - escrow_creation = cvat_service.get_escrow_creation_by_escrow_address( - session, - escrow_address, - chain_id, - active=False, - ) - if not escrow_creation: - raise AssertionError(f"Can't find escrow creation for escrow '{escrow_address}'") - - jobs = cvat_service.get_jobs_by_escrow_address(session, escrow_address, chain_id) - if len(jobs) != escrow_creation.total_jobs: - raise AssertionError( - f"Unexpected number of jobs fetched for escrow " - f"'{escrow_address}': {len(jobs)}, expected {escrow_creation.total_jobs}" - ) - - logger.debug(f"Downloading results for the escrow ({escrow_address=})") - - annotation_format = CVAT_EXPORT_FORMAT_MAPPING[manifest.annotation.type] - # FUTURE-TODO: probably can be removed in the future since - # these annotations are no longer used in Recording Oracle - job_annotations = _download_job_annotations(logger, annotation_format, jobs) - - if manifest.annotation.type == TaskTypes.image_skeletons_from_boxes.value: - # we'll have to merge annotations ourselves for skeletons - # might want to make this the only behavior in the future - project_annotations_file = None - project_images = None - else: - # escrows with simple task types must have only one project - try: - (project,) = escrow_projects - except ValueError: - raise NotImplementedError( - f"{manifest.annotation.type} is expected to have exactly one project," - f" not {len(escrow_projects)}" - ) - project_annotations_file = _download_project_annotations( - logger, - annotation_format, - project.cvat_id, - ) - project_images = cvat_service.get_project_images(session, project.cvat_id) - - resulting_annotations_file_desc = FileDescriptor( - filename=RESULTING_ANNOTATIONS_FILE, - file=project_annotations_file, - ) - - logger.debug(f"Postprocessing results for the escrow ({escrow_address=})") - - postprocess_annotations( - escrow_address=escrow_address, - chain_id=chain_id, - annotations=( - resulting_annotations_file_desc, - *job_annotations.values(), - ), - merged_annotation=resulting_annotations_file_desc, - manifest=manifest, - project_images=project_images, - ) - logger.debug(f"Uploading annotations for the escrow ({escrow_address=})") - - _upload_escrow_results( - files=( - resulting_annotations_file_desc, - *job_annotations.values(), - prepare_annotation_metafile(jobs=jobs), - ), - chain_id=chain_id, - escrow_address=escrow_address, - ) - - oracle_db_service.outbox.create_webhook( - session, - escrow_address=escrow_address, - chain_id=chain_id, - type=OracleWebhookTypes.recording_oracle, - event=ExchangeOracleEvent_EscrowRecorded(), - ) - - logger.info( - f"The escrow ({escrow_address=}) is completed, " - f"resulting annotations are processed successfully" - ) - - -def _request_escrow_validation( - logger: logging.Logger, - chain_id: int, - escrow_address: str, - escrow_projects: Sequence[Project], - session: Session, -) -> None: - # TODO: lock escrow once there is such a DB object - assert escrow_projects # unused, but must hold a lock - - # TODO: maybe upload only current iteration jobs - jobs = cvat_service.get_jobs_by_escrow_address(session, escrow_address, chain_id) - - logger.debug(f"Uploading assignment info for the escrow ({escrow_address=})") - - _upload_escrow_results( - files=[prepare_annotation_metafile(jobs=jobs)], - chain_id=chain_id, - escrow_address=escrow_address, - ) - - oracle_db_service.outbox.create_webhook( - session, - escrow_address=escrow_address, - chain_id=chain_id, - type=OracleWebhookTypes.recording_oracle, - event=ExchangeOracleEvent_JobFinished(), - ) - - logger.info(f"The escrow ({escrow_address=}) annotation is finished, " f"requesting validation") - - -def _upload_escrow_results( - files: Sequence[FileDescriptor], chain_id: int, escrow_address: str -) -> None: - storage_info = BucketAccessInfo.parse_obj(StorageConfig) - storage_client = cloud_service.make_client(storage_info) - - # always update annotation meta and resulting annotations files - # to have actual data for the current epoch - existing_storage_files = set( - storage_client.list_files( - prefix=compose_results_bucket_filename(escrow_address, chain_id, ""), - trim_prefix=True, - ) - ) - {ANNOTATION_RESULTS_METAFILE_NAME, RESULTING_ANNOTATIONS_FILE} - - for file_descriptor in files: - if file_descriptor.filename in existing_storage_files: - continue - - storage_client.create_file( - compose_results_bucket_filename( - escrow_address, - chain_id, - file_descriptor.filename, - ), - file_descriptor.file.read(), - ) - - -def _download_project_annotations( - logger: logging.Logger, annotation_format: str, project_cvat_id: int -) -> io.RawIOBase: - export_ids = [] - - def _request_export(cvat_id: int): - request_id = cvat_api.request_project_annotations(cvat_id, format_name=annotation_format) - export_ids.append(request_id) - return request_id - - def _download_export(): - request_id = export_ids.pop(0) - return cvat_api.get_project_annotations(request_id=request_id) - - _request_export(project_cvat_id) - - return _download_with_retries( - logger, - download_callback=_download_export, - retry_callback=partial(_request_export, project_cvat_id), - ) - - -def _download_job_annotations( - logger: logging.Logger, annotation_format: str, jobs: Sequence[Job] -) -> dict[int, FileDescriptor]: - # Collect raw annotations from CVAT, validate and convert them - # into a recording oracle suitable format - job_annotations: dict[int, FileDescriptor] = {} - - export_ids: list[tuple[Job, str]] = [] - - def _request_export(job: Job): - request_id = cvat_api.request_job_annotations(job.cvat_id, format_name=annotation_format) - export_ids.append((job, request_id)) - return request_id - - for jobs_batch in take_by( - jobs, count=CronConfig.track_completed_escrows_jobs_downloading_batch_size - ): - # Request jobs before downloading for faster batch downloading - for job in jobs_batch: - _request_export(job) - - while export_ids: - (job, request_id) = export_ids.pop(0) - - job_annotations_file = _download_with_retries( - logger, - download_callback=partial(cvat_api.get_job_annotations, request_id=request_id), - retry_callback=partial(_request_export, job), - ) - - job_assignment = job.latest_assignment - job_annotations[job.cvat_id] = FileDescriptor( - filename="project_{}-task_{}-job_{}-user_{}-assignment_{}.zip".format( - job.cvat_project_id, - job.cvat_task_id, - job.cvat_id, - job_assignment.user.cvat_id, - job_assignment.id, - ), - file=job_annotations_file, - ) - - return job_annotations - - -def _handle_escrow_validation( - logger: logging.Logger, - session: Session, - escrow_address: str, - chain_id: int, -): - validate_escrow(chain_id, escrow_address) - - escrow_projects = cvat_service.get_projects_by_escrow_address( - session, escrow_address, limit=None, for_update=ForUpdateParams(nowait=True) - ) - _request_escrow_validation(logger, chain_id, escrow_address, escrow_projects, session) - - -def handle_escrows_validations(logger: logging.Logger) -> None: - for _ in range(CronConfig.track_escrow_validations_chunk_size): - with SessionLocal.begin() as session: - # Need to work in separate transactions for each escrow, as a failing DB call - # (e.g. a failed lock attempt) will abort the transaction. A nested transaction - # can also be used for handling this. - escrow_validation = cvat_service.lock_escrow_for_validation(session) - if not escrow_validation: - break - - escrow_address = escrow_validation.escrow_address - chain_id = escrow_validation.chain_id - - update_kwargs = {} - try: - _handle_escrow_validation(logger, session, escrow_address, chain_id) - - # Change status so validation won't be attempted again - update_kwargs["status"] = EscrowValidationStatuses.in_progress - except Exception as e: - logger.exception(e) - - cvat_service.update_escrow_validation( - session, - escrow_address=escrow_address, - chain_id=chain_id, - increase_attempts=True, # increase attempts always to allow escrow rotation - **update_kwargs, - ) - - -def handle_escrow_export( - logger: logging.Logger, - session: Session, - escrow_address: str, - chain_id: int, -): - validate_escrow(chain_id, escrow_address) - - escrow_projects = cvat_service.get_projects_by_escrow_address( - session, escrow_address, limit=None, for_update=ForUpdateParams(nowait=True) - ) - _export_escrow_annotations(logger, chain_id, escrow_address, escrow_projects, session) diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/__init__.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/__init__.py new file mode 100644 index 0000000000..3e77c15b70 --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/__init__.py @@ -0,0 +1,3 @@ +from src.handlers.job_completion.handlers import handle_escrows_validations + +__all__ = ["handle_escrows_validations"] diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/factory.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/factory.py new file mode 100644 index 0000000000..7725248a78 --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/factory.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from src.chain.escrow import get_escrow_manifest +from src.core.manifest import get_manifest_task_type, parse_manifest +from src.core.tasks import TaskTypes +from src.handlers.job_completion.validators.audio import AudioTranscriptionJobValidator +from src.handlers.job_completion.validators.base import JobValidator + +if TYPE_CHECKING: + from collections.abc import Sequence + + from sqlalchemy.orm import Session + + from src.models.cvat import Project + + +def create_validator( + escrow_address: str, + chain_id: int, + session: Session, + escrow_projects: Sequence[Project], +) -> JobValidator: + manifest = parse_manifest(get_escrow_manifest(chain_id, escrow_address)) + task_type = get_manifest_task_type(manifest) + + match task_type: + case TaskTypes.audio_transcription: + validator_cls = AudioTranscriptionJobValidator + case ( + TaskTypes.image_label_binary + | TaskTypes.image_boxes + | TaskTypes.image_polygons + | TaskTypes.image_points + | TaskTypes.image_boxes_from_points + | TaskTypes.image_skeletons_from_boxes + ): + validator_cls = JobValidator + case _: + raise Exception(f"Unsupported task type {task_type}") + + return validator_cls(escrow_address, chain_id, session, escrow_projects) diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/handlers.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/handlers.py new file mode 100644 index 0000000000..e0cff6864f --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/handlers.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import src.services.cvat as cvat_service +from src.chain.escrow import validate_escrow +from src.core.config import CronConfig +from src.core.types import EscrowValidationStatuses +from src.db import SessionLocal +from src.db.utils import ForUpdateParams +from src.handlers.job_completion.factory import create_validator +from src.utils.logging import get_function_logger + +if TYPE_CHECKING: + import logging + + from sqlalchemy.orm import Session + + +def _handle_escrow_validation( + logger: logging.Logger, + session: Session, + escrow_address: str, + chain_id: int, +) -> None: + logger = get_function_logger(logger) + + validate_escrow(chain_id, escrow_address) + + # TODO: lock the escrow once there is such a DB object + escrow_projects = cvat_service.get_projects_by_escrow_address( + session, escrow_address, limit=None, for_update=ForUpdateParams(nowait=True) + ) + if not escrow_projects: + raise AssertionError(f"Can't find projects for escrow '{escrow_address}'") + + with create_validator( + escrow_address=escrow_address, + chain_id=chain_id, + session=session, + escrow_projects=escrow_projects, + ) as validator: + validator.set_logger(logger) + validator.validate() + + +def handle_escrows_validations(logger: logging.Logger) -> None: + for _ in range(CronConfig.track_escrow_validations_chunk_size): + with SessionLocal.begin() as session: + # Need to work in separate transactions for each escrow, as a failing DB call + # (e.g. a failed lock attempt) will abort the transaction. A nested transaction + # can also be used for handling this. + escrow_validation = cvat_service.lock_escrow_for_validation(session) + if not escrow_validation: + break + + escrow_address = escrow_validation.escrow_address + chain_id = escrow_validation.chain_id + + update_kwargs = {} + try: + _handle_escrow_validation(logger, session, escrow_address, chain_id) + + # Change status so validation won't be attempted again + update_kwargs["status"] = EscrowValidationStatuses.in_progress + except Exception as e: + logger.exception(e) + + cvat_service.update_escrow_validation( + session, + escrow_address=escrow_address, + chain_id=chain_id, + increase_attempts=True, # increase attempts always to allow escrow rotation + **update_kwargs, + ) diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/validators/__init__.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/validators/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/validators/audio.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/validators/audio.py new file mode 100644 index 0000000000..25e4cac658 --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/validators/audio.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import src.services.cvat as cvat_service +from src.handlers.job_completion.validators.base import JobValidator +from src.handlers.job_export.downloading import download_job_annotations +from src.handlers.job_export.results import ( + FileDescriptor, + prepare_annotation_metafile, + upload_escrow_results, +) + +if TYPE_CHECKING: + from src.models.cvat import Job + +CVAT_AUDIO_EXPORT_FORMAT = "Generic TSV 1.0" + +# subdir (in the escrow results dir) for the per-assignment transcription TSVs +ASSIGNMENTS_DIR = "assignments" + + +class AudioTranscriptionJobValidator(JobValidator): + def _save_annotation_results(self) -> None: + assert self.escrow_projects # unused, but must hold a lock + + cvat_jobs = cvat_service.get_jobs_by_escrow_address( + self.session, self.escrow_address, self.chain_id + ) + + self.logger.debug( + f"Downloading annotations for the escrow " + f"(escrow_address={self.escrow_address})" + ) + cvat_job_annotations = download_job_annotations( + self.logger, CVAT_AUDIO_EXPORT_FORMAT, cvat_jobs, expect_zip=False + ) + + result_files: list[FileDescriptor] = [ + self._make_annotation_descriptor(job, cvat_job_annotations[job.cvat_id]) + for job in cvat_jobs + ] + result_files.append(prepare_annotation_metafile(jobs=cvat_jobs)) + + self.logger.debug( + f"Recording annotations for the escrow " + f"(escrow_address={self.escrow_address})" + ) + upload_escrow_results( + files=result_files, chain_id=self.chain_id, escrow_address=self.escrow_address + ) + + def _make_annotation_descriptor(self, job: Job, annotations: FileDescriptor) -> FileDescriptor: + assignment = job.latest_assignment + return FileDescriptor( + filename=f"{ASSIGNMENTS_DIR}/{job.cvat_id}-{assignment.id}.tsv", + file=annotations.file, + ) diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/validators/base.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/validators/base.py new file mode 100644 index 0000000000..a82a93d76a --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/validators/base.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +from contextlib import ExitStack +from typing import TYPE_CHECKING + +import src.services.cvat as cvat_service +import src.services.webhook as oracle_db_service +from src.core.oracle_events import ExchangeOracleEvent_JobFinished +from src.core.types import OracleWebhookTypes +from src.handlers.job_export.results import prepare_annotation_metafile, upload_escrow_results +from src.utils.logging import NullLogger + +if TYPE_CHECKING: + from collections.abc import Sequence + from logging import Logger + + from sqlalchemy.orm import Session + + from src.handlers.job_export.results import FileDescriptor + from src.models.cvat import Project + + +class JobValidator: + def __init__( + self, + escrow_address: str, + chain_id: int, + session: Session, + escrow_projects: Sequence[Project], + ) -> None: + self.exit_stack = ExitStack() + self.escrow_address = escrow_address + self.chain_id = chain_id + self.session = session + self.escrow_projects = escrow_projects + + self.logger: Logger = NullLogger() + + def __enter__(self): + return self + + def __exit__(self, *args, **kwargs): + self.close() + + def close(self): + self.exit_stack.close() + + def set_logger(self, logger: Logger): + self.logger = logger + return self + + def validate(self) -> None: + self._save_annotation_results() + self._notify() + + def _save_annotation_results(self) -> None: + # TODO: maybe upload only current iteration jobs + jobs = cvat_service.get_jobs_by_escrow_address( + self.session, self.escrow_address, self.chain_id + ) + result_files: list[FileDescriptor] = [prepare_annotation_metafile(jobs=jobs)] + + self.logger.debug( + f"Uploading assignment info for the escrow (escrow_address={self.escrow_address})" + ) + upload_escrow_results( + files=result_files, chain_id=self.chain_id, escrow_address=self.escrow_address + ) + + def _notify(self) -> None: + oracle_db_service.outbox.create_webhook( + self.session, + escrow_address=self.escrow_address, + chain_id=self.chain_id, + type=OracleWebhookTypes.recording_oracle, + event=ExchangeOracleEvent_JobFinished(), + ) + self.logger.info( + f"The escrow (escrow_address={self.escrow_address}) annotation is finished, " + "requesting validation" + ) diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/factory.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/factory.py index 3b35d4161e..7d9f362921 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/factory.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/factory.py @@ -1,9 +1,7 @@ from __future__ import annotations -from typing import cast - from src.chain.escrow import get_escrow_manifest -from src.core import manifest as manifest_utils +from src.core.manifest import get_manifest_task_type, parse_manifest from src.core.tasks import TaskTypes from src.handlers.job_creation.builders.audio.transcription import AudioTranscriptionTaskBuilder from src.handlers.job_creation.builders.vision.basic import ( @@ -25,16 +23,8 @@ def create_task(escrow_address: str, chain_id: int) -> None: logger = get_function_logger(module_logger) - manifest = manifest_utils.parse_manifest(get_escrow_manifest(chain_id, escrow_address)) - - if manifest.version == 1: - manifest = cast("manifest_utils.v1.JobManifest", manifest) - task_type = manifest.annotation.type - elif manifest.version == 2: - manifest = cast("manifest_utils.v2.JobManifest", manifest) - task_type = manifest.job_type - else: - raise NotImplementedError(f"Unknown manifest version '{manifest.version}'") + manifest = parse_manifest(get_escrow_manifest(chain_id, escrow_address)) + task_type = get_manifest_task_type(manifest) match task_type: case TaskTypes.image_boxes: diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/__init__.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/__init__.py new file mode 100644 index 0000000000..a109200e4e --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/__init__.py @@ -0,0 +1,3 @@ +from src.handlers.job_export.handlers import handle_escrow_export + +__all__ = ["handle_escrow_export"] diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/downloading.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/downloading.py new file mode 100644 index 0000000000..61f96b68ed --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/downloading.py @@ -0,0 +1,123 @@ +"""CVAT annotation downloading for the escrow export flow.""" + +from __future__ import annotations + +from functools import partial +from typing import TYPE_CHECKING + +from datumaro.util import take_by + +import src.cvat.api_calls as cvat_api +from src.core.config import CronConfig +from src.handlers.job_export.results import FileDescriptor + +if TYPE_CHECKING: + import io + import logging + from collections.abc import Callable, Sequence + from typing import Any + + from src.models.cvat import Job + + +def download_with_retries( + logger: logging.Logger, + download_callback: Callable[[], io.RawIOBase], + retry_callback: Callable[[], Any], + *, + max_attempts: int | None = None, +) -> io.RawIOBase: + """ + Sometimes CVAT downloading can fail with the 500 error. + This function tries to repeat the export in such cases. + """ + + if max_attempts is None: + max_attempts = CronConfig.track_completed_escrows_max_downloading_retries + + attempt = 0 + while attempt < max_attempts: + try: + return download_callback() + except cvat_api.exceptions.ApiException as e: + if 500 <= e.status < 600 and attempt + 1 < max_attempts: + attempt += 1 + logger.info(f"Retrying downloading, attempt #{attempt} of {max_attempts}...") + retry_callback() + else: + raise + return None + + +def download_project_annotations( + logger: logging.Logger, annotation_format: str, project_cvat_id: int +) -> io.RawIOBase: + export_ids = [] + + def _request_export(cvat_id: int): + request_id = cvat_api.request_project_annotations(cvat_id, format_name=annotation_format) + export_ids.append(request_id) + return request_id + + def _download_export(): + request_id = export_ids.pop(0) + return cvat_api.get_project_annotations(request_id=request_id) + + _request_export(project_cvat_id) + + return download_with_retries( + logger, + download_callback=_download_export, + retry_callback=partial(_request_export, project_cvat_id), + ) + + +def download_job_annotations( + logger: logging.Logger, + annotation_format: str, + jobs: Sequence[Job], + *, + expect_zip: bool = True, +) -> dict[int, FileDescriptor]: + # Collect raw annotations from CVAT, validate and convert them + # into a recording oracle suitable format + job_annotations: dict[int, FileDescriptor] = {} + + export_ids: list[tuple[Job, str]] = [] + + def _request_export(job: Job): + request_id = cvat_api.request_job_annotations(job.cvat_id, format_name=annotation_format) + export_ids.append((job, request_id)) + return request_id + + for jobs_batch in take_by( + jobs, count=CronConfig.track_completed_escrows_jobs_downloading_batch_size + ): + # Request jobs before downloading for faster batch downloading + for job in jobs_batch: + _request_export(job) + + while export_ids: + (job, request_id) = export_ids.pop(0) + + job_annotations_file = download_with_retries( + logger, + download_callback=partial( + cvat_api.get_job_annotations, request_id=request_id, expect_zip=expect_zip + ), + retry_callback=partial(_request_export, job), + ) + + job_assignment = job.latest_assignment + job_annotations[job.cvat_id] = FileDescriptor( + filename="project_{}-task_{}-job_{}-user_{}-assignment_{}.zip".format( + job.cvat_project_id, + job.cvat_task_id, + job.cvat_id, + job_assignment.user.cvat_id, + job_assignment.id, + ), + file=job_annotations_file, + ) + + return job_annotations diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/__init__.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/audio.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/audio.py new file mode 100644 index 0000000000..622c3f006e --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/audio.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from src.handlers.job_export.exporters.base import JobExporter + +if TYPE_CHECKING: + from collections.abc import Sequence + + from sqlalchemy.orm import Session + + from src.models.cvat import Project + + +class AudioTranscriptionJobExporter(JobExporter): + """Export flow for audio_transcription escrows. + + FUTURE-TODO: read the interval (transcription) annotations from the annotation jobs, map placed + regions back to source via the persisted assignments/regions meta, and score against GT (WER). + """ + + def export(self, session: Session, escrow_projects: Sequence[Project]) -> None: + raise NotImplementedError("audio_transcription export is not implemented yet") diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/base.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/base.py new file mode 100644 index 0000000000..fc69656632 --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/base.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +from abc import ABCMeta, abstractmethod +from contextlib import ExitStack +from typing import TYPE_CHECKING + +import src.services.webhook as oracle_db_service +from src.core.oracle_events import ExchangeOracleEvent_EscrowRecorded +from src.core.types import OracleWebhookTypes +from src.handlers.job_export.results import upload_escrow_results +from src.utils.logging import NullLogger + +if TYPE_CHECKING: + from collections.abc import Sequence + from logging import Logger + + from sqlalchemy.orm import Session + + from src.core.manifest import ManifestBase + from src.handlers.job_export.results import FileDescriptor + from src.models.cvat import Project + + +class JobExporter(metaclass=ABCMeta): + """ + Base for task-specific escrow annotation exporters. + + Subclasses must implement :meth:`export` + """ + + def __init__(self, manifest: ManifestBase, escrow_address: str, chain_id: int) -> None: + self.exit_stack = ExitStack() + self.manifest = manifest + self.escrow_address = escrow_address + self.chain_id = chain_id + + self.logger: Logger = NullLogger() + + def __enter__(self): + return self + + def __exit__(self, *args, **kwargs): + self.close() + + def close(self): + self.exit_stack.close() + + def set_logger(self, logger: Logger): + self.logger = logger + return self + + @abstractmethod + def export(self, session: Session, escrow_projects: Sequence[Project]) -> None: ... + + def _upload_results(self, files: Sequence[FileDescriptor]) -> None: + upload_escrow_results( + files=files, chain_id=self.chain_id, escrow_address=self.escrow_address + ) + + def _emit_escrow_recorded(self, session: Session) -> None: + oracle_db_service.outbox.create_webhook( + session, + escrow_address=self.escrow_address, + chain_id=self.chain_id, + type=OracleWebhookTypes.recording_oracle, + event=ExchangeOracleEvent_EscrowRecorded(), + ) diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_export.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/vision.py similarity index 73% rename from packages/examples/cvat/exchange-oracle/src/handlers/job_export.py rename to packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/vision.py index 9abeae929a..646ba6a114 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_export.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/vision.py @@ -1,27 +1,49 @@ +"""Image task exporters. + +Each exporter owns both the orchestration (download annotations, upload results, notify the +recording oracle) and its task-specific datumaro postprocessing. ``ImageJobExporter`` holds the +shared export flow + the generic CVAT->datumaro conversion pipeline; per-task subclasses override +only the parts that differ. +""" + +from __future__ import annotations + import io import os import zipfile -from collections.abc import Sequence -from dataclasses import dataclass from tempfile import TemporaryDirectory +from typing import TYPE_CHECKING import datumaro as dm -from datumaro.components.dataset import Dataset import src.core.tasks.boxes_from_points as boxes_from_points_task import src.core.tasks.skeletons_from_boxes as skeletons_from_boxes_task +import src.services.cvat as cvat_service import src.utils.annotations as annotation_utils -from src.core.annotation_meta import ANNOTATION_RESULTS_METAFILE_NAME, AnnotationMeta, JobMeta +from src.core.annotation_meta import RESULTING_ANNOTATIONS_FILE from src.core.config import Config -from src.core.manifest.v1 import JobManifest +from src.core.manifest import get_manifest_task_type from src.core.storage import compose_data_bucket_filename from src.core.tasks import TaskTypes from src.core.tasks.cvat_formats import DM_DATASET_FORMAT_MAPPING -from src.models.cvat import Image, Job +from src.handlers.job_export.downloading import ( + download_job_annotations, + download_project_annotations, +) +from src.handlers.job_export.exporters.base import JobExporter +from src.handlers.job_export.results import FileDescriptor, prepare_annotation_metafile from src.services.cloud import make_client as make_cloud_client from src.services.cloud.utils import BucketAccessInfo from src.utils.zip_archive import extract_zip_archive, write_dir_to_zip_archive +if TYPE_CHECKING: + from collections.abc import Sequence + + from datumaro.components.dataset import Dataset + from sqlalchemy.orm import Session + + from src.models.cvat import Image, Project + CVAT_EXPORT_FORMAT_MAPPING = { TaskTypes.image_label_binary: "CVAT for images 1.1", TaskTypes.image_points: "CVAT for images 1.1", @@ -37,65 +59,114 @@ } -@dataclass -class FileDescriptor: - filename: str - file: io.RawIOBase | None - +class ImageJobExporter(JobExporter): + """Base image exporter: download the single project + per-job annotations, run the datumaro + conversion pipeline, upload the results, and notify the recording oracle.""" -def prepare_annotation_metafile(jobs: list[Job]) -> FileDescriptor: - """ - Prepares a task/project annotation descriptor file with annotator mapping. - """ + def export(self, session: Session, escrow_projects: Sequence[Project]) -> None: + escrow_address = self.escrow_address + chain_id = self.chain_id - meta = AnnotationMeta( - jobs=[ - JobMeta( - job_id=job.cvat_id, - annotator_wallet_address=job.latest_assignment.user_wallet_address, - assignment_id=job.latest_assignment.id, - task_id=job.cvat_task_id, - start_frame=job.start_frame, - stop_frame=job.stop_frame, + escrow_creation = cvat_service.get_escrow_creation_by_escrow_address( + session, escrow_address, chain_id, active=False + ) + if not escrow_creation: + raise AssertionError(f"Can't find escrow creation for escrow '{escrow_address}'") + + jobs = cvat_service.get_jobs_by_escrow_address(session, escrow_address, chain_id) + if len(jobs) != escrow_creation.total_jobs: + raise AssertionError( + f"Unexpected number of jobs fetched for escrow " + f"'{escrow_address}': {len(jobs)}, expected {escrow_creation.total_jobs}" ) - for job in jobs - ] - ) - return FileDescriptor( - ANNOTATION_RESULTS_METAFILE_NAME, file=io.BytesIO(meta.model_dump_json().encode()) - ) + self.logger.debug(f"Downloading results for the escrow ({escrow_address=})") + + annotation_format = self._annotation_format + # FUTURE-TODO: probably can be removed in the future since + # these annotations are no longer used in Recording Oracle + job_annotations = download_job_annotations(self.logger, annotation_format, jobs) + + project_annotations_file, project_images = self._collect_project_annotations( + session, escrow_projects, annotation_format + ) + + resulting_annotations_file_desc = FileDescriptor( + filename=RESULTING_ANNOTATIONS_FILE, + file=project_annotations_file, + ) + annotations = (resulting_annotations_file_desc, *job_annotations.values()) + + self.logger.debug(f"Postprocessing results for the escrow ({escrow_address=})") + + self._merged_annotation_file = resulting_annotations_file_desc + self._project_images = project_images + self._prepare() + self._postprocess(annotations) + + self.logger.debug(f"Uploading annotations for the escrow ({escrow_address=})") + + self._upload_results(files=(*annotations, prepare_annotation_metafile(jobs=jobs))) + + self._emit_escrow_recorded(session) + self.logger.info( + f"The escrow ({escrow_address=}) is completed, " + f"resulting annotations are processed successfully" + ) + + # -- formats ----------------------------------------------------------- # + + @property + def _task_type(self) -> TaskTypes: + return get_manifest_task_type(self.manifest) + + @property + def _annotation_format(self) -> str: + return CVAT_EXPORT_FORMAT_MAPPING[self._task_type] + + @property + def _input_format(self) -> str: + return CVAT_EXPORT_FORMAT_TO_DM_MAPPING[self._annotation_format] -class _TaskProcessor: - def __init__( + @property + def _output_format(self) -> str: + return DM_DATASET_FORMAT_MAPPING[self._task_type] + + # -- project annotations ----------------------------------------------- # + + def _collect_project_annotations( self, - escrow_address: str, - chain_id: int, - annotations: Sequence[FileDescriptor], - merged_annotation: FileDescriptor, - *, - manifest: JobManifest, - project_images: list[Image], - ) -> None: - self.escrow_address = escrow_address - self.chain_id = chain_id - self.annotation_files = annotations - self.merged_annotation_file = merged_annotation - self.manifest = manifest - self.project_images = project_images - - self.input_format = CVAT_EXPORT_FORMAT_TO_DM_MAPPING[ - CVAT_EXPORT_FORMAT_MAPPING[manifest.annotation.type] - ] - self.output_format = DM_DATASET_FORMAT_MAPPING[manifest.annotation.type] + session: Session, + escrow_projects: Sequence[Project], + annotation_format: str, + ) -> tuple[io.RawIOBase | None, list[Image] | None]: + # escrows with simple task types must have only one project + try: + (project,) = escrow_projects + except ValueError: + raise NotImplementedError( + f"{self._task_type} is expected to have exactly one project," + f" not {len(escrow_projects)}" + ) - def _is_merged_dataset(self, ann_descriptor: FileDescriptor) -> bool: - return ann_descriptor == self.merged_annotation_file + project_annotations_file = download_project_annotations( + self.logger, annotation_format, project.cvat_id + ) + project_images = cvat_service.get_project_images(session, project.cvat_id) + return project_annotations_file, project_images + + # -- datumaro conversion pipeline -------------------------------------- # + + def _prepare(self) -> None: + """Download any task-specific meta needed for postprocessing (default: nothing).""" - def process(self): + def _postprocess(self, annotations: Sequence[FileDescriptor]) -> None: + self._run_pipeline(annotations) + + def _run_pipeline(self, annotations: Sequence[FileDescriptor]) -> None: with TemporaryDirectory() as tempdir: - for ann_descriptor in self.annotation_files: + for ann_descriptor in annotations: if not zipfile.is_zipfile(ann_descriptor.file): raise ValueError("Annotation files must be zip files") ann_descriptor.file.seek(0) @@ -119,6 +190,9 @@ def process(self): ann_descriptor.file = converted_dataset_archive + def _is_merged_dataset(self, ann_descriptor: FileDescriptor) -> bool: + return ann_descriptor == self._merged_annotation_file + def _process_annotation_file( self, ann_descriptor: FileDescriptor, input_dir: str, output_dir: str ): @@ -127,10 +201,10 @@ def _process_annotation_file( self._export_dataset(output_dataset, output_dir) def _parse_dataset(self, ann_descriptor: FileDescriptor, dataset_dir: str) -> dm.Dataset: # noqa: ARG002 - return dm.Dataset.import_from(dataset_dir, self.input_format) + return dm.Dataset.import_from(dataset_dir, self._input_format) def _export_dataset(self, dataset: dm.Dataset, output_dir: str): - dataset.export(output_dir, self.output_format, save_media=False) + dataset.export(output_dir, self._output_format, save_media=False) def _process_dataset( self, dataset: dm.Dataset, *, ann_descriptor: FileDescriptor @@ -149,23 +223,23 @@ def _process_dataset( def _process_merged_dataset(self, input_dataset: dm.Dataset) -> dm.Dataset: return annotation_utils.remove_duplicated_gt_frames( input_dataset, - known_frames=[image.filename for image in self.project_images], + known_frames=[image.filename for image in self._project_images], ) -class _LabelsTaskProcessor(_TaskProcessor): +class LabelsJobExporter(ImageJobExporter): pass -class _BoxesTaskProcessor(_TaskProcessor): +class BoxesJobExporter(ImageJobExporter): pass -class _PolygonsTaskProcessor(_TaskProcessor): +class PolygonsJobExporter(ImageJobExporter): pass -class _PointsTaskProcessor(_TaskProcessor): +class PointsJobExporter(ImageJobExporter): def _parse_dataset(self, ann_descriptor: FileDescriptor, dataset_dir: str) -> Dataset: annotation_utils.prepare_cvat_annotations_for_dm(dataset_dir) return super()._parse_dataset(ann_descriptor, dataset_dir) @@ -182,10 +256,8 @@ def _process_dataset(self, dataset: Dataset, *, ann_descriptor: FileDescriptor) return super()._process_dataset(dataset, ann_descriptor=ann_descriptor) -class _BoxesFromPointsTaskProcessor(_TaskProcessor): - def __init__(self, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - +class BoxesFromPointsJobExporter(ImageJobExporter): + def _prepare(self) -> None: roi_filenames, roi_infos, points_dataset = self._download_task_meta() self.points_dataset = points_dataset @@ -288,10 +360,21 @@ def _process_merged_dataset(self, input_dataset: Dataset) -> Dataset: return merged_sample_dataset -class _SkeletonsFromBoxesTaskProcessor(_TaskProcessor): - def __init__(self, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) +class SkeletonsJobExporter(ImageJobExporter): + """image_skeletons_from_boxes: annotations are accumulated into a merged dataset from the + per-job files here, so there is no single project export to download.""" + def _collect_project_annotations( + self, + session: Session, + escrow_projects: Sequence[Project], + annotation_format: str, + ) -> tuple[io.RawIOBase | None, list[Image] | None]: + assert escrow_projects # unused, but must hold the lock + del session, annotation_format # the jobs are self-merged, no project export + return None, None + + def _prepare(self) -> None: roi_filenames, roi_infos, boxes_dataset, job_label_mapping = self._download_task_meta() self.boxes_dataset = boxes_dataset @@ -559,22 +642,19 @@ def _process_dataset(self, dataset: Dataset, *, ann_descriptor: FileDescriptor) def _process_merged_dataset(self, merged_dataset: Dataset) -> Dataset: return merged_dataset - def process(self): - assert self.merged_annotation_file.file is None + def _postprocess(self, annotations: Sequence[FileDescriptor]) -> None: + assert self._merged_annotation_file.file is None # Accumulate points from separate job annotations, then export the resulting dataset self._init_merged_dataset() - self.annotation_files = [ - fd for fd in self.annotation_files if not self._is_merged_dataset(fd) - ] - super().process() - self.annotation_files.append(self.merged_annotation_file) + job_files = [fd for fd in annotations if not self._is_merged_dataset(fd)] + self._run_pipeline(job_files) with TemporaryDirectory() as tempdir: export_dir = os.path.join( tempdir, - os.path.splitext(os.path.basename(self.merged_annotation_file.filename))[0] + os.path.splitext(os.path.basename(self._merged_annotation_file.filename))[0] + "_conv", ) @@ -585,37 +665,4 @@ def process(self): write_dir_to_zip_archive(export_dir, converted_dataset_archive) converted_dataset_archive.seek(0) - self.merged_annotation_file.file = converted_dataset_archive - - -def postprocess_annotations( - escrow_address: str, - chain_id: int, - annotations: Sequence[FileDescriptor], - merged_annotation: FileDescriptor, - *, - manifest: JobManifest, - project_images: list[Image], -) -> None: - """ - Processes annotations and updates the files list inplace - """ - processor_classes: dict[TaskTypes, type[_TaskProcessor]] = { - TaskTypes.image_label_binary: _LabelsTaskProcessor, - TaskTypes.image_boxes: _BoxesTaskProcessor, - TaskTypes.image_polygons: _PolygonsTaskProcessor, - TaskTypes.image_points: _PointsTaskProcessor, - TaskTypes.image_boxes_from_points: _BoxesFromPointsTaskProcessor, - TaskTypes.image_skeletons_from_boxes: _SkeletonsFromBoxesTaskProcessor, - } - - task_type = manifest.annotation.type - processor = processor_classes[task_type]( - escrow_address=escrow_address, - chain_id=chain_id, - annotations=annotations, - merged_annotation=merged_annotation, - manifest=manifest, - project_images=project_images, - ) - processor.process() + self._merged_annotation_file.file = converted_dataset_archive diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/factory.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/factory.py new file mode 100644 index 0000000000..0e71f63c4d --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/factory.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from src.core.manifest import get_manifest_task_type +from src.core.tasks import TaskTypes +from src.handlers.job_export.exporters.audio import AudioTranscriptionJobExporter +from src.handlers.job_export.exporters.vision import ( + BoxesFromPointsJobExporter, + BoxesJobExporter, + LabelsJobExporter, + PointsJobExporter, + PolygonsJobExporter, + SkeletonsJobExporter, +) + +if TYPE_CHECKING: + from src.core.manifest import ManifestBase + from src.handlers.job_export.exporters.base import JobExporter + + +def create_exporter(manifest: ManifestBase, escrow_address: str, chain_id: int) -> JobExporter: + task_type = get_manifest_task_type(manifest) + + match task_type: + case TaskTypes.image_label_binary: + exporter_type = LabelsJobExporter + case TaskTypes.image_boxes: + exporter_type = BoxesJobExporter + case TaskTypes.image_polygons: + exporter_type = PolygonsJobExporter + case TaskTypes.image_points: + exporter_type = PointsJobExporter + case TaskTypes.image_boxes_from_points: + exporter_type = BoxesFromPointsJobExporter + case TaskTypes.image_skeletons_from_boxes: + exporter_type = SkeletonsJobExporter + case TaskTypes.audio_transcription: + exporter_type = AudioTranscriptionJobExporter + case _: + raise Exception(f"Unsupported task type {task_type}") + + return exporter_type(manifest, escrow_address, chain_id) diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/handlers.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/handlers.py new file mode 100644 index 0000000000..6fcb3dc9e7 --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/handlers.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import src.services.cvat as cvat_service +from src.chain.escrow import get_escrow_manifest, validate_escrow +from src.core.manifest import parse_manifest +from src.db.utils import ForUpdateParams +from src.handlers.job_export.factory import create_exporter + +if TYPE_CHECKING: + import logging + + from sqlalchemy.orm import Session + + +def handle_escrow_export( + logger: logging.Logger, + session: Session, + escrow_address: str, + chain_id: int, +) -> None: + validate_escrow(chain_id, escrow_address) + + escrow_projects = cvat_service.get_projects_by_escrow_address( + session, escrow_address, limit=None, for_update=ForUpdateParams(nowait=True) + ) + + manifest = parse_manifest(get_escrow_manifest(chain_id, escrow_address)) + + with create_exporter(manifest, escrow_address, chain_id) as exporter: + exporter.set_logger(logger) + exporter.export(session, escrow_projects) diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/results.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/results.py new file mode 100644 index 0000000000..05ef3492ea --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/results.py @@ -0,0 +1,82 @@ +"""Shared result/metafile primitives for escrow export and validation flows.""" + +from __future__ import annotations + +import io +from dataclasses import dataclass +from typing import TYPE_CHECKING + +import src.services.cloud as cloud_service +from src.core.annotation_meta import ( + ANNOTATION_RESULTS_METAFILE_NAME, + RESULTING_ANNOTATIONS_FILE, + AnnotationMeta, + JobMeta, +) +from src.core.config import StorageConfig +from src.core.storage import compose_results_bucket_filename +from src.services.cloud.types import BucketAccessInfo + +if TYPE_CHECKING: + from collections.abc import Sequence + + from src.models.cvat import Job + + +@dataclass +class FileDescriptor: + filename: str + file: io.RawIOBase | None + + +def prepare_annotation_metafile(jobs: list[Job]) -> FileDescriptor: + """ + Prepares a task/project annotation descriptor file with annotator mapping. + """ + + meta = AnnotationMeta( + jobs=[ + JobMeta( + job_id=job.cvat_id, + annotator_wallet_address=job.latest_assignment.user_wallet_address, + assignment_id=job.latest_assignment.id, + task_id=job.cvat_task_id, + start_frame=job.start_frame, + stop_frame=job.stop_frame, + ) + for job in jobs + ] + ) + + return FileDescriptor( + ANNOTATION_RESULTS_METAFILE_NAME, file=io.BytesIO(meta.model_dump_json().encode()) + ) + + +def upload_escrow_results( + files: Sequence[FileDescriptor], chain_id: int, escrow_address: str +) -> None: + storage_info = BucketAccessInfo.parse_obj(StorageConfig) + storage_client = cloud_service.make_client(storage_info) + + # always update annotation meta and resulting annotations files + # to have actual data for the current epoch + existing_storage_files = set( + storage_client.list_files( + prefix=compose_results_bucket_filename(escrow_address, chain_id, ""), + trim_prefix=True, + ) + ) - {ANNOTATION_RESULTS_METAFILE_NAME, RESULTING_ANNOTATIONS_FILE} + + for file_descriptor in files: + if file_descriptor.filename in existing_storage_files: + continue + + storage_client.create_file( + compose_results_bucket_filename( + escrow_address, + chain_id, + file_descriptor.filename, + ), + file_descriptor.file.read(), + ) diff --git a/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_escrows.py b/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_escrows.py index 9cc86c5d64..18e7e422b3 100644 --- a/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_escrows.py +++ b/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_escrows.py @@ -26,7 +26,7 @@ from src.crons import track_completed_escrows from src.crons.cvat.state_trackers import track_escrow_validations from src.db import SessionLocal -from src.handlers.job_completion import handle_escrow_export +from src.handlers.job_export import handle_escrow_export from src.models.cvat import ( Assignment, EscrowCreation, @@ -263,8 +263,8 @@ def test_can_request_validation(self): self.session.commit() with ( - patch("src.handlers.job_completion.validate_escrow"), - patch("src.handlers.job_completion.cloud_service") as mock_cloud_service, + patch("src.handlers.job_completion.handlers.validate_escrow"), + patch("src.handlers.job_export.results.cloud_service") as mock_cloud_service, ): mock_storage_client = Mock() mock_storage_client.create_file = Mock() @@ -373,8 +373,8 @@ def test_request_validation_error_in_file_uploading_increases_attempts(self): self.session.commit() with ( - patch("src.handlers.job_completion.validate_escrow"), - patch("src.handlers.job_completion.cloud_service") as mock_cloud_service, + patch("src.handlers.job_completion.handlers.validate_escrow"), + patch("src.handlers.job_export.results.cloud_service") as mock_cloud_service, patch("src.services.cloud.make_client"), ): mock_cloud_service.make_client.return_value.create_file.side_effect = _TestException() @@ -466,8 +466,8 @@ def test_can_request_validation_multiple_projects_per_escrow_all_completed(self) self.session.commit() with ( - patch("src.handlers.job_completion.validate_escrow"), - patch("src.handlers.job_completion.cloud_service") as mock_cloud_service, + patch("src.handlers.job_completion.handlers.validate_escrow"), + patch("src.handlers.job_export.results.cloud_service") as mock_cloud_service, ): mock_storage_client = Mock() mock_storage_client.create_file = Mock() @@ -573,10 +573,10 @@ def test_can_export_escrow(self): with ( open("tests/assets/manifests/manifest.json") as data, - patch("src.handlers.job_completion.get_escrow_manifest") as mock_get_manifest, - patch("src.handlers.job_completion.validate_escrow"), - patch("src.handlers.job_completion.cvat_api") as mock_cvat_api, - patch("src.handlers.job_completion.cloud_service") as mock_cloud_service, + patch("src.handlers.job_export.handlers.get_escrow_manifest") as mock_get_manifest, + patch("src.handlers.job_export.handlers.validate_escrow"), + patch("src.handlers.job_export.downloading.cvat_api") as mock_cvat_api, + patch("src.handlers.job_export.results.cloud_service") as mock_cloud_service, ): manifest = json.load(data) mock_get_manifest.return_value = manifest @@ -701,12 +701,12 @@ def test_can_export_escrow_error_getting_annotations(self): with ( open("tests/assets/manifests/manifest.json") as data, - patch("src.handlers.job_completion.get_escrow_manifest") as mock_get_manifest, - patch("src.handlers.job_completion.validate_escrow"), + patch("src.handlers.job_export.handlers.get_escrow_manifest") as mock_get_manifest, + patch("src.handlers.job_export.handlers.validate_escrow"), patch( - "src.handlers.job_completion.cvat_api.request_job_annotations" + "src.handlers.job_export.downloading.cvat_api.request_job_annotations" ) as mock_request_job_annotations, - patch("src.handlers.job_completion.cloud_service") as mock_cloud_service, + patch("src.handlers.job_export.results.cloud_service") as mock_cloud_service, ): manifest = json.load(data) mock_get_manifest.return_value = manifest @@ -824,10 +824,10 @@ def test_can_export_escrow_error_uploading_files(self): with ( open("tests/assets/manifests/manifest.json") as data, - patch("src.handlers.job_completion.get_escrow_manifest") as mock_get_manifest, - patch("src.handlers.job_completion.validate_escrow"), - patch("src.handlers.job_completion.cvat_api") as mock_cvat_api, - patch("src.handlers.job_completion.cloud_service") as mock_cloud_service, + patch("src.handlers.job_export.handlers.get_escrow_manifest") as mock_get_manifest, + patch("src.handlers.job_export.handlers.validate_escrow"), + patch("src.handlers.job_export.downloading.cvat_api") as mock_cvat_api, + patch("src.handlers.job_export.results.cloud_service") as mock_cloud_service, patch("src.services.cloud.make_client"), ): manifest = json.load(data) @@ -949,12 +949,14 @@ def test_can_export_multiple_projects_per_escrow(self): with ( open("tests/assets/manifests/manifest.json") as manifest_data, - patch("src.handlers.job_completion.get_escrow_manifest") as mock_get_manifest, - patch("src.handlers.job_completion.validate_escrow"), - patch("src.handlers.job_completion.cvat_api") as mock_cvat_api, - patch("src.handlers.job_completion.cloud_service") as mock_cloud_service, + patch("src.handlers.job_export.handlers.get_escrow_manifest") as mock_get_manifest, + patch("src.handlers.job_export.handlers.validate_escrow"), + patch("src.handlers.job_export.downloading.cvat_api") as mock_cvat_api, + patch("src.handlers.job_export.results.cloud_service") as mock_cloud_service, + patch("src.handlers.job_export.exporters.vision.SkeletonsJobExporter._prepare"), patch( - "src.handlers.job_completion.postprocess_annotations" + "src.handlers.job_export.exporters.vision.SkeletonsJobExporter._postprocess", + autospec=True, ) as mock_postprocess_annotations, ): manifest = json.load(manifest_data) @@ -989,16 +991,8 @@ def _fake_get_annotations(*args, **kwargs): mock_cvat_api.get_job_annotations.side_effect = _fake_get_annotations mock_cvat_api.get_project_annotations.side_effect = _fake_get_annotations - def _fake_postprocess_annotations( - escrow_address, - chain_id, - annotations, - merged_annotation, - *, - manifest, - project_images, - ): - merged_annotation.file = io.BytesIO() + def _fake_postprocess_annotations(self, annotations): + self._merged_annotation_file.file = io.BytesIO() mock_postprocess_annotations.side_effect = _fake_postprocess_annotations From eaff5aba8b81a82f8c8563c86cd8dcef6a35322e Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Mon, 6 Jul 2026 19:30:52 +0300 Subject: [PATCH 19/53] Add audio transcription export --- .../core/tasks/audio_transcription/meta.py | 39 ++++- .../exchange-oracle/src/cvat/api_calls.py | 20 ++- .../src/handlers/job_completion/factory.py | 13 +- .../src/handlers/job_completion/handlers.py | 6 +- .../job_completion/validators/audio.py | 5 +- .../builders/audio/transcription.py | 34 +--- .../src/handlers/job_creation/factory.py | 2 +- .../src/handlers/job_export/downloading.py | 10 +- .../handlers/job_export/exporters/audio.py | 147 ++++++++++++++++-- .../src/handlers/job_export/exporters/base.py | 23 +-- .../handlers/job_export/exporters/vision.py | 36 ++--- .../src/handlers/job_export/factory.py | 17 +- .../src/handlers/job_export/handlers.py | 10 +- 13 files changed, 253 insertions(+), 109 deletions(-) diff --git a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py index c4c5b18b23..c77df2c7c4 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py +++ b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py @@ -7,16 +7,49 @@ from __future__ import annotations +import time +from datetime import timedelta from enum import Enum -from typing import TYPE_CHECKING from attrs import Factory, frozen from datumaro.util import dump_json, parse_json from src.utils.enums import BetterEnumMeta -if TYPE_CHECKING: - from datetime import timedelta +CVAT_EXPORT_FORMAT = "Generic TSV 1.0" + + +def parse_time(value: str) -> timedelta: + """Parse an ``H:MM:SS(.ffffff)`` timestamp (as used in the region/GT TSVs).""" + for fmt_string in ("%H:%M:%S.%f", "%H:%M:%S"): + try: + parsed_time = time.strptime(value, fmt_string) + if fmt_string.endswith(".%f"): + # time.strptime drops sub-second precision, so recover it from the raw string. + frac = value.split(".", maxsplit=1)[-1] + microseconds = int(frac.ljust(6, "0")[:6]) + else: + microseconds = 0 + except ValueError: + continue + else: + return timedelta( + hours=parsed_time.tm_hour, + minutes=parsed_time.tm_min, + seconds=parsed_time.tm_sec, + microseconds=microseconds, + ) + + raise ValueError(f"Failed to parse timestamp '{value}'") + + +def format_time(value: timedelta) -> str: + """Render a timedelta as an ``H:MM:SS.ffffff`` timestamp.""" + total = value.total_seconds() + h = int(total // 3600) + m = int((total % 3600) // 60) + s = total - h * 3600 - m * 60 + return f"{h}:{m:02d}:{s:09.6f}" @frozen(kw_only=True) diff --git a/packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py b/packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py index bfacd03134..67252af867 100644 --- a/packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py +++ b/packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py @@ -26,6 +26,9 @@ _NOTSET = object() +_PLAIN_FILE_EXPORT_FORMATS = frozenset({"Generic TSV 1.0"}) +"Export formats that return a plain file instead of a zip archive" + class CVATException(Exception): """Indicates that CVAT API returned unexpected response""" @@ -92,7 +95,6 @@ def _get_annotations( *, attempt_interval: int = 5, timeout: int | None = _NOTSET, - expect_zip: bool = True, ) -> io.RawIOBase: """ Downloads annotations. @@ -137,7 +139,10 @@ def _get_annotations( response = api_client.rest_client.GET(request_info.result_url, headers=headers) file_buffer = io.BytesIO(response.data) - if expect_zip: + + # Validate the data received. Most formats return a zip archive, but some return contents + # directly. + if request_info.operation.format not in _PLAIN_FILE_EXPORT_FORMATS: assert zipfile.is_zipfile(file_buffer) file_buffer.seek(0) @@ -528,26 +533,19 @@ def request_job_annotations(cvat_id: int, format_name: str) -> str: raise -def get_job_annotations( - request_id: str, *, timeout: int | None = _NOTSET, expect_zip: bool = True -) -> io.RawIOBase: +def get_job_annotations(request_id: str, *, timeout: int | None = _NOTSET) -> io.RawIOBase: """ Downloads annotations. The dataset preparation can take some time (e.g. 10 min), so it must be used like this: request_id = request_job_annotations(...): get_job_annotations(request_id, ...) - - Some export formats (e.g. "Generic TSV 1.0") return a plain file instead of a zip archive; - pass ``expect_zip=False`` for those. """ logger = logging.getLogger("app") with get_api_client() as api_client: try: - return _get_annotations( - api_client, request_id=request_id, timeout=timeout, expect_zip=expect_zip - ) + return _get_annotations(api_client, request_id=request_id, timeout=timeout) except exceptions.ApiException as e: logger.exception(f"Exception when calling JobsApi.retrieve_annotations: {e}\n") raise diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/factory.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/factory.py index 7725248a78..f11c00dd27 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/factory.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/factory.py @@ -2,8 +2,7 @@ from typing import TYPE_CHECKING -from src.chain.escrow import get_escrow_manifest -from src.core.manifest import get_manifest_task_type, parse_manifest +from src.core.manifest import ManifestBase, get_manifest_task_type from src.core.tasks import TaskTypes from src.handlers.job_completion.validators.audio import AudioTranscriptionJobValidator from src.handlers.job_completion.validators.base import JobValidator @@ -17,17 +16,17 @@ def create_validator( + manifest: ManifestBase, escrow_address: str, chain_id: int, session: Session, escrow_projects: Sequence[Project], ) -> JobValidator: - manifest = parse_manifest(get_escrow_manifest(chain_id, escrow_address)) task_type = get_manifest_task_type(manifest) match task_type: case TaskTypes.audio_transcription: - validator_cls = AudioTranscriptionJobValidator + validator_type = AudioTranscriptionJobValidator case ( TaskTypes.image_label_binary | TaskTypes.image_boxes @@ -36,8 +35,8 @@ def create_validator( | TaskTypes.image_boxes_from_points | TaskTypes.image_skeletons_from_boxes ): - validator_cls = JobValidator + validator_type = JobValidator case _: - raise Exception(f"Unsupported task type {task_type}") + raise NotADirectoryError(f"Unsupported task type '{task_type}'") - return validator_cls(escrow_address, chain_id, session, escrow_projects) + return validator_type(escrow_address, chain_id, session, escrow_projects) diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/handlers.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/handlers.py index e0cff6864f..452d0b97bd 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/handlers.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/handlers.py @@ -3,8 +3,9 @@ from typing import TYPE_CHECKING import src.services.cvat as cvat_service -from src.chain.escrow import validate_escrow +from src.chain.escrow import get_escrow_manifest, validate_escrow from src.core.config import CronConfig +from src.core.manifest import parse_manifest from src.core.types import EscrowValidationStatuses from src.db import SessionLocal from src.db.utils import ForUpdateParams @@ -34,7 +35,10 @@ def _handle_escrow_validation( if not escrow_projects: raise AssertionError(f"Can't find projects for escrow '{escrow_address}'") + manifest = parse_manifest(get_escrow_manifest(chain_id, escrow_address)) + with create_validator( + manifest=manifest, escrow_address=escrow_address, chain_id=chain_id, session=session, diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/validators/audio.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/validators/audio.py index 25e4cac658..f087ffa589 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/validators/audio.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/validators/audio.py @@ -3,6 +3,7 @@ from typing import TYPE_CHECKING import src.services.cvat as cvat_service +from src.core.tasks.audio_transcription.meta import CVAT_EXPORT_FORMAT from src.handlers.job_completion.validators.base import JobValidator from src.handlers.job_export.downloading import download_job_annotations from src.handlers.job_export.results import ( @@ -14,8 +15,6 @@ if TYPE_CHECKING: from src.models.cvat import Job -CVAT_AUDIO_EXPORT_FORMAT = "Generic TSV 1.0" - # subdir (in the escrow results dir) for the per-assignment transcription TSVs ASSIGNMENTS_DIR = "assignments" @@ -33,7 +32,7 @@ def _save_annotation_results(self) -> None: f"(escrow_address={self.escrow_address})" ) cvat_job_annotations = download_job_annotations( - self.logger, CVAT_AUDIO_EXPORT_FORMAT, cvat_jobs, expect_zip=False + self.logger, CVAT_EXPORT_FORMAT, cvat_jobs ) result_files: list[FileDescriptor] = [ diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py index 753c859d1a..ac55a1cca8 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py @@ -3,10 +3,8 @@ import csv import io import random -import time import uuid from collections import Counter -from datetime import timedelta from math import ceil from pathlib import Path, PurePosixPath from tempfile import TemporaryDirectory @@ -26,6 +24,7 @@ RegionKind, TaskMetaLayout, TaskMetaSerializer, + parse_time, ) from src.core.tasks.audio_transcription.spec import parse_audio_manifest from src.core.types import TaskStatuses @@ -610,29 +609,6 @@ def build(self) -> None: # --------------------------------------------------------------------------- # -def _parse_time(v: str) -> timedelta: - for fmt_string in ("%H:%M:%S.%f", "%H:%M:%S"): - try: - parsed_time = time.strptime(v, fmt_string) - if fmt_string.endswith(".%f"): - # time.strptime drops sub-second precision, so recover it from the raw string. - frac = v.split(".", maxsplit=1)[-1] - microseconds = int(frac.ljust(6, "0")[:6]) - else: - microseconds = 0 - except ValueError: - continue - else: - return timedelta( - hours=parsed_time.tm_hour, - minutes=parsed_time.tm_min, - seconds=parsed_time.tm_sec, - microseconds=microseconds, - ) - - raise ValueError(f"Failed to parse timestamp '{v}'") - - def _read_tsv(data: bytes) -> list[dict[str, str]]: reader = csv.DictReader(io.StringIO(data.decode("utf-8")), delimiter="\t") return list(reader) @@ -646,8 +622,8 @@ def _parse_regions_tsv(data: bytes) -> dict[str, list[InputRegion]]: InputRegion( filename=filename, row_idx=row_idx, - start=_parse_time(row["start"]), - stop=_parse_time(row["stop"]), + start=parse_time(row["start"]), + stop=parse_time(row["stop"]), label=(row.get("label") or "").strip() or None, ) ) @@ -662,8 +638,8 @@ def _parse_gt_tsv(data: bytes) -> dict[str, list[InputGtRegion]]: InputGtRegion( filename=filename, row_idx=row_idx, - start=_parse_time(row["start"]), - stop=_parse_time(row["stop"]), + start=parse_time(row["start"]), + stop=parse_time(row["stop"]), label=(row.get("label") or "").strip() or None, text=(row.get("text") or "").strip(), ) diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/factory.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/factory.py index 7d9f362921..e56c4bb296 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/factory.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/factory.py @@ -42,7 +42,7 @@ def create_task(escrow_address: str, chain_id: int) -> None: case TaskTypes.audio_transcription: builder_type = AudioTranscriptionTaskBuilder case _: - raise Exception(f"Unsupported task type {task_type}") + raise NotImplementedError(f"Unsupported task type '{task_type}'") with builder_type(manifest, escrow_address, chain_id) as task_builder: task_builder.set_logger(logger) diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/downloading.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/downloading.py index 61f96b68ed..bf8674aec7 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/downloading.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/downloading.py @@ -73,11 +73,7 @@ def _download_export(): def download_job_annotations( - logger: logging.Logger, - annotation_format: str, - jobs: Sequence[Job], - *, - expect_zip: bool = True, + logger: logging.Logger, annotation_format: str, jobs: Sequence[Job] ) -> dict[int, FileDescriptor]: # Collect raw annotations from CVAT, validate and convert them # into a recording oracle suitable format @@ -102,9 +98,7 @@ def _request_export(job: Job): job_annotations_file = download_with_retries( logger, - download_callback=partial( - cvat_api.get_job_annotations, request_id=request_id, expect_zip=expect_zip - ), + download_callback=partial(cvat_api.get_job_annotations, request_id=request_id), retry_callback=partial(_request_export, job), ) diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/audio.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/audio.py index 622c3f006e..761e6f117e 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/audio.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/audio.py @@ -1,23 +1,152 @@ from __future__ import annotations +import csv +import io +from datetime import timedelta from typing import TYPE_CHECKING +import src.services.cvat as cvat_service +from src.core.config import Config +from src.core.storage import compose_data_bucket_filename +from src.core.tasks.audio_transcription.meta import ( + CVAT_EXPORT_FORMAT, + TaskMetaLayout, + TaskMetaSerializer, + format_time, + parse_time, +) +from src.core.tasks.audio_transcription.spec import parse_audio_manifest +from src.handlers.job_export.downloading import download_job_annotations from src.handlers.job_export.exporters.base import JobExporter +from src.handlers.job_export.results import ( + FileDescriptor, + prepare_annotation_metafile, + upload_escrow_results, +) +from src.services.cloud import make_client as make_cloud_client +from src.services.cloud.utils import BucketAccessInfo if TYPE_CHECKING: - from collections.abc import Sequence + from src.core.tasks.audio_transcription.meta import Assignment, PlacedRegion - from sqlalchemy.orm import Session - from src.models.cvat import Project +# merged resulting annotations file (in the escrow results dir), GT-annotations structure +RESULTING_ANNOTATIONS_FILE = "annotations.tsv" + +_LEADING_COLUMNS = ["id", "input_region_ids", "filename", "start", "stop", "label"] class AudioTranscriptionJobExporter(JobExporter): - """Export flow for audio_transcription escrows. + def export(self) -> None: + assert self.escrow_projects # unused, but must hold the lock + + jobs = cvat_service.get_jobs_by_escrow_address( + self.session, self.escrow_address, self.chain_id + ) + + spec = parse_audio_manifest(self.manifest) + attr_names = [ + spec.details.transcription_attr_name, + *(a.name for a in spec.shared_attributes), + ] + + assignments_by_clip = self._load_assignments() + + # TODO: use project export when CVAT supports it to speedup downloading + self.logger.debug(f"Downloading annotations for the escrow ({self.escrow_address=})") + job_annotations = download_job_annotations(self.logger, CVAT_EXPORT_FORMAT, jobs) + + rows: list[dict] = [] + for job in jobs: + rows.extend( + self._annotation_rows( + job_annotations[job.cvat_id], assignments_by_clip, attr_names + ) + ) + rows.sort(key=lambda r: (r["filename"], r["_start_s"])) + merged = self._render_tsv([*_LEADING_COLUMNS, *attr_names], rows) + + self.logger.debug(f"Uploading merged annotations for the escrow ({self.escrow_address=})") + upload_escrow_results( + files=[ + FileDescriptor(filename=RESULTING_ANNOTATIONS_FILE, file=io.BytesIO(merged)), + prepare_annotation_metafile(jobs=jobs), + ], + chain_id=self.chain_id, + escrow_address=self.escrow_address, + ) + + self._emit_escrow_recorded() + self.logger.info(f"The escrow ({self.escrow_address=}) is completed, annotations merged") + + def _load_assignments(self) -> dict[str, Assignment]: + storage_client = make_cloud_client(BucketAccessInfo.parse_obj(Config.storage_config)) + assignments = TaskMetaSerializer().parse_assignments( + storage_client.download_file( + compose_data_bucket_filename( + self.escrow_address, self.chain_id, TaskMetaLayout().ASSIGNMENTS_FILENAME + ) + ) + ) + return {a.clip_filename: a for a in assignments} + + def _annotation_rows( + self, + annotations: FileDescriptor, + assignments_by_clip: dict[str, Assignment], + attr_names: list[str], + ) -> list[dict]: + annotations.file.seek(0) + text_stream = io.TextIOWrapper(annotations.file, encoding="utf-8") + reader = csv.DictReader(text_stream, delimiter="\t") + + rows: list[dict] = [] + for interval in reader: + clip = interval["filename"].rsplit("/", 1)[-1] + assignment = assignments_by_clip.get(clip) + if assignment is None: + raise AssertionError(f"No assignment found for clip '{clip}'") + + start_s = parse_time(interval["start"]).total_seconds() + stop_s = parse_time(interval["stop"]).total_seconds() + + placed = self._placed_at(assignment, start_s) + if placed is None or placed.kind.value == "gt": + # outside any region, or a honeypot -> dropped + continue + + offset = placed.file_start - placed.clip_start + rows.append( + { + "filename": placed.source_filename, + "start": format_time(timedelta(seconds=start_s + offset)), + "stop": format_time(timedelta(seconds=stop_s + offset)), + "_start_s": start_s + offset, + "label": interval.get("label", ""), + "input_region_ids": ",".join(i.split(":")[-1] for i in placed.input_ids), + **{attr: interval.get(attr, "") or "" for attr in attr_names}, + } + ) + return rows - FUTURE-TODO: read the interval (transcription) annotations from the annotation jobs, map placed - regions back to source via the persisted assignments/regions meta, and score against GT (WER). - """ + @staticmethod + def _placed_at(assignment: Assignment, clip_time_s: float) -> PlacedRegion | None: + return next( + ( + p + for p in assignment.placed + if p.clip_start <= clip_time_s < p.clip_stop + ), + None, + ) - def export(self, session: Session, escrow_projects: Sequence[Project]) -> None: - raise NotImplementedError("audio_transcription export is not implemented yet") + @staticmethod + def _render_tsv(columns: list[str], rows: list[dict]) -> bytes: + buffer = io.StringIO() + writer = csv.DictWriter( + buffer, fieldnames=columns, delimiter="\t", extrasaction="ignore" + ) + writer.writeheader() + for global_id, row in enumerate(rows): + writer.writerow({**row, "id": global_id}) + return buffer.getvalue().encode("utf-8") diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/base.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/base.py index fc69656632..43fb434b8d 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/base.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/base.py @@ -22,17 +22,20 @@ class JobExporter(metaclass=ABCMeta): - """ - Base for task-specific escrow annotation exporters. - - Subclasses must implement :meth:`export` - """ - - def __init__(self, manifest: ManifestBase, escrow_address: str, chain_id: int) -> None: + def __init__( + self, + manifest: ManifestBase, + escrow_address: str, + chain_id: int, + session: Session, + escrow_projects: Sequence[Project], + ) -> None: self.exit_stack = ExitStack() self.manifest = manifest self.escrow_address = escrow_address self.chain_id = chain_id + self.session = session + self.escrow_projects = escrow_projects self.logger: Logger = NullLogger() @@ -50,16 +53,16 @@ def set_logger(self, logger: Logger): return self @abstractmethod - def export(self, session: Session, escrow_projects: Sequence[Project]) -> None: ... + def export(self) -> None: ... def _upload_results(self, files: Sequence[FileDescriptor]) -> None: upload_escrow_results( files=files, chain_id=self.chain_id, escrow_address=self.escrow_address ) - def _emit_escrow_recorded(self, session: Session) -> None: + def _emit_escrow_recorded(self) -> None: oracle_db_service.outbox.create_webhook( - session, + self.session, escrow_address=self.escrow_address, chain_id=self.chain_id, type=OracleWebhookTypes.recording_oracle, diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/vision.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/vision.py index 646ba6a114..f3706b8693 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/vision.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/vision.py @@ -40,9 +40,8 @@ from collections.abc import Sequence from datumaro.components.dataset import Dataset - from sqlalchemy.orm import Session - from src.models.cvat import Image, Project + from src.models.cvat import Image CVAT_EXPORT_FORMAT_MAPPING = { TaskTypes.image_label_binary: "CVAT for images 1.1", @@ -63,17 +62,17 @@ class ImageJobExporter(JobExporter): """Base image exporter: download the single project + per-job annotations, run the datumaro conversion pipeline, upload the results, and notify the recording oracle.""" - def export(self, session: Session, escrow_projects: Sequence[Project]) -> None: + def export(self) -> None: escrow_address = self.escrow_address chain_id = self.chain_id escrow_creation = cvat_service.get_escrow_creation_by_escrow_address( - session, escrow_address, chain_id, active=False + self.session, escrow_address, chain_id, active=False ) if not escrow_creation: raise AssertionError(f"Can't find escrow creation for escrow '{escrow_address}'") - jobs = cvat_service.get_jobs_by_escrow_address(session, escrow_address, chain_id) + jobs = cvat_service.get_jobs_by_escrow_address(self.session, escrow_address, chain_id) if len(jobs) != escrow_creation.total_jobs: raise AssertionError( f"Unexpected number of jobs fetched for escrow " @@ -88,7 +87,7 @@ def export(self, session: Session, escrow_projects: Sequence[Project]) -> None: job_annotations = download_job_annotations(self.logger, annotation_format, jobs) project_annotations_file, project_images = self._collect_project_annotations( - session, escrow_projects, annotation_format + annotation_format ) resulting_annotations_file_desc = FileDescriptor( @@ -108,7 +107,7 @@ def export(self, session: Session, escrow_projects: Sequence[Project]) -> None: self._upload_results(files=(*annotations, prepare_annotation_metafile(jobs=jobs))) - self._emit_escrow_recorded(session) + self._emit_escrow_recorded() self.logger.info( f"The escrow ({escrow_address=}) is completed, " @@ -136,24 +135,21 @@ def _output_format(self) -> str: # -- project annotations ----------------------------------------------- # def _collect_project_annotations( - self, - session: Session, - escrow_projects: Sequence[Project], - annotation_format: str, + self, annotation_format: str ) -> tuple[io.RawIOBase | None, list[Image] | None]: # escrows with simple task types must have only one project try: - (project,) = escrow_projects + (project,) = self.escrow_projects except ValueError: raise NotImplementedError( f"{self._task_type} is expected to have exactly one project," - f" not {len(escrow_projects)}" + f" not {len(self.escrow_projects)}" ) project_annotations_file = download_project_annotations( self.logger, annotation_format, project.cvat_id ) - project_images = cvat_service.get_project_images(session, project.cvat_id) + project_images = cvat_service.get_project_images(self.session, project.cvat_id) return project_annotations_file, project_images # -- datumaro conversion pipeline -------------------------------------- # @@ -361,17 +357,13 @@ def _process_merged_dataset(self, input_dataset: Dataset) -> Dataset: class SkeletonsJobExporter(ImageJobExporter): - """image_skeletons_from_boxes: annotations are accumulated into a merged dataset from the - per-job files here, so there is no single project export to download.""" + # Reconstruct skeletons from per-point annotation jobs def _collect_project_annotations( - self, - session: Session, - escrow_projects: Sequence[Project], - annotation_format: str, + self, annotation_format: str ) -> tuple[io.RawIOBase | None, list[Image] | None]: - assert escrow_projects # unused, but must hold the lock - del session, annotation_format # the jobs are self-merged, no project export + assert self.escrow_projects # unused, but must hold the lock + del annotation_format # the jobs are self-merged, no project export return None, None def _prepare(self) -> None: diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/factory.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/factory.py index 0e71f63c4d..7c5da50ffc 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/factory.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/factory.py @@ -15,11 +15,22 @@ ) if TYPE_CHECKING: + from collections.abc import Sequence + + from sqlalchemy.orm import Session + from src.core.manifest import ManifestBase from src.handlers.job_export.exporters.base import JobExporter + from src.models.cvat import Project -def create_exporter(manifest: ManifestBase, escrow_address: str, chain_id: int) -> JobExporter: +def create_exporter( + manifest: ManifestBase, + escrow_address: str, + chain_id: int, + session: Session, + escrow_projects: Sequence[Project], +) -> JobExporter: task_type = get_manifest_task_type(manifest) match task_type: @@ -38,6 +49,6 @@ def create_exporter(manifest: ManifestBase, escrow_address: str, chain_id: int) case TaskTypes.audio_transcription: exporter_type = AudioTranscriptionJobExporter case _: - raise Exception(f"Unsupported task type {task_type}") + raise NotImplementedError(f"Unsupported task type '{task_type}'") - return exporter_type(manifest, escrow_address, chain_id) + return exporter_type(manifest, escrow_address, chain_id, session, escrow_projects) diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/handlers.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/handlers.py index 6fcb3dc9e7..e45054363f 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/handlers.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/handlers.py @@ -28,6 +28,12 @@ def handle_escrow_export( manifest = parse_manifest(get_escrow_manifest(chain_id, escrow_address)) - with create_exporter(manifest, escrow_address, chain_id) as exporter: + with create_exporter( + manifest=manifest, + escrow_address=escrow_address, + chain_id=chain_id, + session=session, + escrow_projects=escrow_projects, + ) as exporter: exporter.set_logger(logger) - exporter.export(session, escrow_projects) + exporter.export() From a0ce8cfb237ed35488668eb024f59c605275ce9a Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Tue, 7 Jul 2026 13:42:21 +0300 Subject: [PATCH 20/53] Fix typo --- packages/examples/cvat/exchange-oracle/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/examples/cvat/exchange-oracle/README.md b/packages/examples/cvat/exchange-oracle/README.md index e618389c7d..ba04d13e11 100644 --- a/packages/examples/cvat/exchange-oracle/README.md +++ b/packages/examples/cvat/exchange-oracle/README.md @@ -36,7 +36,7 @@ When running service from `./bin/start_debug.sh` (`src/entrypoints/debug.py`), s - When webhook signature is required, `{oracle_name}:unique_string` can be used - You can upload manifest.json to minio `manifests` bucket and use its filename as an escrow_address -### Environemt +### Environment Env example file: [.env.example](https://github.com/humanprotocol/human-protocol/blob/feat/cvat/exchange-oracle/packages/examples/cvat/exchange-oracle/src/.env.example) Config: [config file](https://github.com/humanprotocol/human-protocol/blob/feat/cvat/exchange-oracle/packages/examples/cvat/exchange-oracle/src/config.py) From e3b449fc8982ca77bcfb51814f5a4a2338cba0bf Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Tue, 7 Jul 2026 14:32:14 +0300 Subject: [PATCH 21/53] Sync core changes in EO and RO --- .../src/services/cloud/types.py | 8 +- .../cvat/recording-oracle/pyproject.toml | 1 + .../src/core/manifest/__init__.py | 38 ++++ .../src/core/manifest/base.py | 6 + .../core/{manifest.py => manifest/shared.py} | 84 +------ .../recording-oracle/src/core/manifest/v1.py | 83 +++++++ .../recording-oracle/src/core/manifest/v2.py | 160 +++++++++++++ .../src/core/tasks/__init__.py | 7 + .../tasks/audio_transcription/__init__.py | 0 .../core/tasks/audio_transcription/meta.py | 215 ++++++++++++++++++ .../core/tasks/audio_transcription/spec.py | 136 +++++++++++ .../src/core/tasks/cvat_formats.py | 22 ++ .../recording-oracle/src/core/tasks/errors.py | 2 + .../recording-oracle/src/core/tasks/types.py | 13 ++ .../cvat/recording-oracle/src/core/types.py | 9 - .../src/handlers/completion.py | 4 +- .../handlers/process_intermediate_results.py | 48 ++-- .../src/handlers/validation.py | 19 +- .../src/services/cloud/types.py | 8 +- .../recording-oracle/tests/utils/helpers.py | 4 +- 20 files changed, 729 insertions(+), 138 deletions(-) create mode 100644 packages/examples/cvat/recording-oracle/src/core/manifest/__init__.py create mode 100644 packages/examples/cvat/recording-oracle/src/core/manifest/base.py rename packages/examples/cvat/recording-oracle/src/core/{manifest.py => manifest/shared.py} (52%) create mode 100644 packages/examples/cvat/recording-oracle/src/core/manifest/v1.py create mode 100644 packages/examples/cvat/recording-oracle/src/core/manifest/v2.py create mode 100644 packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/__init__.py create mode 100644 packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/meta.py create mode 100644 packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/spec.py create mode 100644 packages/examples/cvat/recording-oracle/src/core/tasks/cvat_formats.py create mode 100644 packages/examples/cvat/recording-oracle/src/core/tasks/errors.py create mode 100644 packages/examples/cvat/recording-oracle/src/core/tasks/types.py diff --git a/packages/examples/cvat/exchange-oracle/src/services/cloud/types.py b/packages/examples/cvat/exchange-oracle/src/services/cloud/types.py index d6c9c2994d..804032c6aa 100644 --- a/packages/examples/cvat/exchange-oracle/src/services/cloud/types.py +++ b/packages/examples/cvat/exchange-oracle/src/services/cloud/types.py @@ -11,7 +11,7 @@ from httpx import URL from src.core.config import Config, StorageConfig -from src.core.manifest import shared +from src.core.manifest.shared import BucketUrl, BucketUrlBase from src.services.cloud.gcs import DEFAULT_GCS_HOST from src.services.cloud.s3 import DEFAULT_S3_HOST from src.utils.enums import BetterEnumMeta @@ -174,14 +174,14 @@ def from_storage_config(cls, config: type[StorageConfig]) -> BucketAccessInfo: ) @classmethod - def from_bucket_url(cls, bucket_url: shared.BucketUrl) -> BucketAccessInfo: + def from_bucket_url(cls, bucket_url: BucketUrl) -> BucketAccessInfo: return cls._from_dict(bucket_url.model_dump()) @classmethod def parse_obj( - cls, data: str | type[StorageConfig] | shared.BucketUrl | pydantic.AnyUrl + cls, data: str | type[StorageConfig] | BucketUrl | pydantic.AnyUrl ) -> BucketAccessInfo: - if isinstance(data, shared.BucketUrlBase): + if isinstance(data, BucketUrlBase): return cls.from_bucket_url(data) elif isinstance(data, str): return cls.from_url(data) diff --git a/packages/examples/cvat/recording-oracle/pyproject.toml b/packages/examples/cvat/recording-oracle/pyproject.toml index b51dab8f05..e3df56302c 100644 --- a/packages/examples/cvat/recording-oracle/pyproject.toml +++ b/packages/examples/cvat/recording-oracle/pyproject.toml @@ -109,6 +109,7 @@ ignore = [ "PTH119", # `os.path.basename()` should be replaced by `Path.name` "PTH122", # `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, and `Path.parent` "PTH207", # Replace `glob` with `Path.glob` or `Path.rglob` + "RET505", # Unnecessary elif/else statements after/before raise/return. ] diff --git a/packages/examples/cvat/recording-oracle/src/core/manifest/__init__.py b/packages/examples/cvat/recording-oracle/src/core/manifest/__init__.py new file mode 100644 index 0000000000..5d91f29f36 --- /dev/null +++ b/packages/examples/cvat/recording-oracle/src/core/manifest/__init__.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from src.core.manifest import v1, v2 +from src.core.manifest.base import ManifestBase + +if TYPE_CHECKING: + from src.core.tasks import TaskTypes + +__all__ = [ + "ManifestBase", + "get_manifest_task_type", + "parse_manifest", + "v1", + "v2", +] + + +def parse_manifest(manifest: Any) -> ManifestBase: + if isinstance(manifest, dict): + version = manifest.get("version") + else: + version = getattr(manifest, "version", None) + + if version == 2: + return v2.JobManifest.model_validate(manifest) + else: + return v1.JobManifest.model_validate(manifest) + + +def get_manifest_task_type(manifest: ManifestBase) -> TaskTypes: + if isinstance(manifest, v1.JobManifest): + return manifest.annotation.type + if isinstance(manifest, v2.JobManifest): + return manifest.job_type + + raise NotImplementedError(f"Unknown manifest version '{manifest.version}'") diff --git a/packages/examples/cvat/recording-oracle/src/core/manifest/base.py b/packages/examples/cvat/recording-oracle/src/core/manifest/base.py new file mode 100644 index 0000000000..5c14e1bc11 --- /dev/null +++ b/packages/examples/cvat/recording-oracle/src/core/manifest/base.py @@ -0,0 +1,6 @@ +from typing import Protocol, runtime_checkable + + +@runtime_checkable +class ManifestBase(Protocol): + version: int diff --git a/packages/examples/cvat/recording-oracle/src/core/manifest.py b/packages/examples/cvat/recording-oracle/src/core/manifest/shared.py similarity index 52% rename from packages/examples/cvat/recording-oracle/src/core/manifest.py rename to packages/examples/cvat/recording-oracle/src/core/manifest/shared.py index ca19196555..089429cc94 100644 --- a/packages/examples/cvat/recording-oracle/src/core/manifest.py +++ b/packages/examples/cvat/recording-oracle/src/core/manifest/shared.py @@ -1,10 +1,8 @@ -from decimal import Decimal from enum import Enum from typing import Annotated, Any, Literal -from pydantic import AnyUrl, BaseModel, Field, model_validator +from pydantic import BaseModel, Field, model_validator -from src.core.types import TaskTypes from src.utils.enums import BetterEnumMeta @@ -34,20 +32,6 @@ class GcsBucketUrl(BucketUrlBase, BaseModel): BucketUrl = Annotated[AwsBucketUrl | GcsBucketUrl, Field(discriminator="provider")] -class DataInfo(BaseModel): - data_url: AnyUrl | BucketUrl - "Bucket URL, AWS S3 | GCS, virtual-hosted-style access" - # https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-bucket-intro.html - - points_url: AnyUrl | BucketUrl | None = None - "A path to an archive with a set of points in COCO Keypoints format, " - "which provides information about all objects on images" - - boxes_url: AnyUrl | BucketUrl | None = None - "A path to an archive with a set of boxes in COCO Instances format, " - "which provides information about all objects on images" - - class LabelTypes(str, Enum, metaclass=BetterEnumMeta): plain = "plain" skeleton = "skeleton" @@ -55,7 +39,7 @@ class LabelTypes(str, Enum, metaclass=BetterEnumMeta): class LabelInfoBase(BaseModel): name: str = Field(min_length=1) - # https://opencv.github.io/cvat/docs/api_sdk/sdk/reference/models/label/ + # https://docs.cvat.ai/docs/api_sdk/sdk/reference/models/label/ type: LabelTypes = LabelTypes.plain @@ -119,67 +103,3 @@ def validate_type(cls, values: dict[str, Any]) -> dict[str, Any]: LabelInfo = Annotated[PlainLabelInfo | SkeletonLabelInfo, Field(discriminator="type")] - - -class AnnotationInfo(BaseModel): - type: TaskTypes - - labels: list[LabelInfo] = Field(min_length=1) - "Label declarations with accepted annotation types" - - description: str = "" - "Brief task description" - - user_guide: str = "" - "User guide in markdown format" - - job_size: int = 10 - "Frames per job, validation frames are not included" - - @model_validator(mode="before") - @classmethod - def validate_label_type(cls, values: dict[str, Any]) -> dict[str, Any]: - if not isinstance(values, dict): - raise NotImplementedError - - default_label_type = LabelTypes.plain - if values["type"] == TaskTypes.image_skeletons_from_boxes: - default_label_type = LabelTypes.skeleton - - # Add default value for labels, if none provided. - # pydantic can't do this for tagged unions - try: - labels = values["labels"] - for label_info in labels: - label_info["type"] = label_info.get("type", default_label_type) - except KeyError: - pass - - return values - - -class ValidationInfo(BaseModel): - min_quality: float = Field(ge=0) - "Minimal accepted annotation accuracy" - - val_size: int = Field(default=2, gt=0) - "Validation frames per job" - - gt_url: AnyUrl | BucketUrl - "URL to the archive with Ground Truth annotations, the format is COCO keypoints" - - -class TaskManifest(BaseModel): - data: DataInfo - annotation: AnnotationInfo - validation: ValidationInfo - - job_bounty: Decimal = Field(ge=0) - "Assignment bounty, a decimal value in HMT" - - qualifications: list[str] = Field(default_factory=list) - "A list of annotator qualifications required for participation" - - -def parse_manifest(manifest: Any) -> TaskManifest: - return TaskManifest.model_validate(manifest) diff --git a/packages/examples/cvat/recording-oracle/src/core/manifest/v1.py b/packages/examples/cvat/recording-oracle/src/core/manifest/v1.py new file mode 100644 index 0000000000..1b774ebd5f --- /dev/null +++ b/packages/examples/cvat/recording-oracle/src/core/manifest/v1.py @@ -0,0 +1,83 @@ +from decimal import Decimal +from typing import Any, Literal + +from pydantic import AnyUrl, BaseModel, Field, model_validator + +from src.core.manifest.shared import BucketUrl, LabelInfo, LabelTypes +from src.core.tasks import TaskTypes + + +class DataInfo(BaseModel): + data_url: AnyUrl | BucketUrl + "Bucket URL, AWS S3 | GCS, virtual-hosted-style access" + # https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-bucket-intro.html + + points_url: AnyUrl | BucketUrl | None = None + "A path to an archive with a set of points in COCO Keypoints format, " + "which provides information about all objects on images" + + boxes_url: AnyUrl | BucketUrl | None = None + "A path to an archive with a set of boxes in COCO Instances format, " + "which provides information about all objects on images" + + +class AnnotationInfo(BaseModel): + type: TaskTypes + + labels: list[LabelInfo] = Field(min_length=1) + "Label declarations with accepted annotation types" + + description: str = "" + "Brief task description" + + user_guide: str = "" + "User guide in markdown format" + + job_size: int = 10 + "Frames per job, validation frames are not included" + + qualifications: list[str] = Field(default_factory=list) + "A list of annotator qualifications required for participation" + + @model_validator(mode="before") + @classmethod + def validate_label_type(cls, values: dict[str, Any]) -> dict[str, Any]: + if not isinstance(values, dict): + raise NotImplementedError + + default_label_type = LabelTypes.plain + if values["type"] == TaskTypes.image_skeletons_from_boxes: + default_label_type = LabelTypes.skeleton + + # Add default value for labels, if none provided. + # pydantic can't do this for tagged unions + try: + labels = values["labels"] + for label_info in labels: + label_info["type"] = label_info.get("type", default_label_type) + except KeyError: + pass + + return values + + +class ValidationInfo(BaseModel): + min_quality: float = Field(ge=0) + "Minimal accepted annotation accuracy" + + val_size: int = Field(default=2, gt=0) + "Validation frames per job" + + gt_url: AnyUrl | BucketUrl + "URL to the archive with Ground Truth annotations, the format is COCO keypoints" + + +class JobManifest(BaseModel): + version: Literal[1] = 1 + + data: DataInfo + annotation: AnnotationInfo + validation: ValidationInfo + + job_bounty: Decimal = Field(ge=0) + "Assignment bounty, a decimal value in HMT" diff --git a/packages/examples/cvat/recording-oracle/src/core/manifest/v2.py b/packages/examples/cvat/recording-oracle/src/core/manifest/v2.py new file mode 100644 index 0000000000..76ac94fade --- /dev/null +++ b/packages/examples/cvat/recording-oracle/src/core/manifest/v2.py @@ -0,0 +1,160 @@ +from enum import Enum +from typing import Any, Literal + +from pydantic import AnyUrl, BaseModel, ConfigDict, Field, model_validator + +from src.core.manifest.shared import BucketUrl, LabelInfo, LabelTypes +from src.core.tasks import TaskTypes +from src.utils.enums import BetterEnumMeta + + +class DataInfo(BaseModel): + media_url: AnyUrl | BucketUrl + "Bucket URL, AWS S3 | GCS, virtual-hosted-style access" + + gt_url: AnyUrl | BucketUrl + "URL to the archive with Ground Truth annotations" + + points_url: AnyUrl | BucketUrl | None = None + """ + A path to a file or an archive with a set of points in COCO Keypoints format. + Applicable for: image_boxes_from_points + """ + + boxes_url: AnyUrl | BucketUrl | None = None + """ + A path to a file or an archive with a set of boxes in COCO Instances format. + Applicable for: image_skeletons_from_boxes + """ + + regions_url: AnyUrl | BucketUrl | None = None + """ + A path to a file or an archive with audio regions in the CVAT Generic TSV format. + Applicable for: audio_transcription + """ + + +class TargetMetrics(str, Enum, metaclass=BetterEnumMeta): + accuracy = "accuracy" + wer = "wer" + cer = "cer" + + +class ValidationInfo(BaseModel): + target_score: float = Field(ge=0) + "Required annotation score. Can be minimal or maximal depending on the metric used" + + target_metric: TargetMetrics = TargetMetrics.accuracy + "Metric used to score annotations against the target" + + +class MinComposition(BaseModel): + gt: int + "Minimal number of Ground Truth samples per composition" + + ds: int + "Minimal number of dataset samples per composition" + + +class DetailsInfoBase(BaseModel): + model_config = ConfigDict(extra="forbid") + + max_time: int | None = None + "Maximum assignment time, seconds" + + +class ImageJobDetails(DetailsInfoBase): + job_size: int | None = None + "Frames per job, validation frames are not included" + + val_size: int | None = None + "Validation frames per job" + + +class AudioJobDetails(DetailsInfoBase): + normalizer: str | None = "basic" + "Text normalizer for audio transcription" + + max_segment_duration: int | None = None + "Maximum audio segment duration, seconds" + + min_composition: MinComposition | None = None + "Minimal composition of a job" + + validation_overhead: int | None = None + "Extra validation samples added on top of the required amount" + + +DetailsInfo = ImageJobDetails | AudioJobDetails + + +class SharedAttribute(BaseModel): + name: str = Field(min_length=1) + type: str + values: list[str] = Field(default_factory=list) + default_value: str = "" + + +class AnnotationInfo(BaseModel): + description: str = "" + "Brief task description" + + user_guide_url: str = "" + "URL to the user guide in markdown format" + + labels: list[LabelInfo] = Field(min_length=1) + "Label declarations with accepted annotation types" + + shared_attributes: list[SharedAttribute] = Field(default_factory=list) + "Shared annotation attributes" + + validation: ValidationInfo + + qualifications: list[str] = Field(default_factory=list) + "A list of annotator qualifications required for participation" + + details: ImageJobDetails | AudioJobDetails | None = None + "Task-specific details" + + +class JobManifest(BaseModel): + version: Literal[2] + + job_type: TaskTypes + data: DataInfo + annotation: AnnotationInfo + + @model_validator(mode="before") + @classmethod + def parse_task_specific_params(cls, values: dict[str, Any]) -> dict[str, Any]: + if not isinstance(values, dict): + raise NotImplementedError + + job_type = values.get("job_type") + + annotation = values.get("annotation") + if not isinstance(annotation, dict): + raise NotImplementedError + + # Default label types (pydantic can't do this for tagged unions). + default_label_type = ( + LabelTypes.skeleton + if job_type == TaskTypes.image_skeletons_from_boxes + else LabelTypes.plain + ) + for label_info in annotation.get("labels", []): + if isinstance(label_info, dict): + label_info.setdefault("type", default_label_type) + + if details := annotation.get("details"): + if not isinstance(details, dict): + raise NotImplementedError + + details_cls = ( + AudioJobDetails + if job_type == TaskTypes.audio_transcription + else ImageJobDetails + ) + annotation["details"] = details_cls.model_validate(details) + + return values diff --git a/packages/examples/cvat/recording-oracle/src/core/tasks/__init__.py b/packages/examples/cvat/recording-oracle/src/core/tasks/__init__.py index e69de29bb2..3f24f51e5c 100644 --- a/packages/examples/cvat/recording-oracle/src/core/tasks/__init__.py +++ b/packages/examples/cvat/recording-oracle/src/core/tasks/__init__.py @@ -0,0 +1,7 @@ +from src.core.tasks import errors +from src.core.tasks.types import TaskTypes + +__all__ = [ + "TaskTypes", + "errors", +] diff --git a/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/__init__.py b/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/meta.py b/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/meta.py new file mode 100644 index 0000000000..c77df2c7c4 --- /dev/null +++ b/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/meta.py @@ -0,0 +1,215 @@ +"""Domain types and meta persistence for audio transcription tasks. + +Regions are mixed across input files into assignments, so every region carries its own source +provenance. The persisted mappings (regions + assignments) let each region be located back in its +source media after mixing. +""" + +from __future__ import annotations + +import time +from datetime import timedelta +from enum import Enum + +from attrs import Factory, frozen +from datumaro.util import dump_json, parse_json + +from src.utils.enums import BetterEnumMeta + +CVAT_EXPORT_FORMAT = "Generic TSV 1.0" + + +def parse_time(value: str) -> timedelta: + """Parse an ``H:MM:SS(.ffffff)`` timestamp (as used in the region/GT TSVs).""" + for fmt_string in ("%H:%M:%S.%f", "%H:%M:%S"): + try: + parsed_time = time.strptime(value, fmt_string) + if fmt_string.endswith(".%f"): + # time.strptime drops sub-second precision, so recover it from the raw string. + frac = value.split(".", maxsplit=1)[-1] + microseconds = int(frac.ljust(6, "0")[:6]) + else: + microseconds = 0 + except ValueError: + continue + else: + return timedelta( + hours=parsed_time.tm_hour, + minutes=parsed_time.tm_min, + seconds=parsed_time.tm_sec, + microseconds=microseconds, + ) + + raise ValueError(f"Failed to parse timestamp '{value}'") + + +def format_time(value: timedelta) -> str: + """Render a timedelta as an ``H:MM:SS.ffffff`` timestamp.""" + total = value.total_seconds() + h = int(total // 3600) + m = int((total % 3600) // 60) + s = total - h * 3600 - m * 60 + return f"{h}:{m:02d}:{s:09.6f}" + + +@frozen(kw_only=True) +class InputRegion: + """A region to annotate, parsed from the input regions TSV.""" + + filename: str + start: timedelta + stop: timedelta + label: str | None = None + row_idx: int + + @property + def id(self) -> str: + return f"{self.filename}:{self.row_idx}" + + +@frozen(kw_only=True) +class InputGtRegion: + """A ground-truth region, parsed from the input GT TSV.""" + + filename: str + start: timedelta + stop: timedelta + label: str | None = None + text: str = "" + row_idx: int + + @property + def id(self) -> str: + return f"{self.filename}:{self.row_idx}" + + +class RegionKind(str, Enum, metaclass=BetterEnumMeta): + gt = "gt" # ground-truth honeypot region + ds = "ds" # regular dataset region to annotate + + +@frozen(kw_only=True) +class PresentedRegion: + "A bundle of consecutive input ROIs from one source file, presented as a single region." + + source_filename: str + start: float # bundle span (file-time seconds): first ROI start .. last ROI stop + stop: float + kind: RegionKind + input_ids: list[str] = Factory(list) + + @property + def duration(self) -> float: + return self.stop - self.start + + +@frozen(kw_only=True) +class PlacedRegion: + "A presented region placed in an assignment" + + index: int # 0-based position within the assignment clip + source_filename: str + file_start: float + file_stop: float + clip_start: float + clip_stop: float + kind: RegionKind = RegionKind.ds + input_ids: list[str] = Factory(list) # ids of the input ROIs this bundle was built from + + +@frozen(kw_only=True) +class Assignment: + """One CVAT task: a single concatenated clip built from ``placed`` regions.""" + + id: str + clip_filename: str + clip_duration: float + placed: list[PlacedRegion] = Factory(list) + + +def _region_to_dict(r: PresentedRegion) -> dict: + return { + "source_filename": r.source_filename, + "start": r.start, + "stop": r.stop, + "kind": r.kind.value, + "input_ids": list(r.input_ids), + } + + +def _region_from_dict(d: dict) -> PresentedRegion: + return PresentedRegion( + source_filename=d["source_filename"], + start=d["start"], + stop=d["stop"], + kind=RegionKind(d["kind"]), + input_ids=list(d.get("input_ids", [])), + ) + + +def _placed_to_dict(r: PlacedRegion) -> dict: + return { + "index": r.index, + "source_filename": r.source_filename, + "file_start": r.file_start, + "file_stop": r.file_stop, + "clip_start": r.clip_start, + "clip_stop": r.clip_stop, + "kind": r.kind.value, + "input_ids": list(r.input_ids), + } + + +def _placed_from_dict(d: dict) -> PlacedRegion: + return PlacedRegion( + index=d["index"], + source_filename=d["source_filename"], + file_start=d["file_start"], + file_stop=d["file_stop"], + clip_start=d["clip_start"], + clip_stop=d["clip_stop"], + kind=RegionKind(d["kind"]), + input_ids=list(d.get("input_ids", [])), + ) + + +class TaskMetaLayout: + CLIPS_DIR = "clips" # assignment clips subdir on the oracle bucket + GT_FILENAME = "gt.tsv" + REGIONS_TSV_FILENAME = "regions.tsv" + REGIONS_FILENAME = "regions.json" + ASSIGNMENTS_FILENAME = "assignments.json" + DS_CUTS_DIR = "ds_cuts" + GT_CUTS_DIR = "gt_cuts" + + +class TaskMetaSerializer: + def serialize_regions(self, regions: list[PresentedRegion]) -> bytes: + return dump_json([_region_to_dict(r) for r in regions]) + + def parse_regions(self, data: bytes) -> list[PresentedRegion]: + return [_region_from_dict(d) for d in parse_json(data)] + + def serialize_assignments(self, assignments: list[Assignment]) -> bytes: + return dump_json( + [ + { + "id": a.id, + "clip_filename": a.clip_filename, + "clip_dur": a.clip_duration, + "placed": [_placed_to_dict(p) for p in a.placed], + } + for a in assignments + ] + ) + + def parse_assignments(self, data: bytes) -> list[Assignment]: + return [ + Assignment( + id=d["id"], + clip_filename=d["clip_filename"], + clip_duration=d["clip_dur"], + placed=[_placed_from_dict(p) for p in d.get("placed", [])], + ) + for d in parse_json(data) + ] diff --git a/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/spec.py b/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/spec.py new file mode 100644 index 0000000000..f80bfc3344 --- /dev/null +++ b/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/spec.py @@ -0,0 +1,136 @@ +from __future__ import annotations + +from datetime import timedelta +from enum import Enum +from typing import TYPE_CHECKING + +from pydantic import AnyUrl, BaseModel, Field + +from src.core.manifest.shared import BucketUrl # noqa: TC001 # runtime-needed by pydantic +from src.core.tasks.errors import UnsupportedManifestError +from src.utils.enums import BetterEnumMeta + +if TYPE_CHECKING: + from src.core.manifest.v2 import JobManifest + + +class NormalizerPreset(str, Enum, metaclass=BetterEnumMeta): + basic = "basic" + + +class TranscriptionTaskData(BaseModel): + media_url: AnyUrl | BucketUrl + regions_url: AnyUrl | BucketUrl + gt_url: AnyUrl | BucketUrl + + +class TranscriptionTaskValidation(BaseModel): + target_metric: str + target_score: float + + +class TranscriptionDetails(BaseModel): + assignment_time_limit: timedelta | None = None + "Maximum time to complete an assignment" + + validation_overhead: float = 0.2 + "Honeypot share of the assignment duration" + + normalizer: NormalizerPreset | None = NormalizerPreset.basic + "Text normalization for transcriptions" + + min_composition: tuple[int, int] = (2, 2) + "(gt, ds): minimum number of honeypot and DS regions per assignment" + + roi_min_duration: timedelta = timedelta(seconds=5) + "Minimum presented region duration. Smaller segments are bundled together" + + roi_max_duration: timedelta = timedelta(seconds=120) + "Maximum presented region duration. Used for input validation" + + roi_join_pause: timedelta = timedelta(milliseconds=700) + "Silence inserted between concatenated regions in an assignment clip" + + standard_assignment_duration: timedelta = timedelta(seconds=60) + "Target audio duration per assignment" + + # TODO: add honeypot rotation later + # max_validation_gt: float = 0.05 + # "Maximum share of GT regions used as honeypots. The rest is used for honeypot rotation" + + max_discarded_gt: float = 0.5 + "Maximum share of GT regions that may be discarded before job creation fails" + + sample_rate: int = 48000 + "Sample rate of the generated assignment clips, Hz" + + transcription_attr_name: str = "transcription" + "The name of the output attribute for transcriptions in CVAT tasks and annotations" + + random_seed: int = 0 + "Seed for deterministic honeypot selection and assignment mixing" + + +class TranscriptionTaskSpecification(BaseModel): + data: TranscriptionTaskData + + labels: list[str] + shared_attributes: list = Field(default_factory=list) + user_guide: str = "" + + validation: TranscriptionTaskValidation + + details: TranscriptionDetails = Field(default_factory=TranscriptionDetails) + + +def parse_audio_manifest(manifest: JobManifest) -> TranscriptionTaskSpecification: + """ + Produce the task specification from a manifest, encapsulating manifest-version differences. + + Only v2 manifests are supported. + """ + + version = getattr(manifest, "version", 1) + if version != 2: + raise UnsupportedManifestError( + f"Audio transcription requires a v2 manifest, got version {version}" + ) + + annotation = manifest.annotation + validation = annotation.validation + manifest_details = annotation.details + + # Only pass the details fields the manifest provides; pydantic fills the rest with defaults + # and coerces raw values (e.g. normalizer str -> NormalizerPreset). + details_fields: dict = {} + if manifest_details is not None: + if manifest_details.max_segment_duration is not None: + details_fields["roi_max_duration"] = timedelta( + seconds=manifest_details.max_segment_duration + ) + if manifest_details.validation_overhead is not None: + details_fields["validation_overhead"] = manifest_details.validation_overhead / 100.0 + if manifest_details.min_composition is not None: + details_fields["min_composition"] = ( + manifest_details.min_composition.gt, + manifest_details.min_composition.ds, + ) + details_fields["normalizer"] = manifest_details.normalizer + if manifest_details.max_time is not None: + details_fields["assignment_time_limit"] = timedelta(seconds=manifest_details.max_time) + + return TranscriptionTaskSpecification( + data=TranscriptionTaskData( + media_url=manifest.data.media_url, + regions_url=manifest.data.regions_url, + gt_url=manifest.data.gt_url, + ), + labels=[label.name for label in annotation.labels], + shared_attributes=list(annotation.shared_attributes), + user_guide=annotation.user_guide_url, + validation=TranscriptionTaskValidation( + target_metric=validation.target_metric.value, + target_score=validation.target_score, + ), + details=TranscriptionDetails(**details_fields), + ) diff --git a/packages/examples/cvat/recording-oracle/src/core/tasks/cvat_formats.py b/packages/examples/cvat/recording-oracle/src/core/tasks/cvat_formats.py new file mode 100644 index 0000000000..a2a465c891 --- /dev/null +++ b/packages/examples/cvat/recording-oracle/src/core/tasks/cvat_formats.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from src.core.tasks.types import TaskTypes + +DM_DATASET_FORMAT_MAPPING = { + TaskTypes.image_label_binary: "cvat_images", + TaskTypes.image_points: "coco_person_keypoints", + TaskTypes.image_boxes: "coco_instances", + TaskTypes.image_polygons: "coco_instances", + TaskTypes.image_boxes_from_points: "coco_instances", + TaskTypes.image_skeletons_from_boxes: "coco_person_keypoints", +} + +DM_GT_DATASET_FORMAT_MAPPING = { + # GT uses the same format both for boxes and points + TaskTypes.image_label_binary: "cvat_images", + TaskTypes.image_points: "coco_instances", + TaskTypes.image_boxes: "coco_instances", + TaskTypes.image_polygons: "coco_instances", + TaskTypes.image_boxes_from_points: "coco_instances", + TaskTypes.image_skeletons_from_boxes: "coco_person_keypoints", +} diff --git a/packages/examples/cvat/recording-oracle/src/core/tasks/errors.py b/packages/examples/cvat/recording-oracle/src/core/tasks/errors.py new file mode 100644 index 0000000000..6f002952f7 --- /dev/null +++ b/packages/examples/cvat/recording-oracle/src/core/tasks/errors.py @@ -0,0 +1,2 @@ +class UnsupportedManifestError(Exception): + pass diff --git a/packages/examples/cvat/recording-oracle/src/core/tasks/types.py b/packages/examples/cvat/recording-oracle/src/core/tasks/types.py new file mode 100644 index 0000000000..6912483e39 --- /dev/null +++ b/packages/examples/cvat/recording-oracle/src/core/tasks/types.py @@ -0,0 +1,13 @@ +from enum import Enum + +from src.utils.enums import BetterEnumMeta + + +class TaskTypes(str, Enum, metaclass=BetterEnumMeta): + image_label_binary = "image_label_binary" + image_points = "image_points" + image_boxes = "image_boxes" + image_boxes_from_points = "image_boxes_from_points" + image_skeletons_from_boxes = "image_skeletons_from_boxes" + image_polygons = "image_polygons" + audio_transcription = "audio_transcription" diff --git a/packages/examples/cvat/recording-oracle/src/core/types.py b/packages/examples/cvat/recording-oracle/src/core/types.py index 682b9f2703..07f4b3e8aa 100644 --- a/packages/examples/cvat/recording-oracle/src/core/types.py +++ b/packages/examples/cvat/recording-oracle/src/core/types.py @@ -10,15 +10,6 @@ class Networks(int, Enum): localhost = Config.localhost.chain_id -class TaskTypes(str, Enum, metaclass=BetterEnumMeta): - image_label_binary = "image_label_binary" - image_points = "image_points" - image_polygons = "image_polygons" - image_boxes = "image_boxes" - image_boxes_from_points = "image_boxes_from_points" - image_skeletons_from_boxes = "image_skeletons_from_boxes" - - class OracleWebhookTypes(str, Enum, metaclass=BetterEnumMeta): exchange_oracle = "exchange_oracle" recording_oracle = "recording_oracle" diff --git a/packages/examples/cvat/recording-oracle/src/handlers/completion.py b/packages/examples/cvat/recording-oracle/src/handlers/completion.py index 22db6e9f29..d82651982a 100644 --- a/packages/examples/cvat/recording-oracle/src/handlers/completion.py +++ b/packages/examples/cvat/recording-oracle/src/handlers/completion.py @@ -9,7 +9,7 @@ import src.services.webhook as oracle_db_service from src.chain import escrow from src.core.config import Config -from src.core.manifest import TaskManifest, parse_manifest +from src.core.manifest import ManifestBase, parse_manifest from src.core.oracle_events import ( RecordingOracleEvent_JobCompleted, ) @@ -34,7 +34,7 @@ class _TaskUploader: def __init__( - self, escrow_address: str, chain_id: int, manifest: TaskManifest, db_session: Session + self, escrow_address: str, chain_id: int, manifest: ManifestBase, db_session: Session ) -> None: self.escrow_address = escrow_address self.chain_id = chain_id diff --git a/packages/examples/cvat/recording-oracle/src/handlers/process_intermediate_results.py b/packages/examples/cvat/recording-oracle/src/handlers/process_intermediate_results.py index 74a1bde5ca..5daadf9dbd 100644 --- a/packages/examples/cvat/recording-oracle/src/handlers/process_intermediate_results.py +++ b/packages/examples/cvat/recording-oracle/src/handlers/process_intermediate_results.py @@ -19,7 +19,9 @@ from src.core.annotation_meta import AnnotationMeta from src.core.config import Config from src.core.gt_stats import GtKey, GtStats, ValidationFrameStats -from src.core.types import TaskTypes +from src.core.manifest import get_manifest_task_type +from src.core.tasks import TaskTypes +from src.core.tasks.cvat_formats import DM_DATASET_FORMAT_MAPPING, DM_GT_DATASET_FORMAT_MAPPING from src.core.validation_errors import ( DatasetValidationError, LowAccuracyError, @@ -41,27 +43,9 @@ from sqlalchemy.orm import Session - from src.core.manifest import TaskManifest + from src.core.manifest import ManifestBase from src.cvat.interface import QualityReportData -DM_DATASET_FORMAT_MAPPING = { - TaskTypes.image_label_binary: "cvat_images", - TaskTypes.image_polygons: "coco_instances", - TaskTypes.image_points: "coco_person_keypoints", - TaskTypes.image_boxes: "coco_instances", - TaskTypes.image_boxes_from_points: "coco_instances", - TaskTypes.image_skeletons_from_boxes: "coco_person_keypoints", -} - -DM_GT_DATASET_FORMAT_MAPPING = { - TaskTypes.image_label_binary: "cvat_images", - TaskTypes.image_points: "coco_instances", # we compare points against boxes - TaskTypes.image_polygons: "coco_instances", - TaskTypes.image_boxes: "coco_instances", - TaskTypes.image_boxes_from_points: "coco_instances", - TaskTypes.image_skeletons_from_boxes: "coco_person_keypoints", -} - _JobResults = dict[int, float] _RejectedJobs = dict[int, DatasetValidationError] @@ -94,7 +78,7 @@ def __init__( self, escrow_address: str, chain_id: int, - manifest: TaskManifest, + manifest: ManifestBase, ) -> None: self.escrow_address = escrow_address self.chain_id = chain_id @@ -115,7 +99,7 @@ def __init__( self, escrow_address: str, chain_id: int, - manifest: TaskManifest, + manifest: ManifestBase, *, meta: AnnotationMeta, gt_stats: GtStats | None = None, @@ -133,6 +117,8 @@ def __init__( def _validate_jobs(self): manifest = self._require_field(self.manifest) meta = self._require_field(self._meta) + if manifest.version != 1: + raise NotImplementedError job_results: _JobResults = {} rejected_jobs: _RejectedJobs = {} @@ -260,7 +246,7 @@ def __init__( self, escrow_address: str, chain_id: int, - manifest: TaskManifest, + manifest: ManifestBase, *, merged_annotations: io.IOBase, ) -> None: @@ -281,7 +267,7 @@ def _parse_gt_dataset(self, gt_file_data: bytes) -> dm.Dataset: gt_dataset = dm.Dataset.import_from( gt_filename, - format=DM_GT_DATASET_FORMAT_MAPPING[self.manifest.annotation.type], + format=DM_GT_DATASET_FORMAT_MAPPING[get_manifest_task_type(self.manifest)], ) gt_dataset.init_cache() @@ -324,7 +310,7 @@ def _prepare_merged_dataset(self): input_gt_dataset = self._require_field(self._input_gt_dataset) merged_dataset_path = tempdir / "merged" - merged_dataset_format = DM_DATASET_FORMAT_MAPPING[manifest.annotation.type] + merged_dataset_format = DM_DATASET_FORMAT_MAPPING[get_manifest_task_type(manifest)] extract_zip_archive(merged_annotations, merged_dataset_path) merged_dataset = dm.Dataset.import_from( @@ -346,13 +332,13 @@ def _prepare_merged_dataset(self): @classmethod def _put_gt_into_merged_dataset( - cls, input_gt_dataset: dm.Dataset, merged_dataset: dm.Dataset, *, manifest: TaskManifest + cls, input_gt_dataset: dm.Dataset, merged_dataset: dm.Dataset, *, manifest: ManifestBase ) -> None: """ Updates the merged dataset inplace, writing GT annotations corresponding to the task type. """ - match manifest.annotation.type: + match get_manifest_task_type(manifest): case TaskTypes.image_boxes.value | TaskTypes.image_polygons.value: merged_dataset.update(input_gt_dataset) case TaskTypes.image_points.value: @@ -398,7 +384,7 @@ def _put_gt_into_merged_dataset( ) merged_dataset.update(input_gt_dataset) case _: - raise AssertionError(f"Unknown task type {manifest.annotation.type}") + raise AssertionError(f"Unknown task type {get_manifest_task_type(manifest)}") def merge_results(self) -> io.IOBase: with TemporaryDirectory() as tempdir: @@ -423,7 +409,7 @@ class _TaskHoneypotManager: def __init__( self, task: db_models.Task, - manifest: TaskManifest, + manifest: ManifestBase, *, annotation_meta: AnnotationMeta, gt_stats: GtStats, @@ -744,7 +730,7 @@ def process_intermediate_results( # noqa: PLR0912 escrow_address: str, chain_id: int, meta: AnnotationMeta, - manifest: TaskManifest, + manifest: ManifestBase, logger: logging.Logger, ) -> ValidationSuccess | ValidationFailure: should_complete = False @@ -953,7 +939,7 @@ def process_final_results( chain_id: int, meta: AnnotationMeta, merged_annotations: io.RawIOBase, - manifest: TaskManifest, + manifest: ManifestBase, logger: logging.Logger, ) -> FinalResult: assert logger # unused diff --git a/packages/examples/cvat/recording-oracle/src/handlers/validation.py b/packages/examples/cvat/recording-oracle/src/handlers/validation.py index 8c13346efa..7b20f1daa5 100644 --- a/packages/examples/cvat/recording-oracle/src/handlers/validation.py +++ b/packages/examples/cvat/recording-oracle/src/handlers/validation.py @@ -1,15 +1,17 @@ import io from collections import Counter from logging import Logger +from typing import cast from sqlalchemy.orm import Session import src.core.annotation_meta as annotation +import src.core.manifest as manifest_utils import src.core.validation_meta as validation import src.services.webhook as oracle_db_service from src.chain import escrow from src.core.config import Config -from src.core.manifest import TaskManifest, parse_manifest +from src.core.manifest import ManifestBase from src.core.oracle_events import ( RecordingOracleEvent_JobCompleted, RecordingOracleEvent_SubmissionRejected, @@ -35,7 +37,7 @@ class _TaskValidator: def __init__( - self, escrow_address: str, chain_id: int, manifest: TaskManifest, db_session: Session + self, escrow_address: str, chain_id: int, manifest: ManifestBase, db_session: Session ) -> None: self.escrow_address = escrow_address self.chain_id = chain_id @@ -137,6 +139,15 @@ def _handle_validation_result(self, validation_result: ValidationResult): job_meta.job_id: job_meta.assignment_id for job_meta in self.annotation_meta.jobs } + if self.manifest.version == 1: + manifest = cast(manifest_utils.v1.JobManifest, self.manifest) + quality_threshold = manifest.validation.min_quality + elif self.manifest.version == 2: + manifest = cast(manifest_utils.v2.JobManifest, self.manifest) + quality_threshold = manifest.annotation.validation.target_score + else: + raise NotImplementedError(f"Unknown manifest version '{manifest.version}'") + oracle_db_service.outbox.create_webhook( db_session, escrow_address, @@ -149,7 +160,7 @@ def _handle_validation_result(self, validation_result: ValidationResult): assignment_id=job_id_to_assignment_id[rejected_job_id], reason=self._LOW_QUALITY_REASON_MESSAGE_TEMPLATE.format( validation_result.job_results[rejected_job_id], - self.manifest.validation.min_quality, + quality_threshold, ), ) for rejected_job_id, reason in validation_result.rejected_jobs.items() @@ -168,7 +179,7 @@ def validate_results( ): logger = get_function_logger(module_logger_name) - manifest = parse_manifest(escrow.get_escrow_manifest(chain_id, escrow_address)) + manifest = manifest_utils.parse_manifest(escrow.get_escrow_manifest(chain_id, escrow_address)) validator = _TaskValidator( escrow_address=escrow_address, chain_id=chain_id, manifest=manifest, db_session=db_session diff --git a/packages/examples/cvat/recording-oracle/src/services/cloud/types.py b/packages/examples/cvat/recording-oracle/src/services/cloud/types.py index 935dc7f4b1..830714f119 100644 --- a/packages/examples/cvat/recording-oracle/src/services/cloud/types.py +++ b/packages/examples/cvat/recording-oracle/src/services/cloud/types.py @@ -9,8 +9,8 @@ import pydantic -from src.core import manifest from src.core.config import Config, IStorageConfig +from src.core.manifest.shared import BucketUrl, BucketUrlBase from src.services.cloud.gcs import DEFAULT_GCS_HOST from src.services.cloud.s3 import DEFAULT_S3_HOST from src.utils.enums import BetterEnumMeta @@ -171,14 +171,14 @@ def from_storage_config(cls, config: type[IStorageConfig]) -> BucketAccessInfo: ) @classmethod - def from_bucket_url(cls, bucket_url: manifest.BucketUrl) -> BucketAccessInfo: + def from_bucket_url(cls, bucket_url: BucketUrl) -> BucketAccessInfo: return cls._from_dict(bucket_url.model_dump()) @classmethod def parse_obj( - cls, data: str | type[IStorageConfig] | manifest.BucketUrl | pydantic.AnyUrl + cls, data: str | type[IStorageConfig] | BucketUrl | pydantic.AnyUrl ) -> BucketAccessInfo: - if isinstance(data, manifest.BucketUrlBase): + if isinstance(data, BucketUrlBase): return cls.from_bucket_url(data) if isinstance(data, str): return cls.from_url(data) diff --git a/packages/examples/cvat/recording-oracle/tests/utils/helpers.py b/packages/examples/cvat/recording-oracle/tests/utils/helpers.py index 22ff66fa0f..eff59da8cf 100644 --- a/packages/examples/cvat/recording-oracle/tests/utils/helpers.py +++ b/packages/examples/cvat/recording-oracle/tests/utils/helpers.py @@ -1,6 +1,6 @@ from collections.abc import Iterable -from src.core.manifest import TaskManifest, parse_manifest +from src.core.manifest import ManifestBase, parse_manifest def generate_manifest( @@ -9,7 +9,7 @@ def generate_manifest( job_size: int = 10, validation_frames_per_job: int = 2, labels: int | Iterable[int] = 1, -) -> TaskManifest: +) -> ManifestBase: label_definitions = [] if isinstance(labels, int): label_definitions.extend({"name": f"label_{i}"} for i in range(labels)) From 53ea4ae3e30df335c7ed6040c236b93f8de2bf25 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Tue, 7 Jul 2026 17:41:54 +0300 Subject: [PATCH 22/53] Refactor factory placement --- .../src/handlers/job_completion/handlers.py | 2 +- .../{ => validators}/factory.py | 0 .../src/handlers/job_creation/__init__.py | 2 +- .../job_creation/{ => builders}/factory.py | 24 +++++++++---------- .../src/handlers/job_creation/handlers.py | 17 +++++++++++++ .../job_export/{ => exporters}/factory.py | 0 .../src/handlers/job_export/handlers.py | 2 +- .../test_process_job_launcher_webhooks.py | 4 ++-- 8 files changed, 34 insertions(+), 17 deletions(-) rename packages/examples/cvat/exchange-oracle/src/handlers/job_completion/{ => validators}/factory.py (100%) rename packages/examples/cvat/exchange-oracle/src/handlers/job_creation/{ => builders}/factory.py (70%) create mode 100644 packages/examples/cvat/exchange-oracle/src/handlers/job_creation/handlers.py rename packages/examples/cvat/exchange-oracle/src/handlers/job_export/{ => exporters}/factory.py (100%) diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/handlers.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/handlers.py index 452d0b97bd..c050160e07 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/handlers.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/handlers.py @@ -9,7 +9,7 @@ from src.core.types import EscrowValidationStatuses from src.db import SessionLocal from src.db.utils import ForUpdateParams -from src.handlers.job_completion.factory import create_validator +from src.handlers.job_completion.validators.factory import create_validator from src.utils.logging import get_function_logger if TYPE_CHECKING: diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/factory.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/validators/factory.py similarity index 100% rename from packages/examples/cvat/exchange-oracle/src/handlers/job_completion/factory.py rename to packages/examples/cvat/exchange-oracle/src/handlers/job_completion/validators/factory.py diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/__init__.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/__init__.py index 9e4f056f12..a74072dd7d 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/__init__.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/__init__.py @@ -1,3 +1,3 @@ -from src.handlers.job_creation.factory import create_task +from src.handlers.job_creation.handlers import create_task __all__ = ["create_task"] diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/factory.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/factory.py similarity index 70% rename from packages/examples/cvat/exchange-oracle/src/handlers/job_creation/factory.py rename to packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/factory.py index e56c4bb296..a77a2370e5 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/factory.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/factory.py @@ -1,7 +1,8 @@ from __future__ import annotations -from src.chain.escrow import get_escrow_manifest -from src.core.manifest import get_manifest_task_type, parse_manifest +from typing import TYPE_CHECKING + +from src.core.manifest import get_manifest_task_type from src.core.tasks import TaskTypes from src.handlers.job_creation.builders.audio.transcription import AudioTranscriptionTaskBuilder from src.handlers.job_creation.builders.vision.basic import ( @@ -14,16 +15,17 @@ from src.handlers.job_creation.builders.vision.skeletons_from_boxes import ( SkeletonsFromBoxesTaskBuilder, ) -from src.log import ROOT_LOGGER_NAME -from src.utils.logging import get_function_logger - -module_logger = f"{ROOT_LOGGER_NAME}.cron.cvat" +if TYPE_CHECKING: + from src.core.manifest import ManifestBase + from src.handlers.job_creation.builders.vision.base import TaskBuilderBase -def create_task(escrow_address: str, chain_id: int) -> None: - logger = get_function_logger(module_logger) - manifest = parse_manifest(get_escrow_manifest(chain_id, escrow_address)) +def create_builder( + manifest: ManifestBase, + escrow_address: str, + chain_id: int, +) -> TaskBuilderBase: task_type = get_manifest_task_type(manifest) match task_type: @@ -44,6 +46,4 @@ def create_task(escrow_address: str, chain_id: int) -> None: case _: raise NotImplementedError(f"Unsupported task type '{task_type}'") - with builder_type(manifest, escrow_address, chain_id) as task_builder: - task_builder.set_logger(logger) - task_builder.build() + return builder_type(manifest, escrow_address, chain_id) diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/handlers.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/handlers.py new file mode 100644 index 0000000000..243f06122f --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/handlers.py @@ -0,0 +1,17 @@ +from src.chain.escrow import get_escrow_manifest +from src.core.manifest import parse_manifest +from src.handlers.job_creation.builders.factory import create_builder +from src.log import ROOT_LOGGER_NAME +from src.utils.logging import get_function_logger + +module_logger = f"{ROOT_LOGGER_NAME}.cron.cvat" + + +def create_task(escrow_address: str, chain_id: int) -> None: + logger = get_function_logger(module_logger) + + manifest = parse_manifest(get_escrow_manifest(chain_id, escrow_address)) + + with create_builder(manifest, escrow_address, chain_id) as task_builder: + task_builder.set_logger(logger) + task_builder.build() diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/factory.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/factory.py similarity index 100% rename from packages/examples/cvat/exchange-oracle/src/handlers/job_export/factory.py rename to packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/factory.py diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/handlers.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/handlers.py index e45054363f..af6fc8006e 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/handlers.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/handlers.py @@ -6,7 +6,7 @@ from src.chain.escrow import get_escrow_manifest, validate_escrow from src.core.manifest import parse_manifest from src.db.utils import ForUpdateParams -from src.handlers.job_export.factory import create_exporter +from src.handlers.job_export.exporters.factory import create_exporter if TYPE_CHECKING: import logging diff --git a/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_job_launcher_webhooks.py b/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_job_launcher_webhooks.py index 17857e0081..5ac9267b70 100644 --- a/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_job_launcher_webhooks.py +++ b/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_job_launcher_webhooks.py @@ -63,7 +63,7 @@ def test_process_incoming_job_launcher_webhooks_escrow_created_type(self): with ( patch("src.chain.escrow.get_escrow") as mock_escrow, open("tests/assets/manifests/manifest.json") as data, - patch("src.handlers.job_creation.factory.get_escrow_manifest") as mock_get_manifest, + patch("src.handlers.job_creation.handlers.get_escrow_manifest") as mock_get_manifest, patch("src.handlers.job_creation.builders.vision.basic.cvat_api") as mock_cvat_api, patch( "src.handlers.job_creation.builders.vision.basic.cloud_service.make_client" @@ -249,7 +249,7 @@ def test_process_incoming_job_launcher_webhooks_escrow_created_type_remove_when_ with ( patch("src.chain.escrow.get_escrow") as mock_escrow, open("tests/assets/manifests/manifest.json") as data, - patch("src.handlers.job_creation.factory.get_escrow_manifest") as mock_get_manifest, + patch("src.handlers.job_creation.handlers.get_escrow_manifest") as mock_get_manifest, patch("src.handlers.job_creation.builders.vision.basic.cvat_api") as mock_cvat_api, patch( "src.handlers.job_creation.builders.vision.basic.cloud_service.make_client" From f130c69f749e4d0e04a2a0036e0bd5c56c455bd1 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Tue, 7 Jul 2026 17:42:35 +0300 Subject: [PATCH 23/53] Refactor validation in RO --- .../src/handlers/completion.py | 2 +- .../src/handlers/validation/__init__.py | 12 + .../src/handlers/validation/common.py | 66 +++ .../src/handlers/validation/final_results.py | 282 ++++++++++ .../{validation.py => validation/handlers.py} | 11 +- .../intermediate_results.py} | 509 +----------------- .../src/handlers/validation/meta.py | 18 + .../validation/quality_checkers/__init__.py | 0 .../validation/quality_checkers/audio.py | 8 + .../validation/quality_checkers/base.py | 60 +++ .../validation/quality_checkers/factory.py | 42 ++ .../validation/quality_checkers/image.py | 125 +++++ .../services/test_validation_service.py | 128 ++--- 13 files changed, 700 insertions(+), 563 deletions(-) create mode 100644 packages/examples/cvat/recording-oracle/src/handlers/validation/__init__.py create mode 100644 packages/examples/cvat/recording-oracle/src/handlers/validation/common.py create mode 100644 packages/examples/cvat/recording-oracle/src/handlers/validation/final_results.py rename packages/examples/cvat/recording-oracle/src/handlers/{validation.py => validation/handlers.py} (96%) rename packages/examples/cvat/recording-oracle/src/handlers/{process_intermediate_results.py => validation/intermediate_results.py} (54%) create mode 100644 packages/examples/cvat/recording-oracle/src/handlers/validation/meta.py create mode 100644 packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/__init__.py create mode 100644 packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/audio.py create mode 100644 packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/base.py create mode 100644 packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/factory.py create mode 100644 packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/image.py diff --git a/packages/examples/cvat/recording-oracle/src/handlers/completion.py b/packages/examples/cvat/recording-oracle/src/handlers/completion.py index d82651982a..00efffdb25 100644 --- a/packages/examples/cvat/recording-oracle/src/handlers/completion.py +++ b/packages/examples/cvat/recording-oracle/src/handlers/completion.py @@ -18,7 +18,7 @@ ) from src.core.types import OracleWebhookTypes from src.core.validation_results import FinalResult -from src.handlers.process_intermediate_results import ( +from src.handlers.validation import ( parse_annotation_metafile, process_final_results, serialize_validation_meta, diff --git a/packages/examples/cvat/recording-oracle/src/handlers/validation/__init__.py b/packages/examples/cvat/recording-oracle/src/handlers/validation/__init__.py new file mode 100644 index 0000000000..03e1f4040e --- /dev/null +++ b/packages/examples/cvat/recording-oracle/src/handlers/validation/__init__.py @@ -0,0 +1,12 @@ +from src.handlers.validation.final_results import process_final_results +from src.handlers.validation.handlers import validate_results +from src.handlers.validation.intermediate_results import process_intermediate_results +from src.handlers.validation.meta import parse_annotation_metafile, serialize_validation_meta + +__all__ = [ + "parse_annotation_metafile", + "process_final_results", + "process_intermediate_results", + "serialize_validation_meta", + "validate_results", +] diff --git a/packages/examples/cvat/recording-oracle/src/handlers/validation/common.py b/packages/examples/cvat/recording-oracle/src/handlers/validation/common.py new file mode 100644 index 0000000000..2cc9784a11 --- /dev/null +++ b/packages/examples/cvat/recording-oracle/src/handlers/validation/common.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, TypeVar + +import src.cvat.api_calls as cvat_api +from src.core.validation_errors import DatasetValidationError + +if TYPE_CHECKING: + from pathlib import Path + + from src.core.gt_stats import GtStats + from src.core.manifest import ManifestBase + +UNKNOWN_QUALITY = -1 +"The value to be used when job quality cannot be computed (e.g. no GT images available)" + +_JobResults = dict[int, float] + +_RejectedJobs = dict[int, DatasetValidationError] + +_HoneypotFrameId = int +_ValidationFrameId = int +_HoneypotFrameToValFrame = dict[_HoneypotFrameId, _ValidationFrameId] +_TaskIdToValidationLayout = dict[int, cvat_api.models.ITaskValidationLayoutRead] +_TaskIdToHoneypotsMapping = dict[int, _HoneypotFrameToValFrame] +_TaskIdToFrameNames = dict[int, list[str]] +_TaskIdToLabels = dict[int, list[str]] + + +@dataclass +class _ValidationResult: + job_results: _JobResults + rejected_jobs: _RejectedJobs + gt_stats: GtStats + task_id_to_val_layout: _TaskIdToValidationLayout + task_id_to_honeypots_mapping: _TaskIdToHoneypotsMapping + task_id_to_frame_names: _TaskIdToFrameNames + task_id_to_labels: _TaskIdToLabels + + +@dataclass +class _HoneypotUpdateResult: + updated_gt_stats: GtStats + can_continue_annotation: bool + + +T = TypeVar("T") + + +class _TaskHandler: + def __init__( + self, + escrow_address: str, + chain_id: int, + manifest: ManifestBase, + ) -> None: + self.escrow_address = escrow_address + self.chain_id = chain_id + self.manifest = manifest + + self._temp_dir: Path | None = None + + def _require_field(self, field: T | None) -> T: + assert field is not None + return field diff --git a/packages/examples/cvat/recording-oracle/src/handlers/validation/final_results.py b/packages/examples/cvat/recording-oracle/src/handlers/validation/final_results.py new file mode 100644 index 0000000000..0f6a303579 --- /dev/null +++ b/packages/examples/cvat/recording-oracle/src/handlers/validation/final_results.py @@ -0,0 +1,282 @@ +from __future__ import annotations + +import io +import os +from pathlib import Path +from tempfile import TemporaryDirectory +from typing import TYPE_CHECKING + +import datumaro as dm +import numpy as np + +import src.services.validation as db_service +from src.core.manifest import get_manifest_task_type +from src.core.tasks import TaskTypes +from src.core.tasks.cvat_formats import DM_DATASET_FORMAT_MAPPING, DM_GT_DATASET_FORMAT_MAPPING +from src.core.validation_meta import JobMeta, ResultMeta, ValidationMeta +from src.core.validation_results import FinalResult +from src.db.utils import ForUpdateParams +from src.handlers.validation.common import UNKNOWN_QUALITY, _JobResults, _TaskHandler +from src.services.cloud import make_client as make_cloud_client +from src.services.cloud.utils import BucketAccessInfo +from src.utils.annotations import ProjectLabels +from src.utils.zip_archive import extract_zip_archive, write_dir_to_zip_archive + +if TYPE_CHECKING: + import logging + + from sqlalchemy.orm import Session + + from src.core.annotation_meta import AnnotationMeta + from src.core.manifest import ManifestBase + + +class _TaskAnnotationMerger(_TaskHandler): + def __init__( + self, + escrow_address: str, + chain_id: int, + manifest: ManifestBase, + *, + merged_annotations: io.IOBase, + ) -> None: + super().__init__(escrow_address=escrow_address, chain_id=chain_id, manifest=manifest) + + self._merged_annotations: io.IOBase = merged_annotations + + self._updated_merged_dataset_archive: io.IOBase | None = None + + self._temp_dir: Path | None = None + self._input_gt_dataset: dm.Dataset | None = None + + def _parse_gt_dataset(self, gt_file_data: bytes) -> dm.Dataset: + with TemporaryDirectory() as gt_temp_dir: + gt_filename = os.path.join(gt_temp_dir, "gt_annotations.json") + with open(gt_filename, "wb") as f: + f.write(gt_file_data) + + gt_dataset = dm.Dataset.import_from( + gt_filename, + format=DM_GT_DATASET_FORMAT_MAPPING[get_manifest_task_type(self.manifest)], + ) + + gt_dataset.init_cache() + + return gt_dataset + + def _load_gt_dataset(self): + input_gt_bucket = BucketAccessInfo.parse_obj(self.manifest.validation.gt_url) + gt_bucket_client = make_cloud_client(input_gt_bucket) + gt_data = gt_bucket_client.download_file(input_gt_bucket.path) + self._input_gt_dataset = self._parse_gt_dataset(gt_data) + + def _restore_original_image_paths(self, merged_dataset: dm.Dataset) -> dm.Dataset: + class RemoveCommonPrefix(dm.ItemTransform): + def __init__(self, extractor: dm.IExtractor, *, prefix: str) -> None: + super().__init__(extractor) + self._prefix = prefix + + def transform_item(self, item: dm.DatasetItem) -> dm.DatasetItem: + if item.id.startswith(self._prefix): + item = item.wrap(id=item.id[len(self._prefix) :]) + return item + + prefix = BucketAccessInfo.parse_obj(self.manifest.data.data_url).path.strip("/\\") + "/" + + # Remove prefixes if it can be done safely + sample_ids = {sample.id for sample in merged_dataset} + if all( + sample_id.startswith(prefix) and (sample_id[len(prefix) :] not in sample_ids) + for sample_id in sample_ids + ): + merged_dataset.transform(RemoveCommonPrefix, prefix=prefix) + + return merged_dataset + + def _prepare_merged_dataset(self): + tempdir = self._require_field(self._temp_dir) + manifest = self._require_field(self.manifest) + merged_annotations = self._require_field(self._merged_annotations) + input_gt_dataset = self._require_field(self._input_gt_dataset) + + merged_dataset_path = tempdir / "merged" + merged_dataset_format = DM_DATASET_FORMAT_MAPPING[get_manifest_task_type(manifest)] + extract_zip_archive(merged_annotations, merged_dataset_path) + + merged_dataset = dm.Dataset.import_from( + os.fspath(merged_dataset_path), format=merged_dataset_format + ) + self._restore_original_image_paths(merged_dataset) + self._put_gt_into_merged_dataset(input_gt_dataset, merged_dataset, manifest=manifest) + + updated_merged_dataset_path = tempdir / "merged_updated" + merged_dataset.export( + updated_merged_dataset_path, merged_dataset_format, save_media=False, reindex=True + ) + + updated_merged_dataset_archive = io.BytesIO() + write_dir_to_zip_archive(updated_merged_dataset_path, updated_merged_dataset_archive) + updated_merged_dataset_archive.seek(0) + + self._updated_merged_dataset_archive = updated_merged_dataset_archive + + @classmethod + def _put_gt_into_merged_dataset( + cls, input_gt_dataset: dm.Dataset, merged_dataset: dm.Dataset, *, manifest: ManifestBase + ) -> None: + """ + Updates the merged dataset inplace, writing GT annotations corresponding to the task type. + """ + + match get_manifest_task_type(manifest): + case TaskTypes.image_boxes.value | TaskTypes.image_polygons.value: + merged_dataset.update(input_gt_dataset) + case TaskTypes.image_points.value: + merged_label_cat: dm.LabelCategories = merged_dataset.categories()[ + dm.AnnotationType.label + ] + + # we support no more than 1 label so far + assert len(manifest.annotation.labels) == 1 + + skeleton_label_id = next( + i for i, label in enumerate(merged_label_cat) if not label.parent + ) + point_label_id = next(i for i, label in enumerate(merged_label_cat) if label.parent) + + for sample in input_gt_dataset: + annotations = [ + dm.Skeleton( + elements=[ + # Put a point in the center of each GT bbox + # Not ideal, but it's the target for now + dm.Points( + [bbox.x + bbox.w / 2, bbox.y + bbox.h / 2], + label=point_label_id, + attributes=bbox.attributes, + ) + ], + label=skeleton_label_id, + ) + for bbox in sample.annotations + if isinstance(bbox, dm.Bbox) + ] + merged_dataset.put(sample.wrap(annotations=annotations)) + case TaskTypes.image_label_binary.value: + merged_dataset.update(input_gt_dataset) + case TaskTypes.image_boxes_from_points: + merged_dataset.update(input_gt_dataset) + case TaskTypes.image_skeletons_from_boxes: + # The original behavior of project_labels is broken for skeletons + input_gt_dataset = dm.Dataset(input_gt_dataset) + input_gt_dataset = input_gt_dataset.transform( + ProjectLabels, dst_labels=merged_dataset.categories()[dm.AnnotationType.label] + ) + merged_dataset.update(input_gt_dataset) + case _: + raise AssertionError(f"Unknown task type {get_manifest_task_type(manifest)}") + + def merge_results(self) -> io.IOBase: + with TemporaryDirectory() as tempdir: + self._temp_dir = Path(tempdir) + + self._load_gt_dataset() + self._prepare_merged_dataset() + + return self._require_field(self._updated_merged_dataset_archive) + + +def process_final_results( + session: Session, + *, + escrow_address: str, + chain_id: int, + meta: AnnotationMeta, + merged_annotations: io.RawIOBase, + manifest: ManifestBase, + logger: logging.Logger, +) -> FinalResult: + assert logger # unused + + task = db_service.get_task_by_escrow_address( + session, + escrow_address, + for_update=ForUpdateParams( + nowait=True + ), # should not happen, but waiting should not block processing + ) + if not task: + raise AssertionError(f"Validation results for escrow {escrow_address} not found") + + merger = _TaskAnnotationMerger( + escrow_address=escrow_address, + chain_id=chain_id, + manifest=manifest, + merged_annotations=merged_annotations, + ) + + merged_annotations = merger.merge_results() + + job_final_result_ids: dict[str, str] = {} + + for job_meta in meta.jobs: + job = db_service.get_job_by_cvat_id(session, job_meta.job_id) + if not job: + raise AssertionError( + f"Can't find validation results for job " f"{job_meta.job_id} ({escrow_address=})" + ) + + assignment_validation_result = db_service.get_validation_result_by_assignment_id( + session, job_meta.assignment_id + ) + if not assignment_validation_result: + raise AssertionError( + f"Can't find validation results for assignments " + f"{job_meta.assignment_id} ({escrow_address=})" + ) + + job_final_result_ids[job.id] = assignment_validation_result.id + + task_jobs = task.jobs + + task_validation_results = db_service.get_task_validation_results(session, task.id) + + job_id_to_meta_id = {job.id: i for i, job in enumerate(task_jobs)} + + validation_result_id_to_meta_id = {r.id: i for i, r in enumerate(task_validation_results)} + + validation_meta = ValidationMeta( + jobs=[ + JobMeta( + job_id=job_id_to_meta_id[job.id], + final_result_id=validation_result_id_to_meta_id[job_final_result_ids[job.id]], + ) + for job in task_jobs + ], + results=[ + ResultMeta( + id=validation_result_id_to_meta_id[r.id], + job_id=job_id_to_meta_id[r.job.id], + annotator_wallet_address=r.annotator_wallet_address, + annotation_quality=r.annotation_quality, + ) + for r in task_validation_results + ], + ) + + # Include final results for all jobs + job_results: _JobResults = { + job.cvat_id: task_validation_results[ + validation_result_id_to_meta_id[job_final_result_ids[job.id]] + ].annotation_quality + for job in task_jobs + } + + return FinalResult( + job_results=job_results, + validation_meta=validation_meta, + resulting_annotations=merged_annotations.read(), + average_quality=np.mean( + [v for v in job_results.values() if v != UNKNOWN_QUALITY and v >= 0] or [0] + ), + ) diff --git a/packages/examples/cvat/recording-oracle/src/handlers/validation.py b/packages/examples/cvat/recording-oracle/src/handlers/validation/handlers.py similarity index 96% rename from packages/examples/cvat/recording-oracle/src/handlers/validation.py rename to packages/examples/cvat/recording-oracle/src/handlers/validation/handlers.py index 7b20f1daa5..536e4dcbcd 100644 --- a/packages/examples/cvat/recording-oracle/src/handlers/validation.py +++ b/packages/examples/cvat/recording-oracle/src/handlers/validation/handlers.py @@ -22,11 +22,8 @@ from src.core.types import OracleWebhookTypes from src.core.validation_errors import TooFewGtError from src.core.validation_results import ValidationFailure, ValidationSuccess -from src.handlers.process_intermediate_results import ( - parse_annotation_metafile, - process_intermediate_results, - serialize_validation_meta, -) +from src.handlers.validation.intermediate_results import process_intermediate_results +from src.handlers.validation.meta import parse_annotation_metafile, serialize_validation_meta from src.log import ROOT_LOGGER_NAME from src.services.cloud import make_client as make_cloud_client from src.services.cloud.utils import BucketAccessInfo @@ -35,7 +32,7 @@ module_logger_name = f"{ROOT_LOGGER_NAME}.cron.webhook" -class _TaskValidator: +class _EscrowValidator: def __init__( self, escrow_address: str, chain_id: int, manifest: ManifestBase, db_session: Session ) -> None: @@ -181,7 +178,7 @@ def validate_results( manifest = manifest_utils.parse_manifest(escrow.get_escrow_manifest(chain_id, escrow_address)) - validator = _TaskValidator( + validator = _EscrowValidator( escrow_address=escrow_address, chain_id=chain_id, manifest=manifest, db_session=db_session ) validator.set_logger(logger) diff --git a/packages/examples/cvat/recording-oracle/src/handlers/process_intermediate_results.py b/packages/examples/cvat/recording-oracle/src/handlers/validation/intermediate_results.py similarity index 54% rename from packages/examples/cvat/recording-oracle/src/handlers/process_intermediate_results.py rename to packages/examples/cvat/recording-oracle/src/handlers/validation/intermediate_results.py index 5daadf9dbd..ee7dc13724 100644 --- a/packages/examples/cvat/recording-oracle/src/handlers/process_intermediate_results.py +++ b/packages/examples/cvat/recording-oracle/src/handlers/validation/intermediate_results.py @@ -1,406 +1,39 @@ from __future__ import annotations -import io import logging -import os from collections import Counter -from dataclasses import dataclass from functools import cached_property -from pathlib import Path -from tempfile import TemporaryDirectory from typing import TYPE_CHECKING, TypeVar -import datumaro as dm import numpy as np import src.cvat.api_calls as cvat_api import src.models.validation as db_models import src.services.validation as db_service -from src.core.annotation_meta import AnnotationMeta from src.core.config import Config -from src.core.gt_stats import GtKey, GtStats, ValidationFrameStats -from src.core.manifest import get_manifest_task_type -from src.core.tasks import TaskTypes -from src.core.tasks.cvat_formats import DM_DATASET_FORMAT_MAPPING, DM_GT_DATASET_FORMAT_MAPPING -from src.core.validation_errors import ( - DatasetValidationError, - LowAccuracyError, - TooFewGtError, - TooSlowAnnotationError, -) +from src.core.gt_stats import GtKey, ValidationFrameStats +from src.core.validation_errors import TooFewGtError, TooSlowAnnotationError from src.core.validation_meta import JobMeta, ResultMeta, ValidationMeta -from src.core.validation_results import FinalResult, ValidationFailure, ValidationSuccess +from src.core.validation_results import ValidationFailure, ValidationSuccess from src.db.utils import ForUpdateParams -from src.services.cloud import make_client as make_cloud_client -from src.services.cloud.utils import BucketAccessInfo +from src.handlers.validation.common import ( + UNKNOWN_QUALITY, + _HoneypotUpdateResult, + _JobResults, + _ValidationResult, +) +from src.handlers.validation.quality_checkers.factory import create_quality_checker from src.utils import grouped -from src.utils.annotations import ProjectLabels from src.utils.formatting import value_and_percent -from src.utils.zip_archive import extract_zip_archive, write_dir_to_zip_archive if TYPE_CHECKING: from collections.abc import Callable, Sequence from sqlalchemy.orm import Session + from src.core.annotation_meta import AnnotationMeta + from src.core.gt_stats import GtStats from src.core.manifest import ManifestBase - from src.cvat.interface import QualityReportData - -_JobResults = dict[int, float] - -_RejectedJobs = dict[int, DatasetValidationError] - -_HoneypotFrameId = int -_ValidationFrameId = int -_HoneypotFrameToValFrame = dict[_HoneypotFrameId, _ValidationFrameId] -_TaskIdToValidationLayout = dict[int, cvat_api.models.ITaskValidationLayoutRead] -_TaskIdToHoneypotsMapping = dict[int, _HoneypotFrameToValFrame] -_TaskIdToFrameNames = dict[int, list[str]] -_TaskIdToLabels = dict[int, list[str]] - - -@dataclass -class _ValidationResult: - job_results: _JobResults - rejected_jobs: _RejectedJobs - gt_stats: GtStats - task_id_to_val_layout: _TaskIdToValidationLayout - task_id_to_honeypots_mapping: _TaskIdToHoneypotsMapping - task_id_to_frame_names: _TaskIdToFrameNames - task_id_to_labels: _TaskIdToLabels - - -T = TypeVar("T") - - -class _TaskHandler: - def __init__( - self, - escrow_address: str, - chain_id: int, - manifest: ManifestBase, - ) -> None: - self.escrow_address = escrow_address - self.chain_id = chain_id - self.manifest = manifest - - self._temp_dir: Path | None = None - - def _require_field(self, field: T | None) -> T: - assert field is not None - return field - - -class _TaskValidator(_TaskHandler): - UNKNOWN_QUALITY = -1 - "The value to be used when job quality cannot be computed (e.g. no GT images available)" - - def __init__( - self, - escrow_address: str, - chain_id: int, - manifest: ManifestBase, - *, - meta: AnnotationMeta, - gt_stats: GtStats | None = None, - ) -> None: - super().__init__(escrow_address=escrow_address, chain_id=chain_id, manifest=manifest) - - self._gt_stats: GtStats = gt_stats or {} - - self._job_results: _JobResults | None = None - self._rejected_jobs: _RejectedJobs | None = None - - self._temp_dir: Path | None = None - self._meta: AnnotationMeta = meta - - def _validate_jobs(self): - manifest = self._require_field(self.manifest) - meta = self._require_field(self._meta) - if manifest.version != 1: - raise NotImplementedError - - job_results: _JobResults = {} - rejected_jobs: _RejectedJobs = {} - - cvat_task_ids = {job_meta.task_id for job_meta in meta.jobs} - - task_id_to_quality_report_data: dict[int, QualityReportData] = {} - task_id_to_val_layout: dict[int, cvat_api.models.TaskValidationLayoutRead] = {} - task_id_to_honeypots_mapping: dict[int, _HoneypotFrameToValFrame] = {} - - # store sequence of frame names for each task - # task honeypot with frame index matches the sequence[index] - task_id_to_sequence_of_frame_names: dict[int, list[str]] = {} - - task_id_to_labels: dict[int, list[str]] = {} - - min_quality = manifest.validation.min_quality - - job_id_to_quality_report: dict[int, cvat_api.models.QualityReport] = {} - - for cvat_task_id in cvat_task_ids: - # obtain quality report details - task_labels = cvat_api.get_task_labels(cvat_task_id) - task_quality_report = cvat_api.get_task_quality_report(cvat_task_id) - task_quality_report_data = cvat_api.get_quality_report_data(task_quality_report.id) - task_id_to_quality_report_data[cvat_task_id] = task_quality_report_data - - # obtain task validation layout and define honeypots mapping - task_val_layout = cvat_api.get_task_validation_layout(cvat_task_id) - task_honeypot_frame_to_real = { - f: task_val_layout.honeypot_real_frames[idx] - for idx, f in enumerate(task_val_layout.honeypot_frames) - } - task_id_to_val_layout[cvat_task_id] = task_val_layout - task_id_to_honeypots_mapping[cvat_task_id] = task_honeypot_frame_to_real - task_id_to_sequence_of_frame_names[cvat_task_id] = [ - frame.name for frame in cvat_api.get_task_data_meta(cvat_task_id).frames - ] - task_id_to_labels[cvat_task_id] = task_labels - - # obtain quality reports for each job from the task - job_id_to_quality_report.update( - { - quality_report.job_id: quality_report - for quality_report in cvat_api.get_jobs_quality_reports(task_quality_report.id) - } - ) - - # accepted jobs from the previous epochs are not included - for job_meta in meta.jobs: - cvat_job_id = job_meta.job_id - cvat_task_id = job_meta.task_id - - # assess quality of the job's honeypots - task_quality_report_data = task_id_to_quality_report_data[cvat_task_id] - task_frame_names = task_id_to_sequence_of_frame_names[cvat_task_id] - task_honeypots = set(task_id_to_val_layout[cvat_task_id].honeypot_frames) - task_honeypots_mapping = task_id_to_honeypots_mapping[cvat_task_id] - task_labels = task_id_to_labels[cvat_task_id] - - job_honeypots = task_honeypots & set(job_meta.job_frame_range) - if not job_honeypots: - job_results[cvat_job_id] = self.UNKNOWN_QUALITY - rejected_jobs[cvat_job_id] = TooFewGtError - continue - - for honeypot in job_honeypots: - val_frame = task_honeypots_mapping[honeypot] - val_frame_name = task_frame_names[val_frame] - val_frame_key = GtKey(filename=val_frame_name, labels=task_labels) - - result = task_quality_report_data.frame_results[str(honeypot)] - self._gt_stats.setdefault(val_frame_key, ValidationFrameStats()) - self._gt_stats[val_frame_key].accumulated_quality += result.annotations.accuracy - - if result.annotations.accuracy < min_quality: - self._gt_stats[val_frame_key].failed_attempts += 1 - else: - self._gt_stats[val_frame_key].accepted_attempts += 1 - - # assess job quality - job_quality_report = job_id_to_quality_report[cvat_job_id] - - accuracy = job_quality_report.summary.accuracy - - job_results[cvat_job_id] = accuracy - - if accuracy < min_quality: - rejected_jobs[cvat_job_id] = LowAccuracyError() - - for gt_stat in self._gt_stats.values(): - gt_stat.total_uses = max( - gt_stat.total_uses, - gt_stat.failed_attempts + gt_stat.accepted_attempts, - # at the first iteration we have no information on total uses - # from previous iterations, so we derive it from the validation results - ) - - self._job_results = job_results - self._rejected_jobs = rejected_jobs - self._task_id_to_val_layout = task_id_to_val_layout - self._task_id_to_honeypots_mapping = task_id_to_honeypots_mapping - self._task_id_to_sequence_of_frame_names = task_id_to_sequence_of_frame_names - self._task_id_to_labels = task_id_to_labels - - def validate(self) -> _ValidationResult: - with TemporaryDirectory() as tempdir: - self._temp_dir = Path(tempdir) - - self._validate_jobs() - - return _ValidationResult( - job_results=self._require_field(self._job_results), - rejected_jobs=self._require_field(self._rejected_jobs), - gt_stats=self._require_field(self._gt_stats), - task_id_to_val_layout=self._require_field(self._task_id_to_val_layout), - task_id_to_honeypots_mapping=self._require_field(self._task_id_to_honeypots_mapping), - task_id_to_frame_names=self._require_field(self._task_id_to_sequence_of_frame_names), - task_id_to_labels=self._require_field(self._task_id_to_labels), - ) - - -class _TaskAnnotationMerger(_TaskHandler): - def __init__( - self, - escrow_address: str, - chain_id: int, - manifest: ManifestBase, - *, - merged_annotations: io.IOBase, - ) -> None: - super().__init__(escrow_address=escrow_address, chain_id=chain_id, manifest=manifest) - - self._merged_annotations: io.IOBase = merged_annotations - - self._updated_merged_dataset_archive: io.IOBase | None = None - - self._temp_dir: Path | None = None - self._input_gt_dataset: dm.Dataset | None = None - - def _parse_gt_dataset(self, gt_file_data: bytes) -> dm.Dataset: - with TemporaryDirectory() as gt_temp_dir: - gt_filename = os.path.join(gt_temp_dir, "gt_annotations.json") - with open(gt_filename, "wb") as f: - f.write(gt_file_data) - - gt_dataset = dm.Dataset.import_from( - gt_filename, - format=DM_GT_DATASET_FORMAT_MAPPING[get_manifest_task_type(self.manifest)], - ) - - gt_dataset.init_cache() - - return gt_dataset - - def _load_gt_dataset(self): - input_gt_bucket = BucketAccessInfo.parse_obj(self.manifest.validation.gt_url) - gt_bucket_client = make_cloud_client(input_gt_bucket) - gt_data = gt_bucket_client.download_file(input_gt_bucket.path) - self._input_gt_dataset = self._parse_gt_dataset(gt_data) - - def _restore_original_image_paths(self, merged_dataset: dm.Dataset) -> dm.Dataset: - class RemoveCommonPrefix(dm.ItemTransform): - def __init__(self, extractor: dm.IExtractor, *, prefix: str) -> None: - super().__init__(extractor) - self._prefix = prefix - - def transform_item(self, item: dm.DatasetItem) -> dm.DatasetItem: - if item.id.startswith(self._prefix): - item = item.wrap(id=item.id[len(self._prefix) :]) - return item - - prefix = BucketAccessInfo.parse_obj(self.manifest.data.data_url).path.strip("/\\") + "/" - - # Remove prefixes if it can be done safely - sample_ids = {sample.id for sample in merged_dataset} - if all( - sample_id.startswith(prefix) and (sample_id[len(prefix) :] not in sample_ids) - for sample_id in sample_ids - ): - merged_dataset.transform(RemoveCommonPrefix, prefix=prefix) - - return merged_dataset - - def _prepare_merged_dataset(self): - tempdir = self._require_field(self._temp_dir) - manifest = self._require_field(self.manifest) - merged_annotations = self._require_field(self._merged_annotations) - input_gt_dataset = self._require_field(self._input_gt_dataset) - - merged_dataset_path = tempdir / "merged" - merged_dataset_format = DM_DATASET_FORMAT_MAPPING[get_manifest_task_type(manifest)] - extract_zip_archive(merged_annotations, merged_dataset_path) - - merged_dataset = dm.Dataset.import_from( - os.fspath(merged_dataset_path), format=merged_dataset_format - ) - self._restore_original_image_paths(merged_dataset) - self._put_gt_into_merged_dataset(input_gt_dataset, merged_dataset, manifest=manifest) - - updated_merged_dataset_path = tempdir / "merged_updated" - merged_dataset.export( - updated_merged_dataset_path, merged_dataset_format, save_media=False, reindex=True - ) - - updated_merged_dataset_archive = io.BytesIO() - write_dir_to_zip_archive(updated_merged_dataset_path, updated_merged_dataset_archive) - updated_merged_dataset_archive.seek(0) - - self._updated_merged_dataset_archive = updated_merged_dataset_archive - - @classmethod - def _put_gt_into_merged_dataset( - cls, input_gt_dataset: dm.Dataset, merged_dataset: dm.Dataset, *, manifest: ManifestBase - ) -> None: - """ - Updates the merged dataset inplace, writing GT annotations corresponding to the task type. - """ - - match get_manifest_task_type(manifest): - case TaskTypes.image_boxes.value | TaskTypes.image_polygons.value: - merged_dataset.update(input_gt_dataset) - case TaskTypes.image_points.value: - merged_label_cat: dm.LabelCategories = merged_dataset.categories()[ - dm.AnnotationType.label - ] - - # we support no more than 1 label so far - assert len(manifest.annotation.labels) == 1 - - skeleton_label_id = next( - i for i, label in enumerate(merged_label_cat) if not label.parent - ) - point_label_id = next(i for i, label in enumerate(merged_label_cat) if label.parent) - - for sample in input_gt_dataset: - annotations = [ - dm.Skeleton( - elements=[ - # Put a point in the center of each GT bbox - # Not ideal, but it's the target for now - dm.Points( - [bbox.x + bbox.w / 2, bbox.y + bbox.h / 2], - label=point_label_id, - attributes=bbox.attributes, - ) - ], - label=skeleton_label_id, - ) - for bbox in sample.annotations - if isinstance(bbox, dm.Bbox) - ] - merged_dataset.put(sample.wrap(annotations=annotations)) - case TaskTypes.image_label_binary.value: - merged_dataset.update(input_gt_dataset) - case TaskTypes.image_boxes_from_points: - merged_dataset.update(input_gt_dataset) - case TaskTypes.image_skeletons_from_boxes: - # The original behavior of project_labels is broken for skeletons - input_gt_dataset = dm.Dataset(input_gt_dataset) - input_gt_dataset = input_gt_dataset.transform( - ProjectLabels, dst_labels=merged_dataset.categories()[dm.AnnotationType.label] - ) - merged_dataset.update(input_gt_dataset) - case _: - raise AssertionError(f"Unknown task type {get_manifest_task_type(manifest)}") - - def merge_results(self) -> io.IOBase: - with TemporaryDirectory() as tempdir: - self._temp_dir = Path(tempdir) - - self._load_gt_dataset() - self._prepare_merged_dataset() - - return self._require_field(self._updated_merged_dataset_archive) - - -@dataclass -class _HoneypotUpdateResult: - updated_gt_stats: GtStats - can_continue_annotation: bool - _K = TypeVar("_K") @@ -770,15 +403,15 @@ def process_intermediate_results( # noqa: PLR0912 for gt_stat in db_service.get_task_gt_stats(session, task.id) } - validator = _TaskValidator( - escrow_address=escrow_address, - chain_id=chain_id, - manifest=manifest, + checker = create_quality_checker( + manifest, + escrow_address, + chain_id, meta=unchecked_jobs_meta, gt_stats=gt_stats, ) - validation_result = validator.validate() + validation_result = checker.validate() job_results = validation_result.job_results rejected_jobs = validation_result.rejected_jobs @@ -926,112 +559,6 @@ def process_intermediate_results( # noqa: PLR0912 job_results=job_results, validation_meta=validation_meta, average_quality=np.mean( - [v for v in job_results.values() if v != _TaskValidator.UNKNOWN_QUALITY and v >= 0] - or [0] + [v for v in job_results.values() if v != UNKNOWN_QUALITY and v >= 0] or [0] ), ) - - -def process_final_results( - session: Session, - *, - escrow_address: str, - chain_id: int, - meta: AnnotationMeta, - merged_annotations: io.RawIOBase, - manifest: ManifestBase, - logger: logging.Logger, -) -> FinalResult: - assert logger # unused - - task = db_service.get_task_by_escrow_address( - session, - escrow_address, - for_update=ForUpdateParams( - nowait=True - ), # should not happen, but waiting should not block processing - ) - if not task: - raise AssertionError(f"Validation results for escrow {escrow_address} not found") - - merger = _TaskAnnotationMerger( - escrow_address=escrow_address, - chain_id=chain_id, - manifest=manifest, - merged_annotations=merged_annotations, - ) - - merged_annotations = merger.merge_results() - - job_final_result_ids: dict[str, str] = {} - - for job_meta in meta.jobs: - job = db_service.get_job_by_cvat_id(session, job_meta.job_id) - if not job: - raise AssertionError( - f"Can't find validation results for job " f"{job_meta.job_id} ({escrow_address=})" - ) - - assignment_validation_result = db_service.get_validation_result_by_assignment_id( - session, job_meta.assignment_id - ) - if not assignment_validation_result: - raise AssertionError( - f"Can't find validation results for assignments " - f"{job_meta.assignment_id} ({escrow_address=})" - ) - - job_final_result_ids[job.id] = assignment_validation_result.id - - task_jobs = task.jobs - - task_validation_results = db_service.get_task_validation_results(session, task.id) - - job_id_to_meta_id = {job.id: i for i, job in enumerate(task_jobs)} - - validation_result_id_to_meta_id = {r.id: i for i, r in enumerate(task_validation_results)} - - validation_meta = ValidationMeta( - jobs=[ - JobMeta( - job_id=job_id_to_meta_id[job.id], - final_result_id=validation_result_id_to_meta_id[job_final_result_ids[job.id]], - ) - for job in task_jobs - ], - results=[ - ResultMeta( - id=validation_result_id_to_meta_id[r.id], - job_id=job_id_to_meta_id[r.job.id], - annotator_wallet_address=r.annotator_wallet_address, - annotation_quality=r.annotation_quality, - ) - for r in task_validation_results - ], - ) - - # Include final results for all jobs - job_results: _JobResults = { - job.cvat_id: task_validation_results[ - validation_result_id_to_meta_id[job_final_result_ids[job.id]] - ].annotation_quality - for job in task_jobs - } - - return FinalResult( - job_results=job_results, - validation_meta=validation_meta, - resulting_annotations=merged_annotations.read(), - average_quality=np.mean( - [v for v in job_results.values() if v != _TaskValidator.UNKNOWN_QUALITY and v >= 0] - or [0] - ), - ) - - -def parse_annotation_metafile(metafile: io.RawIOBase) -> AnnotationMeta: - return AnnotationMeta.model_validate_json(metafile.read()) - - -def serialize_validation_meta(validation_meta: ValidationMeta) -> bytes: - return validation_meta.model_dump_json().encode() diff --git a/packages/examples/cvat/recording-oracle/src/handlers/validation/meta.py b/packages/examples/cvat/recording-oracle/src/handlers/validation/meta.py new file mode 100644 index 0000000000..fd2d87b834 --- /dev/null +++ b/packages/examples/cvat/recording-oracle/src/handlers/validation/meta.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from src.core.annotation_meta import AnnotationMeta + +if TYPE_CHECKING: + import io + + from src.core.validation_meta import ValidationMeta + + +def parse_annotation_metafile(metafile: io.RawIOBase) -> AnnotationMeta: + return AnnotationMeta.model_validate_json(metafile.read()) + + +def serialize_validation_meta(validation_meta: ValidationMeta) -> bytes: + return validation_meta.model_dump_json().encode() diff --git a/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/__init__.py b/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/audio.py b/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/audio.py new file mode 100644 index 0000000000..0e85ddd8c3 --- /dev/null +++ b/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/audio.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from src.handlers.validation.quality_checkers.base import TaskQualityChecker + + +class AudioTaskQualityChecker(TaskQualityChecker): + def _validate_jobs(self) -> None: + raise NotImplementedError("audio_transcription validation not implemented yet") diff --git a/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/base.py b/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/base.py new file mode 100644 index 0000000000..f029628eb2 --- /dev/null +++ b/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/base.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +from abc import ABCMeta, abstractmethod +from pathlib import Path +from tempfile import TemporaryDirectory +from typing import TYPE_CHECKING + +from src.handlers.validation.common import _TaskHandler, _ValidationResult + +if TYPE_CHECKING: + from src.core.annotation_meta import AnnotationMeta + from src.core.gt_stats import GtStats + from src.core.manifest import ManifestBase + from src.handlers.validation.common import _JobResults, _RejectedJobs + + +class TaskQualityChecker(_TaskHandler, metaclass=ABCMeta): + def __init__( + self, + escrow_address: str, + chain_id: int, + manifest: ManifestBase, + *, + meta: AnnotationMeta, + gt_stats: GtStats | None = None, + ) -> None: + super().__init__(escrow_address=escrow_address, chain_id=chain_id, manifest=manifest) + + self._gt_stats: GtStats = gt_stats or {} + + self._job_results: _JobResults | None = None + self._rejected_jobs: _RejectedJobs | None = None + + self._temp_dir: Path | None = None + self._meta: AnnotationMeta = meta + + @abstractmethod + def _validate_jobs(self) -> None: + """ + Compute per-job quality and populate: + ``_job_results``, ``_rejected_jobs``, ``_gt_stats``, ``_task_id_to_val_layout``, + ``_task_id_to_honeypots_mapping``, ``_task_id_to_sequence_of_frame_names``, + ``_task_id_to_labels``. + """ + + def validate(self) -> _ValidationResult: + with TemporaryDirectory() as tempdir: + self._temp_dir = Path(tempdir) + + self._validate_jobs() + + return _ValidationResult( + job_results=self._require_field(self._job_results), + rejected_jobs=self._require_field(self._rejected_jobs), + gt_stats=self._require_field(self._gt_stats), + task_id_to_val_layout=self._require_field(self._task_id_to_val_layout), + task_id_to_honeypots_mapping=self._require_field(self._task_id_to_honeypots_mapping), + task_id_to_frame_names=self._require_field(self._task_id_to_sequence_of_frame_names), + task_id_to_labels=self._require_field(self._task_id_to_labels), + ) diff --git a/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/factory.py b/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/factory.py new file mode 100644 index 0000000000..a6d012304e --- /dev/null +++ b/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/factory.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from src.core.manifest import get_manifest_task_type +from src.core.tasks import TaskTypes +from src.handlers.validation.quality_checkers.audio import AudioTaskQualityChecker +from src.handlers.validation.quality_checkers.image import ImageTaskQualityChecker + +if TYPE_CHECKING: + from src.core.annotation_meta import AnnotationMeta + from src.core.gt_stats import GtStats + from src.core.manifest import ManifestBase + from src.handlers.validation.quality_checkers.base import TaskQualityChecker + + +def create_quality_checker( + manifest: ManifestBase, + escrow_address: str, + chain_id: int, + *, + meta: AnnotationMeta, + gt_stats: GtStats | None = None, +) -> TaskQualityChecker: + task_type = get_manifest_task_type(manifest) + + match task_type: + case TaskTypes.audio_transcription: + checker_type = AudioTaskQualityChecker + case ( + TaskTypes.image_label_binary + | TaskTypes.image_boxes + | TaskTypes.image_polygons + | TaskTypes.image_points + | TaskTypes.image_boxes_from_points + | TaskTypes.image_skeletons_from_boxes + ): + checker_type = ImageTaskQualityChecker + case _: + raise NotImplementedError(f"Unsupported task type '{task_type}'") + + return checker_type(escrow_address, chain_id, manifest, meta=meta, gt_stats=gt_stats) diff --git a/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/image.py b/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/image.py new file mode 100644 index 0000000000..337480d934 --- /dev/null +++ b/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/image.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import src.cvat.api_calls as cvat_api +from src.core.gt_stats import GtKey, ValidationFrameStats +from src.core.validation_errors import LowAccuracyError, TooFewGtError +from src.handlers.validation.common import UNKNOWN_QUALITY +from src.handlers.validation.quality_checkers.base import TaskQualityChecker + +if TYPE_CHECKING: + from src.cvat.interface import QualityReportData + from src.handlers.validation.common import _HoneypotFrameToValFrame, _JobResults, _RejectedJobs + + +class ImageTaskQualityChecker(TaskQualityChecker): + def _validate_jobs(self) -> None: + manifest = self._require_field(self.manifest) + meta = self._require_field(self._meta) + if manifest.version != 1: + raise NotImplementedError + + job_results: _JobResults = {} + rejected_jobs: _RejectedJobs = {} + + cvat_task_ids = {job_meta.task_id for job_meta in meta.jobs} + + task_id_to_quality_report_data: dict[int, QualityReportData] = {} + task_id_to_val_layout: dict[int, cvat_api.models.TaskValidationLayoutRead] = {} + task_id_to_honeypots_mapping: dict[int, _HoneypotFrameToValFrame] = {} + + # store sequence of frame names for each task + # task honeypot with frame index matches the sequence[index] + task_id_to_sequence_of_frame_names: dict[int, list[str]] = {} + + task_id_to_labels: dict[int, list[str]] = {} + + min_quality = manifest.validation.min_quality + + job_id_to_quality_report: dict[int, cvat_api.models.QualityReport] = {} + + for cvat_task_id in cvat_task_ids: + # obtain quality report details + task_labels = cvat_api.get_task_labels(cvat_task_id) + task_quality_report = cvat_api.get_task_quality_report(cvat_task_id) + task_quality_report_data = cvat_api.get_quality_report_data(task_quality_report.id) + task_id_to_quality_report_data[cvat_task_id] = task_quality_report_data + + # obtain task validation layout and define honeypots mapping + task_val_layout = cvat_api.get_task_validation_layout(cvat_task_id) + task_honeypot_frame_to_real = { + f: task_val_layout.honeypot_real_frames[idx] + for idx, f in enumerate(task_val_layout.honeypot_frames) + } + task_id_to_val_layout[cvat_task_id] = task_val_layout + task_id_to_honeypots_mapping[cvat_task_id] = task_honeypot_frame_to_real + task_id_to_sequence_of_frame_names[cvat_task_id] = [ + frame.name for frame in cvat_api.get_task_data_meta(cvat_task_id).frames + ] + task_id_to_labels[cvat_task_id] = task_labels + + # obtain quality reports for each job from the task + job_id_to_quality_report.update( + { + quality_report.job_id: quality_report + for quality_report in cvat_api.get_jobs_quality_reports(task_quality_report.id) + } + ) + + # accepted jobs from the previous epochs are not included + for job_meta in meta.jobs: + cvat_job_id = job_meta.job_id + cvat_task_id = job_meta.task_id + + # assess quality of the job's honeypots + task_quality_report_data = task_id_to_quality_report_data[cvat_task_id] + task_frame_names = task_id_to_sequence_of_frame_names[cvat_task_id] + task_honeypots = set(task_id_to_val_layout[cvat_task_id].honeypot_frames) + task_honeypots_mapping = task_id_to_honeypots_mapping[cvat_task_id] + task_labels = task_id_to_labels[cvat_task_id] + + job_honeypots = task_honeypots & set(job_meta.job_frame_range) + if not job_honeypots: + job_results[cvat_job_id] = UNKNOWN_QUALITY + rejected_jobs[cvat_job_id] = TooFewGtError + continue + + for honeypot in job_honeypots: + val_frame = task_honeypots_mapping[honeypot] + val_frame_name = task_frame_names[val_frame] + val_frame_key = GtKey(filename=val_frame_name, labels=task_labels) + + result = task_quality_report_data.frame_results[str(honeypot)] + self._gt_stats.setdefault(val_frame_key, ValidationFrameStats()) + self._gt_stats[val_frame_key].accumulated_quality += result.annotations.accuracy + + if result.annotations.accuracy < min_quality: + self._gt_stats[val_frame_key].failed_attempts += 1 + else: + self._gt_stats[val_frame_key].accepted_attempts += 1 + + # assess job quality + job_quality_report = job_id_to_quality_report[cvat_job_id] + + accuracy = job_quality_report.summary.accuracy + + job_results[cvat_job_id] = accuracy + + if accuracy < min_quality: + rejected_jobs[cvat_job_id] = LowAccuracyError() + + for gt_stat in self._gt_stats.values(): + gt_stat.total_uses = max( + gt_stat.total_uses, + gt_stat.failed_attempts + gt_stat.accepted_attempts, + # at the first iteration we have no information on total uses + # from previous iterations, so we derive it from the validation results + ) + + self._job_results = job_results + self._rejected_jobs = rejected_jobs + self._task_id_to_val_layout = task_id_to_val_layout + self._task_id_to_honeypots_mapping = task_id_to_honeypots_mapping + self._task_id_to_sequence_of_frame_names = task_id_to_sequence_of_frame_names + self._task_id_to_labels = task_id_to_labels diff --git a/packages/examples/cvat/recording-oracle/tests/integration/services/test_validation_service.py b/packages/examples/cvat/recording-oracle/tests/integration/services/test_validation_service.py index e901f89c14..13453cc81b 100644 --- a/packages/examples/cvat/recording-oracle/tests/integration/services/test_validation_service.py +++ b/packages/examples/cvat/recording-oracle/tests/integration/services/test_validation_service.py @@ -24,7 +24,7 @@ from src.core.validation_results import ValidationFailure, ValidationSuccess from src.cvat import api_calls as cvat_api from src.db import SessionLocal -from src.handlers.process_intermediate_results import ( +from src.handlers.validation import ( process_final_results, process_intermediate_results, ) @@ -129,7 +129,7 @@ def test_can_handle_lowered_quality_requirements_in_manifest(self, session: Sess logger = mock.Mock(Logger) common_lock_es.enter_context( - mock.patch("src.handlers.process_intermediate_results.BucketAccessInfo.parse_obj") + mock.patch("src.handlers.validation.final_results.BucketAccessInfo.parse_obj") ) common_lock_es.enter_context( @@ -137,13 +137,13 @@ def test_can_handle_lowered_quality_requirements_in_manifest(self, session: Sess ) mock_make_cloud_client = common_lock_es.enter_context( - mock.patch("src.handlers.process_intermediate_results.make_cloud_client") + mock.patch("src.handlers.validation.final_results.make_cloud_client") ) mock_make_cloud_client.return_value.download_file = mock.Mock(return_value=b"") mock_get_task_validation_layout = common_lock_es.enter_context( mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_task_validation_layout" + "src.handlers.validation.quality_checkers.image.cvat_api.get_task_validation_layout" ) ) mock_get_task_validation_layout.return_value = mock.Mock( @@ -156,7 +156,7 @@ def test_can_handle_lowered_quality_requirements_in_manifest(self, session: Sess ) mock_get_task_data_meta = common_lock_es.enter_context( - mock.patch("src.handlers.process_intermediate_results.cvat_api.get_task_data_meta") + mock.patch("src.handlers.validation.quality_checkers.image.cvat_api.get_task_data_meta") ) mock_get_task_data_meta.return_value = mock.Mock( cvat_api.models.IDataMetaRead, @@ -164,7 +164,7 @@ def test_can_handle_lowered_quality_requirements_in_manifest(self, session: Sess ) mock_get_task_labels = common_lock_es.enter_context( - mock.patch("src.handlers.process_intermediate_results.cvat_api.get_task_labels") + mock.patch("src.handlers.validation.quality_checkers.image.cvat_api.get_task_labels") ) mock_get_task_labels.return_value = [label.name for label in manifest.annotation.labels] @@ -183,16 +183,16 @@ def test_can_handle_lowered_quality_requirements_in_manifest(self, session: Sess with ( mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_task_quality_report" + "src.handlers.validation.quality_checkers.image.cvat_api.get_task_quality_report" ) as mock_get_task_quality_report, mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_quality_report_data" + "src.handlers.validation.quality_checkers.image.cvat_api.get_quality_report_data" ) as mock_get_quality_report_data, mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_jobs_quality_reports" + "src.handlers.validation.quality_checkers.image.cvat_api.get_jobs_quality_reports" ) as mock_get_jobs_quality_reports, mock.patch( - "src.handlers.process_intermediate_results.cvat_api.update_task_validation_layout" + "src.handlers.validation.quality_checkers.image.cvat_api.update_task_validation_layout" ), mock.patch("src.core.config.ValidationConfig.min_available_gt_threshold", 0), mock.patch("src.core.config.ValidationConfig.max_gt_share", 1), @@ -233,13 +233,13 @@ def test_can_handle_lowered_quality_requirements_in_manifest(self, session: Sess with ( mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_task_quality_report" + "src.handlers.validation.quality_checkers.image.cvat_api.get_task_quality_report" ) as mock_get_task_quality_report, mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_quality_report_data" + "src.handlers.validation.quality_checkers.image.cvat_api.get_quality_report_data" ) as mock_get_quality_report_data, mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_jobs_quality_reports" + "src.handlers.validation.quality_checkers.image.cvat_api.get_jobs_quality_reports" ) as mock_get_jobs_quality_reports, ): mock_get_task_quality_report.return_value = mock.Mock( @@ -333,17 +333,17 @@ def test_can_change_bad_honeypots_in_jobs(self, session: Session, seed: int): logger = mock.Mock(Logger) common_lock_es.enter_context( - mock.patch("src.handlers.process_intermediate_results.BucketAccessInfo.parse_obj") + mock.patch("src.handlers.validation.final_results.BucketAccessInfo.parse_obj") ) mock_make_cloud_client = common_lock_es.enter_context( - mock.patch("src.handlers.process_intermediate_results.make_cloud_client") + mock.patch("src.handlers.validation.final_results.make_cloud_client") ) mock_make_cloud_client.return_value.download_file = mock.Mock(return_value=b"") mock_get_task_validation_layout = common_lock_es.enter_context( mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_task_validation_layout" + "src.handlers.validation.quality_checkers.image.cvat_api.get_task_validation_layout" ) ) mock_get_task_validation_layout.return_value = mock.Mock( @@ -357,7 +357,7 @@ def test_can_change_bad_honeypots_in_jobs(self, session: Session, seed: int): ) mock_get_task_data_meta = common_lock_es.enter_context( - mock.patch("src.handlers.process_intermediate_results.cvat_api.get_task_data_meta") + mock.patch("src.handlers.validation.quality_checkers.image.cvat_api.get_task_data_meta") ) mock_get_task_data_meta.return_value = mock.Mock( cvat_api.models.IDataMetaRead, @@ -365,7 +365,7 @@ def test_can_change_bad_honeypots_in_jobs(self, session: Session, seed: int): ) mock_get_task_labels = common_lock_es.enter_context( - mock.patch("src.handlers.process_intermediate_results.cvat_api.get_task_labels") + mock.patch("src.handlers.validation.quality_checkers.image.cvat_api.get_task_labels") ) mock_get_task_labels.return_value = [label.name for label in manifest.annotation.labels] @@ -388,16 +388,16 @@ def test_can_change_bad_honeypots_in_jobs(self, session: Session, seed: int): with ( mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_task_quality_report" + "src.handlers.validation.quality_checkers.image.cvat_api.get_task_quality_report" ) as mock_get_task_quality_report, mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_quality_report_data" + "src.handlers.validation.quality_checkers.image.cvat_api.get_quality_report_data" ) as mock_get_quality_report_data, mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_jobs_quality_reports" + "src.handlers.validation.quality_checkers.image.cvat_api.get_jobs_quality_reports" ) as mock_get_jobs_quality_reports, mock.patch( - "src.handlers.process_intermediate_results.cvat_api.update_task_validation_layout" + "src.handlers.validation.quality_checkers.image.cvat_api.update_task_validation_layout" ) as mock_update_task_validation_layout, mock.patch("src.core.config.ValidationConfig.min_available_gt_threshold", 0), mock.patch("src.core.config.ValidationConfig.max_gt_share", 1), @@ -568,17 +568,17 @@ def test_can_stop_on_slow_annotation_after_warmup_iterations(self, session: Sess logger = mock.Mock(Logger) common_lock_es.enter_context( - mock.patch("src.handlers.process_intermediate_results.BucketAccessInfo.parse_obj") + mock.patch("src.handlers.validation.final_results.BucketAccessInfo.parse_obj") ) mock_make_cloud_client = common_lock_es.enter_context( - mock.patch("src.handlers.process_intermediate_results.make_cloud_client") + mock.patch("src.handlers.validation.final_results.make_cloud_client") ) mock_make_cloud_client.return_value.download_file = mock.Mock(return_value=b"") mock_get_task_validation_layout = common_lock_es.enter_context( mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_task_validation_layout" + "src.handlers.validation.quality_checkers.image.cvat_api.get_task_validation_layout" ) ) mock_get_task_validation_layout.return_value = mock.Mock( @@ -591,7 +591,7 @@ def test_can_stop_on_slow_annotation_after_warmup_iterations(self, session: Sess ) mock_get_task_data_meta = common_lock_es.enter_context( - mock.patch("src.handlers.process_intermediate_results.cvat_api.get_task_data_meta") + mock.patch("src.handlers.validation.quality_checkers.image.cvat_api.get_task_data_meta") ) mock_get_task_data_meta.return_value = mock.Mock( cvat_api.models.IDataMetaRead, @@ -599,7 +599,7 @@ def test_can_stop_on_slow_annotation_after_warmup_iterations(self, session: Sess ) mock_get_task_labels = common_lock_es.enter_context( - mock.patch("src.handlers.process_intermediate_results.cvat_api.get_task_labels") + mock.patch("src.handlers.validation.quality_checkers.image.cvat_api.get_task_labels") ) mock_get_task_labels.return_value = [label.name for label in manifest.annotation.labels] @@ -618,16 +618,16 @@ def test_can_stop_on_slow_annotation_after_warmup_iterations(self, session: Sess with ( mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_task_quality_report" + "src.handlers.validation.quality_checkers.image.cvat_api.get_task_quality_report" ) as mock_get_task_quality_report, mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_quality_report_data" + "src.handlers.validation.quality_checkers.image.cvat_api.get_quality_report_data" ) as mock_get_quality_report_data, mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_jobs_quality_reports" + "src.handlers.validation.quality_checkers.image.cvat_api.get_jobs_quality_reports" ) as mock_get_jobs_quality_reports, mock.patch( - "src.handlers.process_intermediate_results.cvat_api.update_task_validation_layout" + "src.handlers.validation.quality_checkers.image.cvat_api.update_task_validation_layout" ), mock.patch("src.core.config.ValidationConfig.min_available_gt_threshold", 0), mock.patch("src.core.config.ValidationConfig.max_gt_share", 1), @@ -685,18 +685,18 @@ def test_can_exclude_bad_gt_for_each_label_separately(self, session: Session): logger = mock.Mock(Logger) common_lock_es.enter_context( - mock.patch("src.handlers.process_intermediate_results.BucketAccessInfo.parse_obj") + mock.patch("src.handlers.validation.final_results.BucketAccessInfo.parse_obj") ) mock_make_cloud_client = common_lock_es.enter_context( - mock.patch("src.handlers.process_intermediate_results.make_cloud_client") + mock.patch("src.handlers.validation.final_results.make_cloud_client") ) mock_make_cloud_client.return_value.download_file = mock.Mock(return_value=b"") # All tasks have the same validation layout mock_get_task_validation_layout = common_lock_es.enter_context( mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_task_validation_layout", + "src.handlers.validation.quality_checkers.image.cvat_api.get_task_validation_layout", ) ) mock_get_task_validation_layout.return_value = mock.Mock( @@ -710,7 +710,7 @@ def test_can_exclude_bad_gt_for_each_label_separately(self, session: Session): # All tasks have the same frames mock_get_task_data_meta = common_lock_es.enter_context( - mock.patch("src.handlers.process_intermediate_results.cvat_api.get_task_data_meta") + mock.patch("src.handlers.validation.quality_checkers.image.cvat_api.get_task_data_meta") ) mock_get_task_data_meta.return_value = mock.Mock( cvat_api.models.IDataMetaRead, @@ -722,7 +722,7 @@ def patched_get_task_labels(task_id: int): common_lock_es.enter_context( mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_task_labels", + "src.handlers.validation.quality_checkers.image.cvat_api.get_task_labels", patched_get_task_labels, ) ) @@ -782,19 +782,19 @@ def patched_get_jobs_quality_reports(task_id: int): with ( mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_task_quality_report", + "src.handlers.validation.quality_checkers.image.cvat_api.get_task_quality_report", side_effect=patched_get_task_quality_report, ) as mock_get_task_quality_report, mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_quality_report_data", + "src.handlers.validation.quality_checkers.image.cvat_api.get_quality_report_data", patched_get_quality_report_data, ), mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_jobs_quality_reports", + "src.handlers.validation.quality_checkers.image.cvat_api.get_jobs_quality_reports", patched_get_jobs_quality_reports, ), mock.patch( - "src.handlers.process_intermediate_results.cvat_api.update_task_validation_layout" + "src.handlers.validation.quality_checkers.image.cvat_api.update_task_validation_layout" ) as mock_update_task_validation_layout, mock.patch("src.core.config.ValidationConfig.min_available_gt_threshold", 0), mock.patch("src.core.config.ValidationConfig.max_gt_share", 1), @@ -866,18 +866,18 @@ def test_can_complete_if_not_enough_gt_left_in_task_with_several_jobs( logger = logging.getLogger() common_lock_es.enter_context( - mock.patch("src.handlers.process_intermediate_results.BucketAccessInfo.parse_obj") + mock.patch("src.handlers.validation.final_results.BucketAccessInfo.parse_obj") ) mock_make_cloud_client = common_lock_es.enter_context( - mock.patch("src.handlers.process_intermediate_results.make_cloud_client") + mock.patch("src.handlers.validation.final_results.make_cloud_client") ) mock_make_cloud_client.return_value.download_file = mock.Mock(return_value=b"") # All tasks have the same validation layout mock_get_task_validation_layout = common_lock_es.enter_context( mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_task_validation_layout", + "src.handlers.validation.quality_checkers.image.cvat_api.get_task_validation_layout", ) ) mock_get_task_validation_layout.return_value = mock.Mock( @@ -891,7 +891,7 @@ def test_can_complete_if_not_enough_gt_left_in_task_with_several_jobs( # All tasks have the same frames mock_get_task_data_meta = common_lock_es.enter_context( - mock.patch("src.handlers.process_intermediate_results.cvat_api.get_task_data_meta") + mock.patch("src.handlers.validation.quality_checkers.image.cvat_api.get_task_data_meta") ) mock_get_task_data_meta.return_value = mock.Mock( cvat_api.models.IDataMetaRead, @@ -903,7 +903,7 @@ def patched_get_task_labels(task_id: int): common_lock_es.enter_context( mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_task_labels", + "src.handlers.validation.quality_checkers.image.cvat_api.get_task_labels", patched_get_task_labels, ) ) @@ -963,19 +963,19 @@ def patched_get_jobs_quality_reports(task_id: int): with ( mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_task_quality_report", + "src.handlers.validation.quality_checkers.image.cvat_api.get_task_quality_report", side_effect=patched_get_task_quality_report, ), mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_quality_report_data", + "src.handlers.validation.quality_checkers.image.cvat_api.get_quality_report_data", patched_get_quality_report_data, ), mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_jobs_quality_reports", + "src.handlers.validation.quality_checkers.image.cvat_api.get_jobs_quality_reports", patched_get_jobs_quality_reports, ), mock.patch( - "src.handlers.process_intermediate_results.cvat_api.update_task_validation_layout" + "src.handlers.validation.quality_checkers.image.cvat_api.update_task_validation_layout" ) as mock_update_task_validation_layout, mock.patch("src.core.config.ValidationConfig.min_available_gt_threshold", 0.7), mock.patch("src.core.config.ValidationConfig.max_gt_share", 1), @@ -1020,18 +1020,18 @@ def test_can_complete_if_not_enough_gt_left_in_task_with_one_job( logger = logging.getLogger() common_lock_es.enter_context( - mock.patch("src.handlers.process_intermediate_results.BucketAccessInfo.parse_obj") + mock.patch("src.handlers.validation.final_results.BucketAccessInfo.parse_obj") ) mock_make_cloud_client = common_lock_es.enter_context( - mock.patch("src.handlers.process_intermediate_results.make_cloud_client") + mock.patch("src.handlers.validation.final_results.make_cloud_client") ) mock_make_cloud_client.return_value.download_file = mock.Mock(return_value=b"") # All tasks have the same validation layout mock_get_task_validation_layout = common_lock_es.enter_context( mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_task_validation_layout", + "src.handlers.validation.quality_checkers.image.cvat_api.get_task_validation_layout", ) ) mock_get_task_validation_layout.return_value = mock.Mock( @@ -1045,7 +1045,7 @@ def test_can_complete_if_not_enough_gt_left_in_task_with_one_job( # All tasks have the same frames mock_get_task_data_meta = common_lock_es.enter_context( - mock.patch("src.handlers.process_intermediate_results.cvat_api.get_task_data_meta") + mock.patch("src.handlers.validation.quality_checkers.image.cvat_api.get_task_data_meta") ) mock_get_task_data_meta.return_value = mock.Mock( cvat_api.models.IDataMetaRead, @@ -1057,7 +1057,7 @@ def patched_get_task_labels(task_id: int): common_lock_es.enter_context( mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_task_labels", + "src.handlers.validation.quality_checkers.image.cvat_api.get_task_labels", patched_get_task_labels, ) ) @@ -1101,19 +1101,19 @@ def patched_get_jobs_quality_reports(task_id: int): with ( mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_task_quality_report", + "src.handlers.validation.quality_checkers.image.cvat_api.get_task_quality_report", side_effect=patched_get_task_quality_report, ), mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_quality_report_data", + "src.handlers.validation.quality_checkers.image.cvat_api.get_quality_report_data", patched_get_quality_report_data, ), mock.patch( - "src.handlers.process_intermediate_results.cvat_api.get_jobs_quality_reports", + "src.handlers.validation.quality_checkers.image.cvat_api.get_jobs_quality_reports", patched_get_jobs_quality_reports, ), mock.patch( - "src.handlers.process_intermediate_results.cvat_api.update_task_validation_layout" + "src.handlers.validation.quality_checkers.image.cvat_api.update_task_validation_layout" ) as mock_update_task_validation_layout, mock.patch("src.core.config.ValidationConfig.min_available_gt_threshold", 0), mock.patch("src.core.config.ValidationConfig.max_gt_share", 1), @@ -1174,15 +1174,15 @@ def patched_prepare_merged_dataset(self): self._updated_merged_dataset_archive = io.BytesIO(b"test") with ( - mock.patch("src.handlers.process_intermediate_results.BucketAccessInfo.parse_obj"), + mock.patch("src.handlers.validation.final_results.BucketAccessInfo.parse_obj"), mock.patch( - "src.handlers.process_intermediate_results.make_cloud_client" + "src.handlers.validation.final_results.make_cloud_client" ) as mock_make_cloud_client, - mock.patch("src.handlers.process_intermediate_results.dm.Dataset.import_from"), - mock.patch("src.handlers.process_intermediate_results.extract_zip_archive"), - mock.patch("src.handlers.process_intermediate_results.write_dir_to_zip_archive"), + mock.patch("src.handlers.validation.final_results.dm.Dataset.import_from"), + mock.patch("src.handlers.validation.final_results.extract_zip_archive"), + mock.patch("src.handlers.validation.final_results.write_dir_to_zip_archive"), mock.patch( - "src.handlers.process_intermediate_results._TaskAnnotationMerger._prepare_merged_dataset", + "src.handlers.validation.final_results._TaskAnnotationMerger._prepare_merged_dataset", patched_prepare_merged_dataset, ), ): From 5725e0f474b16afdcc7581150c482bfdbc3f20e4 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Tue, 7 Jul 2026 19:01:22 +0300 Subject: [PATCH 24/53] add audio qe library --- .../examples/cvat/recording-oracle/Dockerfile | 2 + .../recording-oracle/libs/audio_qe/LICENSE | 21 + .../recording-oracle/libs/audio_qe/README.md | 67 + .../libs/audio_qe/audio_qe/__init__.py | 102 ++ .../libs/audio_qe/audio_qe/config.py | 90 ++ .../libs/audio_qe/audio_qe/data.py | 84 ++ .../audio_qe/audio_qe/interval_matching.py | 215 ++++ .../libs/audio_qe/audio_qe/normalization.py | 553 ++++++++ .../libs/audio_qe/audio_qe/pipeline.py | 24 + .../libs/audio_qe/audio_qe/reports.py | 165 +++ .../audio_qe/transcription_matching.py | 1116 +++++++++++++++++ .../libs/audio_qe/pyproject.toml | 18 + .../cvat/recording-oracle/pyproject.toml | 1 + 13 files changed, 2458 insertions(+) create mode 100644 packages/examples/cvat/recording-oracle/libs/audio_qe/LICENSE create mode 100644 packages/examples/cvat/recording-oracle/libs/audio_qe/README.md create mode 100644 packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/__init__.py create mode 100644 packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/config.py create mode 100644 packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/data.py create mode 100644 packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/interval_matching.py create mode 100644 packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/normalization.py create mode 100644 packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/pipeline.py create mode 100644 packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/reports.py create mode 100644 packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/transcription_matching.py create mode 100644 packages/examples/cvat/recording-oracle/libs/audio_qe/pyproject.toml diff --git a/packages/examples/cvat/recording-oracle/Dockerfile b/packages/examples/cvat/recording-oracle/Dockerfile index bdc0dd597c..db360bf074 100644 --- a/packages/examples/cvat/recording-oracle/Dockerfile +++ b/packages/examples/cvat/recording-oracle/Dockerfile @@ -8,6 +8,8 @@ RUN apt-get update -y && \ RUN pip install --no-cache 'poetry==1.8.5' +COPY libs ./libs + COPY pyproject.toml poetry.lock ./ RUN --mount=type=cache,target=/root/.cache \ diff --git a/packages/examples/cvat/recording-oracle/libs/audio_qe/LICENSE b/packages/examples/cvat/recording-oracle/libs/audio_qe/LICENSE new file mode 100644 index 0000000000..b17d8e9073 --- /dev/null +++ b/packages/examples/cvat/recording-oracle/libs/audio_qe/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (C) CVAT.ai Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/examples/cvat/recording-oracle/libs/audio_qe/README.md b/packages/examples/cvat/recording-oracle/libs/audio_qe/README.md new file mode 100644 index 0000000000..d1fba3a6f1 --- /dev/null +++ b/packages/examples/cvat/recording-oracle/libs/audio_qe/README.md @@ -0,0 +1,67 @@ +# audio-qe + +Audio transcription quality estimation library for Recording Oracle. + +## Install + +Consumed by the recording oracle as an editable path dependency: + +```toml +audio-qe = {path = "libs/audio_qe", develop = true} +``` + +## Usage + +Inputs are `Interval` sequences (ground truth vs annotator); the transcription text lives in +`Interval.extra[req.text_attribute]`. + +```python +from audio_qe import Interval, TranscriptionRequirement, match_transcriptions + +gt = [Interval(id=0, start=0, stop=2000, label="speech", extra={"text": "hello world"})] +ds = [Interval(id=1, start=0, stop=2000, label="speech", extra={"text": "hello word"})] +req = TranscriptionRequirement(text_attribute="text") # WER, char-align, basic normalizer, join +report = match_transcriptions(gt, ds, req=req) +report.corpus_rate # aggregate error rate +``` + +Entrypoints: + +- `match_intervals(gs, ds, *, config)` - pairwise interval matching. +- `match_transcriptions(gt, ds, *, requirement)` - transcription matching. +- `compare(gt, ds, *, settings)` - interval and transcription matching. + +### Choosing settings + +`TranscriptionRequirement` has three orthogonal axes plus an optional binarizer: + +- **`granularity`** - the unit errors are reported in. `WORD` → WER; `CHARACTER` → CER + (use for CJK and other scripts without word boundaries). +- **`align`** - how the alignment runs. `CHAR` (default) aligns characters then rebuilds word + edits, so it tolerates word-boundary disagreements (`sunday` vs `sun day`); `WORD` is the classic, + word-level alignment with no boundary credit. +- **`metric`** - per-chunk cost. `EQUALITY` (default) is 0/1, i.e. classic WER/CER; `ERROR_RATE` + is recall-shaped (partial credit for near misses); `NORMALIZED_LEV` is symmetric and bounded + `[0, 1]` (use when neither side is the reference, e.g. inter-annotator agreement). +- **`threshold`** - binarizes a soft metric (`cost > threshold → 1`); no-op with `EQUALITY`. + Under `CHARACTER` granularity all metrics degenerate to equality (characters are atomic). + +**Normalizer presets** (applied to both texts before alignment): + +- `NormalizerConfig(mode=NONE)` - passthrough. +- `NormalizerConfig(mode=BASIC)` - universal Unicode + case + whitespace fold (language-agnostic). +- `lang_preset(code)` - language-specific preset (`BASIC` plus per-language rules). Supported codes: + `en, es, fr, de, it, pt, nl, pl, ru, tr, zh, ja, ko, hi, ar`. Unlisted languages fall back to + `BASIC`. + +`grouping.strategy`: +- `JOIN` (concatenate each group's text, then one alignment - default) +- `FILTER` (pair intervals by IoU first). + +Recommended default for annotator-vs-GT review: +`granularity=WORD, align=CHAR, metric=EQUALITY` - boundary-tolerant WER. + +## License + +MIT. Ported from [CVAT](https://github.com/cvat-ai/cvat). See [LICENSE](LICENSE); source files +retain their `SPDX-License-Identifier: MIT`. diff --git a/packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/__init__.py b/packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/__init__.py new file mode 100644 index 0000000000..965ca81eed --- /dev/null +++ b/packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/__init__.py @@ -0,0 +1,102 @@ +# Copyright (C) CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +"""Public surface of the audio QE pipeline. + +Domain types, settings, reports, and algorithms are defined in the +thematic sub-modules; this file re-exports the names callers most +often need so they can write `from cvat.apps.quality_control import +audio` and then access everything via `audio.X`. +""" + +from .config import ( + GroupingConfig, + IntervalMatchingConfig, + NormalizerConfig, + QualitySettings, + StepConfig, + TranscriptionRequirement, +) +from .data import ( + AlignMode, + EditOp, + Granularity, + GroupingStrategy, + GroupKey, + Interval, + Metric, + NormalizerMode, +) +from .interval_matching import match_intervals +from .normalization import ( + BASIC_STACK, + LANG_PRESETS, + SUPPORTED_LANGS, + Normalizer, + lang_preset, + register_step, + step_available, +) +from .pipeline import compare +from .reports import ( + AlignmentEdit, + AlignmentResult, + BoundaryAgreement, + ComparisonReport, + FilterPairAlignment, + GroupAlignment, + IntervalPairMetrics, + IntervalReport, + TranscriptionReport, +) +from .transcription_matching import match_transcriptions + +__all__ = [ + # data + "Interval", + "Granularity", + "GroupKey", + "AlignMode", + "EditOp", + "GroupingStrategy", + "Metric", + "NormalizerMode", + # + # config + "StepConfig", + "NormalizerConfig", + "GroupingConfig", + "TranscriptionRequirement", + "IntervalMatchingConfig", + "QualitySettings", + # + # reports + "AlignmentEdit", + "AlignmentResult", + "BoundaryAgreement", + "ComparisonReport", + "FilterPairAlignment", + "GroupAlignment", + "IntervalPairMetrics", + "IntervalReport", + "TranscriptionReport", + # + # normalization + "Normalizer", + "BASIC_STACK", + "LANG_PRESETS", + "SUPPORTED_LANGS", + "lang_preset", + "register_step", + "step_available", + # + # interval matching + "match_intervals", + # + # transcription matching + "match_transcriptions", + # + # top-level + "compare", +] diff --git a/packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/config.py b/packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/config.py new file mode 100644 index 0000000000..79dcc19b40 --- /dev/null +++ b/packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/config.py @@ -0,0 +1,90 @@ +# Copyright (C) CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any + +from .data import AlignMode, Granularity, GroupingStrategy, Metric, NormalizerMode + + +@dataclass +class StepConfig: + """One layer in the normalization pipeline.""" + + name: str # registered step name + options: dict[str, Any] = field(default_factory=dict) + + +@dataclass +class NormalizerConfig: + """ + NormalizerMode selects the stack: + - NONE — passthrough + - BASIC — universal Unicode/whitespace/case layer (language-agnostic) + - CUSTOM — explicit pipeline; caller provides `steps` + """ + + mode: NormalizerMode = NormalizerMode.BASIC + steps: list[StepConfig] = field(default_factory=list) + + +@dataclass +class GroupingConfig: + attribute: str | None = None # None → group by label only + strategy: GroupingStrategy = GroupingStrategy.JOIN + join_separator: str = " " + + +@dataclass +class TranscriptionRequirement: + name: str = "transcription" # human-readable, used in report headings + text_attribute: str = "transcription" # which column / attr holds text + + # Output unit of the reported rate (CER vs WER vs SER scope). + # WORD → WER-family + # CHARACTER → CER-family + granularity: Granularity = Granularity.WORD + + # Alignment regime. + # CHAR — char-level Levenshtein DP (default; handles arbitrary + # N-to-M boundary cases via natural char alignment) + # WORD — word-level (token) Levenshtein DP (faster, no boundary + # detection; equivalent to plain WER when granularity=word) + align: AlignMode = AlignMode.CHAR + + # Per-chunk cost function. Softness is a property of the metric + # itself rather than a side-channel knob. + # For granularity=CHARACTER (atomic units): all three metrics + # degenerate to EQUALITY. Only granularity=WORD makes the choice + # meaningful. + metric: Metric = Metric.EQUALITY + + # If set, per-chunk cost is rounded: cost > threshold → 1, else 0. + # Turns any soft metric into a binary one. With metric=EQUALITY the + # threshold is a no-op. + threshold: float | None = None + + normalizer: NormalizerConfig = field(default_factory=NormalizerConfig) + grouping: GroupingConfig = field(default_factory=GroupingConfig) + iou_threshold: float = 0.3 # used by FILTER strategy + enforce_overlap: bool = True # join: forbid token match between non-overlapping intervals + + # Gap (ms) by which the join overlap gate is relaxed: intervals separated by + # up to this much still count as overlapping. + overlap_tolerance_ms: float = 0.0 + + +@dataclass +class IntervalMatchingConfig: + iou_threshold: float = 0.3 + low_overlap_threshold: float = 0.5 + boundary_tolerance_ms: float = 200.0 # boundary-F1 timestamp tolerance + + +@dataclass +class QualitySettings: + interval_matching: IntervalMatchingConfig = field(default_factory=IntervalMatchingConfig) + transcriptions: list[TranscriptionRequirement] = field(default_factory=list) diff --git a/packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/data.py b/packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/data.py new file mode 100644 index 0000000000..bdd6defc0e --- /dev/null +++ b/packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/data.py @@ -0,0 +1,84 @@ +# Copyright (C) CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +from dataclasses import dataclass, field +from enum import Enum + + +@dataclass(frozen=True) +class Interval: + id: int + start: float # milliseconds + stop: float # milliseconds + label: str + source: str = "" + score: float = 1.0 + extra: dict[str, str] = field(default_factory=dict) + + @property + def duration(self) -> float: + return self.stop - self.start + + +class Granularity(str, Enum): + """Output unit of the reported rate (CER / WER / SER scope).""" + + WORD = "word" + CHARACTER = "character" + + +class AlignMode(str, Enum): + """Transcription alignment mode — what unit the DP operates on.""" + + CHAR = "char" + "char-level Levenshtein DP, word-edit reconstruction" + + WORD = "word" + "word-level Levenshtein DP, no boundary detection" + + +class Metric(str, Enum): + """Per-chunk cost function for L2 scoring.""" + + EQUALITY = "equality" + "0/1 strict per chunk" + + ERROR_RATE = "error-rate" + "char_edits / len(ref) — recall-shaped" + + NORMALIZED_LEV = "normalized-lev" + "char_edits / max(len) — bounded" + + +class NormalizerMode(str, Enum): + """Top-level normalizer mode selector.""" + + NONE = "none" # passthrough + BASIC = "basic" # universal Unicode / whitespace / case stack + CUSTOM = "custom" # caller-supplied `steps` + + +class GroupingStrategy(str, Enum): + """How to combine intervals within a (label, attr) group.""" + + FILTER = "filter" + "Match intervals by IoU and label, match text in pairs" + + JOIN = "join" + "Concatenate text per group key, align" + + +class EditOp(str, Enum): + """Per-chunk operation produced by the L2 aligner.""" + + EQUAL = "equal" + SUBSTITUTE = "substitute" + INSERT = "insert" + DELETE = "delete" + BOUNDARY = "boundary" # N-to-1 / 1-to-N word-boundary disagreement + + +GroupKey = tuple[str, str | None] # (label, attr_value_or_None) diff --git a/packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/interval_matching.py b/packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/interval_matching.py new file mode 100644 index 0000000000..78a14c545d --- /dev/null +++ b/packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/interval_matching.py @@ -0,0 +1,215 @@ +# Copyright (C) CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +from typing import Callable, Sequence + +from .config import IntervalMatchingConfig +from .data import Interval +from .reports import BoundaryAgreement, IntervalPairMetrics, IntervalReport + + +def iou(a: Interval, b: Interval) -> float: + lo = max(a.start, b.start) + hi = min(a.stop, b.stop) + inter = hi - lo + if inter <= 0: + return 0.0 + uni = max(a.stop, b.stop) - min(a.start, b.start) + return inter / uni if uni > 0 else 0.0 + + +def _label_eq(a: Interval, b: Interval) -> bool: + return a.label == b.label + + +def match_segments( + a_segms: Sequence[Interval], + b_segms: Sequence[Interval], + *, + distance: Callable[[Interval, Interval], float], + dist_thresh: float = 1.0, + label_matcher: Callable[[Interval, Interval], bool] = _label_eq, +) -> tuple[ + list[tuple[Interval, Interval]], + list[tuple[Interval, Interval]], + list[Interval], + list[Interval], +]: + # Ported verbatim from cvat.apps.quality_control.quality_reports.match_segments + # (numpy + scipy are imported lazily so the package imports without scipy when the + # Hungarian/FILTER path is unused). + import itertools + + import numpy as np + from scipy.optimize import linear_sum_assignment + + max_anns = max(len(a_segms), len(b_segms)) + distances = np.array( + [ + [ + 1 - distance(a, b) if a is not None and b is not None else 1 + for b, _ in itertools.zip_longest(b_segms, range(max_anns), fillvalue=None) + ] + for a, _ in itertools.zip_longest(a_segms, range(max_anns), fillvalue=None) + ] + ) + distances[~np.isfinite(distances)] = 1 + distances[distances > 1 - dist_thresh] = 1 + + if a_segms and b_segms: + a_matches, b_matches = linear_sum_assignment(distances) + else: + a_matches = [] + b_matches = [] + + matches = [] + mispred = [] + a_unmatched = [] + b_unmatched = [] + + for a_idx, b_idx in zip(a_matches, b_matches): + dist = distances[a_idx, b_idx] + if dist > 1 - dist_thresh or dist == 1: + if a_idx < len(a_segms): + a_unmatched.append(a_segms[a_idx]) + if b_idx < len(b_segms): + b_unmatched.append(b_segms[b_idx]) + else: + a_ann = a_segms[a_idx] + b_ann = b_segms[b_idx] + if label_matcher(a_ann, b_ann): + matches.append((a_ann, b_ann)) + else: + mispred.append((a_ann, b_ann)) + + if not len(a_matches) and not len(b_matches): + a_unmatched = list(a_segms) + b_unmatched = list(b_segms) + + return matches, mispred, a_unmatched, b_unmatched + + +def _hungarian_match( + gt: Sequence[Interval], + ds: Sequence[Interval], + *, + distance: Callable[[Interval, Interval], float], + iou_thresh: float, + label_matcher: Callable[[Interval, Interval], bool] = _label_eq, +) -> tuple[ + list[tuple[Interval, Interval]], + list[tuple[Interval, Interval]], + list[Interval], + list[Interval], +]: + return match_segments( + gt, ds, distance=distance, dist_thresh=iou_thresh, label_matcher=label_matcher + ) + + +def _two_stage_match(gt: Sequence[Interval], ds: Sequence[Interval], *, iou_thresh: float) -> tuple[ + list[tuple[Interval, Interval]], + list[tuple[Interval, Interval]], + list[Interval], + list[Interval], +]: + """Two-pass Hungarian: first pass prefers same-label pairs, second pass + picks up remaining different-label pairs. Keeps high-quality label + matches from getting outbid by IoU-only matches with the wrong label.""" + + def iou_with_label(a: Interval, b: Interval) -> float: + return iou(a, b) if _label_eq(a, b) else 0.0 + + matches, _, gt_u, ds_u = _hungarian_match( + gt, ds, distance=iou_with_label, iou_thresh=iou_thresh + ) + _, mispred, gt_u, ds_u = _hungarian_match(gt_u, ds_u, distance=iou, iou_thresh=iou_thresh) + return matches, mispred, gt_u, ds_u + + +def _collect_boundaries(intervals: Sequence[Interval]) -> list[float]: + pts: list[float] = [] + for iv in intervals: + pts.append(iv.start) + pts.append(iv.stop) + return sorted(pts) + + +def boundary_f1( + ref_boundaries: Sequence[float], + hyp_boundaries: Sequence[float], + *, + tolerance_ms: float, +) -> BoundaryAgreement: + """Greedy nearest-neighbor matching of boundary timestamps within a + fixed temporal tolerance. Returns precision / recall / F1 plus + raw tp / fp / fn counts.""" + + matched: set[int] = set() + tp = 0 + for r in ref_boundaries: + best_idx = -1 + best_d = tolerance_ms + 1e-9 + for i, h in enumerate(hyp_boundaries): + if i in matched: + continue + d = abs(r - h) + if d <= best_d: + best_idx, best_d = i, d + if best_idx >= 0 and best_d <= tolerance_ms: + matched.add(best_idx) + tp += 1 + fp = len(hyp_boundaries) - len(matched) + fn = len(ref_boundaries) - tp + precision = tp / (tp + fp) if (tp + fp) else 1.0 + recall = tp / (tp + fn) if (tp + fn) else 1.0 + f1 = 2 * precision * recall / (precision + recall) if (precision + recall) else 0.0 + return BoundaryAgreement(tp=tp, fp=fp, fn=fn, precision=precision, recall=recall, f1=f1) + + +def match_intervals( + gt: Sequence[Interval], ds: Sequence[Interval], *, config: IntervalMatchingConfig +) -> IntervalReport: + """ + Compare intervals at the boundary level. This function does not touch transcriptions. + + Pairs GT and DS intervals by IoU + label using a two-stage Hungarian + assignment, then derives per-pair spatial metrics (IoU, onset / offset + deltas, label match). Boundary-F1 also lives here because it is a + purely temporal metric — it cares where intervals start / stop, not + what they contain. + """ + + matches, mispred, gt_unmatched, ds_unmatched = _two_stage_match( + gt, ds, iou_thresh=config.iou_threshold + ) + + def to_metric(a: Interval, b: Interval) -> IntervalPairMetrics: + return IntervalPairMetrics( + gt=a, + ds=b, + iou=iou(a, b), + onset_delta=b.start - a.start, + offset_delta=b.stop - a.stop, + label_match=_label_eq(a, b), + ) + + boundary = boundary_f1( + _collect_boundaries(gt), + _collect_boundaries(ds), + tolerance_ms=config.boundary_tolerance_ms, + ) + + return IntervalReport( + matches=[to_metric(a, b) for a, b in matches], + label_mismatches=[to_metric(a, b) for a, b in mispred], + gt_unmatched=gt_unmatched, + ds_unmatched=ds_unmatched, + iou_threshold=config.iou_threshold, + low_overlap_threshold=config.low_overlap_threshold, + boundary_tolerance_ms=config.boundary_tolerance_ms, + boundary=boundary, + ) diff --git a/packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/normalization.py b/packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/normalization.py new file mode 100644 index 0000000000..acde781d22 --- /dev/null +++ b/packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/normalization.py @@ -0,0 +1,553 @@ +# Copyright (C) CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +""" +Text normalization for the audio QE pipeline. + +Pipeline of small composable steps. Pick a preset (`basic`) or build +custom pipelines (e.g. `[unicode, casefold, russian_yo, +collapse_whitespace]`). Layered design lets language-specific and +project-specific rules live next to universal Unicode / whitespace +cleanup without forking the normalizer code. +`step_available(name)` and fall back to DIY equivalents. +""" + +from __future__ import annotations + +import re +import unicodedata +from typing import Callable + +from .config import NormalizerConfig, StepConfig +from .data import NormalizerMode + +# English contractions — used by `expand_contractions_en` step. +# Specific patterns first; broad `\w+n't` last so it can't eat exceptions. +_EN_CONTRACTIONS = [ + (r"\bcan't\b", "can not"), + (r"\bwon't\b", "will not"), + (r"\bshan't\b", "shall not"), + (r"\bain't\b", "is not"), + (r"\bi'm\b", "i am"), + (r"\b(it|he|she|that|there|here|what|who|where|how|when|let)'s\b", r"\1 is"), + (r"\b(i|you|we|they|who)'ve\b", r"\1 have"), + (r"\b(i|you|he|she|it|we|they)'ll\b", r"\1 will"), + (r"\b(i|you|he|she|it|we|they)'d\b", r"\1 would"), + ( + r"\b(do|does|did|is|are|was|were|have|has|had|would|could|should|will|" + r"wouldn|shouldn|couldn|don|doesn|didn|isn|aren|wasn|weren|hasn|" + r"haven|hadn|mustn|needn)'t\b", + r"\1 not", + ), +] + +_ZERO_WIDTH_RE = re.compile(r"[​‌‍⁠­]") +_BRACKETED_RE = re.compile(r"\[[^\]]*\]|\([^\)]*\)|<[^>]*>") +_WS_RE = re.compile(r"\s+") + +NormalizationStep = Callable[[str], str] +_STEP_REGISTRY: dict[str, Callable[..., NormalizationStep]] = {} + + +def register_step(name: str): + def deco(factory: Callable[..., NormalizationStep]) -> Callable[..., NormalizationStep]: + _STEP_REGISTRY[name] = factory + return factory + + return deco + + +@register_step("unicode") +def _step_unicode(form: str = "NFKC") -> NormalizationStep: + return lambda t: unicodedata.normalize(form, t) + + +@register_step("zero_width_strip") +def _step_zero_width_strip() -> NormalizationStep: + return lambda t: _ZERO_WIDTH_RE.sub("", t) + + +@register_step("casefold") +def _step_casefold() -> NormalizationStep: + """Locale-independent lowercase. Beats `.lower()` for Turkish/Azeri/German.""" + return lambda t: t.casefold() + + +@register_step("strip_brackets") +def _step_strip_brackets() -> NormalizationStep: + """Remove [tags], (asides), . Useful for sound events / speaker tags.""" + return lambda t: _BRACKETED_RE.sub(" ", t) + + +@register_step("strip_punct") +def _step_strip_punct(preserve: list[str] | None = None) -> NormalizationStep: + preserve_set = set(preserve or []) + + def fn(t: str) -> str: + out = [] + for ch in t: + if ch.isalnum() or ch.isspace() or ch in preserve_set: + out.append(ch) + else: + cat = unicodedata.category(ch) + # Keep modifier letters etc.; only strip explicit punct/symbols. + if cat.startswith("P") or cat.startswith("S"): + out.append(" ") + else: + out.append(ch) + return "".join(out) + + return fn + + +@register_step("collapse_whitespace") +def _step_collapse_whitespace() -> NormalizationStep: + return lambda t: _WS_RE.sub(" ", t).strip() + + +@register_step("unify_apostrophes") +def _step_unify_apostrophes() -> NormalizationStep: + """Curly + modifier apostrophes → ASCII '. Run before contraction expansion.""" + return lambda t: t.replace("’", "'").replace("‘", "'").replace("ʼ", "'") + + +@register_step("unify_quotes") +def _step_unify_quotes() -> NormalizationStep: + return lambda t: (t.replace("“", '"').replace("”", '"').replace("«", '"').replace("»", '"')) + + +@register_step("substitutions") +def _step_substitutions(map: dict[str, str] | None = None) -> NormalizationStep: + items = list((map or {}).items()) + + def fn(t: str) -> str: + for src, dst in items: + t = t.replace(src, dst) + return t + + return fn + + +@register_step("regex_substitute") +def _step_regex_substitute( + patterns: list[tuple[str, str]] | None = None, flags: int = 0 +) -> NormalizationStep: + compiled = [(re.compile(p, flags), r) for p, r in (patterns or [])] + + def fn(t: str) -> str: + for p, r in compiled: + t = p.sub(r, t) + return t + + return fn + + +@register_step("expand_contractions_en") +def _step_expand_contractions_en() -> NormalizationStep: + compiled = [(re.compile(p, re.IGNORECASE), r) for p, r in _EN_CONTRACTIONS] + + def fn(t: str) -> str: + for p, r in compiled: + t = p.sub(r, t) + return t + + return fn + + +# Language-specific helpers — stdlib-only. Heavier ones (opencc, jaconv, pyarabic) +# live behind optional imports if/when needed. + + +@register_step("russian_yo") +def _step_russian_yo() -> NormalizationStep: + """Collapse ё→е (Russian) — same word, two spellings in practice.""" + return lambda t: t.replace("ё", "е").replace("Ё", "Е") + + +@register_step("german_eszett") +def _step_german_eszett() -> NormalizationStep: + """ß → ss. Common for Swiss-German compat or relaxed comparison.""" + return lambda t: t.replace("ß", "ss") + + +@register_step("arabic_alef_unify") +def _step_arabic_alef_unify() -> NormalizationStep: + """أ إ آ → ا (hamza-bearing alef variants → bare alef).""" + + def fn(t: str) -> str: + return t.replace("أ", "ا").replace("إ", "ا").replace("آ", "ا") + + return fn + + +@register_step("arabic_yaa_unify") +def _step_arabic_yaa_unify() -> NormalizationStep: + """ى → ي (alef maqsura → yaa).""" + return lambda t: t.replace("ى", "ي") + + +@register_step("strip_diacritics") +def _step_strip_diacritics() -> NormalizationStep: + """Strip combining marks (Mn). Useful for Arabic harakat, Hebrew niqqud, etc.""" + + def fn(t: str) -> str: + decomposed = unicodedata.normalize("NFD", t) + out = "".join(c for c in decomposed if unicodedata.category(c) != "Mn") + return unicodedata.normalize("NFC", out) + + return fn + + +@register_step("arabic_tatweel_strip") +def _step_arabic_tatweel_strip() -> NormalizationStep: + """Remove tatweel (ـ U+0640) — purely decorative elongation.""" + return lambda t: t.replace("ـ", "") + + +# Devanagari ----------------------------------------------------------------- + +# Precomposed nukta forms → canonical decomposed forms (via NFC after replace). +# Keep canonical NFC form to match modern data, but unify legacy variants. +_DEVANAGARI_NUKTA_MAP = { + "क़": "क़", # क़ → क + nukta + "ख़": "ख़", # ख़ + "ग़": "ग़", # ग़ + "ज़": "ज़", # ज़ + "ड़": "ड़", # ड़ + "ढ़": "ढ़", # ढ़ + "फ़": "फ़", # फ़ + "य़": "य़", # य़ +} + + +@register_step("hindi_danda") +def _step_hindi_danda() -> NormalizationStep: + """Danda । and double-danda ॥ → space.""" + return lambda t: t.replace("।", " ").replace("॥", " ") + + +@register_step("hindi_nukta_unify") +def _step_hindi_nukta_unify() -> NormalizationStep: + """Decompose precomposed nukta letters; NFC re-composes consistently downstream.""" + items = list(_DEVANAGARI_NUKTA_MAP.items()) + + def fn(t: str) -> str: + for src, dst in items: + t = t.replace(src, dst) + return t + + return fn + + +@register_step("hindi_chandrabindu") +def _step_hindi_chandrabindu() -> NormalizationStep: + """Chandrabindu ँ → anusvara ं (collapse marks treated equivalent in speech).""" + return lambda t: t.replace("ँ", "ं") + + +# CJK ------------------------------------------------------------------------ + +# Common Chinese / Japanese punctuation. NFKC already maps full-width ASCII +# punctuation; this catches genuinely-CJK punctuation that NFKC doesn't touch. +_CJK_PUNCT = ",。、;:?!…—·「」『』〈〉《》【】〖〗〔〕" + + +@register_step("cjk_punct_strip") +def _step_cjk_punct_strip() -> NormalizationStep: + tbl = str.maketrans({ch: " " for ch in _CJK_PUNCT}) + return lambda t: t.translate(tbl) + + +# Turkish -------------------------------------------------------------------- + + +@register_step("turkish_dotted_i") +def _step_turkish_dotted_i() -> NormalizationStep: + """Locale-correct İ/I → i/ı. Apply before casefold for Turkish/Azeri.""" + + def fn(t: str) -> str: + return t.replace("İ", "i").replace( # İ → i + "I", "ı" + ) # I → ı (intentional; pair with casefold) + + return fn + + +# Dutch ---------------------------------------------------------------------- + + +@register_step("dutch_ij_normalize") +def _step_dutch_ij_normalize() -> NormalizationStep: + """Ligature ij/IJ → i+j / I+J. NFKC also does this; explicit step for clarity.""" + return lambda t: t.replace("ij", "ij").replace("IJ", "IJ") + + +# Optional third-party steps ------------------------------------------------- +# +# Registered only when the matching package is importable. Calling +# `step_available(name)` lets presets fall back to DIY if missing. + + +def step_available(name: str) -> bool: + return name in _STEP_REGISTRY + + +try: # pragma: no cover - optional dependency + import inflect as _inflect # type: ignore + + _INFLECT_ENGINE = _inflect.engine() + _NUM_RE = re.compile(r"-?\d+(?:[.,]\d+)?") + + @register_step("expand_numerals_en") + def _step_expand_numerals_en() -> NormalizationStep: + """Replace numbers with English words (`23` → `twenty three`). + Requires `inflect` (MIT, ~250 KB). Skip step if not installed.""" + + def to_words(match: re.Match[str]) -> str: + raw = match.group(0).replace(",", ".") + return _INFLECT_ENGINE.number_to_words(raw).replace("-", " ") + + return lambda t: _NUM_RE.sub(to_words, t) + +except ImportError: # pragma: no cover + pass + + +try: # pragma: no cover - optional dependency + import ftfy as _ftfy # type: ignore + + @register_step("ftfy_fix") + def _step_ftfy_fix( + normalization: str = "NFC", + unescape_html: bool = True, + ) -> NormalizationStep: + """Robust input cleanup: mojibake, HTML entities, curly punct, ligatures, + zero-width chars, NBSP. Requires `ftfy` (MIT, ~1 MB).""" + + cfg = _ftfy.TextFixerConfig( + normalization=normalization, + unescape_html=unescape_html, + ) + return lambda t: _ftfy.fix_text(t, cfg) + +except ImportError: # pragma: no cover + pass + + +try: # pragma: no cover - optional dependency + import zhconv as _zhconv # type: ignore + + @register_step("zhconv_simplify") + def _step_zhconv_simplify(variant: str = "zh-cn") -> NormalizationStep: + """Convert Chinese to Simplified. variant: zh-cn (mainland), zh-sg, zh-my. + Requires `zhconv` (MIT, ~700 KB).""" + return lambda t: _zhconv.convert(t, variant) + + @register_step("zhconv_traditional") + def _step_zhconv_traditional(variant: str = "zh-tw") -> NormalizationStep: + """Convert Chinese to Traditional. variant: zh-tw (Taiwan), zh-hk (HK).""" + return lambda t: _zhconv.convert(t, variant) + +except ImportError: # pragma: no cover + pass + + +# Preset stacks -------------------------------------------------------------- + + +BASIC_STACK: list[StepConfig] = [ + StepConfig("unify_apostrophes"), + StepConfig("unify_quotes"), + StepConfig("unicode", {"form": "NFKC"}), + StepConfig("zero_width_strip"), + StepConfig("casefold"), + StepConfig("strip_brackets"), + StepConfig("strip_punct"), + StepConfig("collapse_whitespace"), +] + + +# Language presets. Each preset is built as: +# +# The prefix prefers `ftfy_fix` if available (handles mojibake / HTML / +# curly punct / zero-width / ligatures in one robust step). Otherwise it +# falls back to DIY apostrophe + quote + zero-width steps. + + +def _prefix_steps(use_ftfy: bool) -> list[StepConfig]: + if use_ftfy and step_available("ftfy_fix"): + return [ + StepConfig("ftfy_fix", {"normalization": "NFC", "unescape_html": True}), + StepConfig("strip_brackets"), + ] + return [ + StepConfig("unify_apostrophes"), + StepConfig("unify_quotes"), + StepConfig("zero_width_strip"), + StepConfig("strip_brackets"), + ] + + +_SUFFIX_STEPS: list[StepConfig] = [ + StepConfig("strip_punct"), + StepConfig("collapse_whitespace"), +] + + +def _lang_body(code: str, *, use_zhconv: bool) -> list[StepConfig]: + if code == "en": + return [ + StepConfig("unicode", {"form": "NFKC"}), + StepConfig("casefold"), + StepConfig("expand_contractions_en"), + ] + if code in ("es", "fr", "it", "pt", "pl"): + return [StepConfig("unicode", {"form": "NFC"}), StepConfig("casefold")] + if code == "de": + return [ + StepConfig("unicode", {"form": "NFC"}), + StepConfig("casefold"), + StepConfig("german_eszett"), + ] + if code == "nl": + return [ + StepConfig("unicode", {"form": "NFC"}), + StepConfig("dutch_ij_normalize"), + StepConfig("casefold"), + ] + if code == "ru": + return [ + StepConfig("unicode", {"form": "NFC"}), + StepConfig("casefold"), + StepConfig("russian_yo"), + ] + if code == "tr": + return [ + StepConfig("unicode", {"form": "NFC"}), + StepConfig("turkish_dotted_i"), + StepConfig("casefold"), + ] + if code == "zh": + steps: list[StepConfig] = [ + StepConfig("unicode", {"form": "NFKC"}), + StepConfig("cjk_punct_strip"), + ] + if use_zhconv and step_available("zhconv_simplify"): + steps.append(StepConfig("zhconv_simplify", {"variant": "zh-cn"})) + return steps + if code == "ja": + return [ + StepConfig("unicode", {"form": "NFKC"}), + StepConfig("cjk_punct_strip"), + ] + if code == "ko": + return [ + StepConfig("unicode", {"form": "NFC"}), + StepConfig("cjk_punct_strip"), + ] + if code == "hi": + return [ + StepConfig("hindi_nukta_unify"), + StepConfig("unicode", {"form": "NFC"}), + StepConfig("hindi_danda"), + StepConfig("hindi_chandrabindu"), + ] + if code == "ar": + return [ + StepConfig("unicode", {"form": "NFC"}), + StepConfig("arabic_tatweel_strip"), + StepConfig("arabic_alef_unify"), + StepConfig("arabic_yaa_unify"), + StepConfig("strip_diacritics"), + ] + raise ValueError(f"no preset for language {code!r}") + + +SUPPORTED_LANGS: tuple[str, ...] = ( + "en", + "es", + "fr", + "de", + "it", + "pt", + "nl", + "pl", + "ru", + "tr", + "zh", + "ja", + "ko", + "hi", + "ar", +) + + +def lang_preset( + code: str, + *, + with_numerals: bool = False, + use_ftfy: bool = True, + use_zhconv: bool = True, +) -> NormalizerConfig: + """Build a NormalizerConfig for a language code. + + `use_ftfy` / `use_zhconv` are opt-out flags — when the optional libs are + installed they replace DIY steps; flip to False for fully-deterministic DIY + output regardless of installed packages. + """ + + if code not in SUPPORTED_LANGS: + raise ValueError(f"no preset for language {code!r}; available: {sorted(SUPPORTED_LANGS)}") + steps: list[StepConfig] = [ + *_prefix_steps(use_ftfy), + *_lang_body(code, use_zhconv=use_zhconv), + *_SUFFIX_STEPS, + ] + if with_numerals and code == "en" and step_available("expand_numerals_en"): + idx = next((i for i, s in enumerate(steps) if s.name == "strip_punct"), len(steps)) + steps.insert(idx, StepConfig("expand_numerals_en")) + return NormalizerConfig(mode=NormalizerMode.CUSTOM, steps=steps) + + +# Backward-compatible attribute for code that read LANG_PRESETS as a dict. +LANG_PRESETS: dict[str, list[StepConfig]] = { + code: lang_preset(code).steps for code in SUPPORTED_LANGS +} + + +def _build_step(cfg: StepConfig) -> NormalizationStep: + try: + factory = _STEP_REGISTRY[cfg.name] + except KeyError as exc: + raise ValueError( + f"unknown normalization step {cfg.name!r}; available: {sorted(_STEP_REGISTRY)}" + ) from exc + return factory(**cfg.options) + + +class Normalizer: + def __init__(self, cfg: NormalizerConfig) -> None: + self.cfg = cfg + if cfg.mode == NormalizerMode.NONE: + steps_cfg: list[StepConfig] = [] + elif cfg.mode == NormalizerMode.BASIC: + steps_cfg = BASIC_STACK + elif cfg.mode == NormalizerMode.CUSTOM: + steps_cfg = cfg.steps + else: + raise ValueError( + f"normalizer mode must be one of " + f"{[m.value for m in NormalizerMode]!r}, got {cfg.mode!r}" + ) + self._steps: list[NormalizationStep] = [_build_step(s) for s in steps_cfg] + self._step_names: list[str] = [s.name for s in steps_cfg] + + @property + def step_names(self) -> list[str]: + return list(self._step_names) + + def __call__(self, text: str) -> str: + for step in self._steps: + text = step(text) + return text diff --git a/packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/pipeline.py b/packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/pipeline.py new file mode 100644 index 0000000000..2b97200717 --- /dev/null +++ b/packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/pipeline.py @@ -0,0 +1,24 @@ +# Copyright (C) CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +from typing import Sequence + +from .config import QualitySettings +from .data import Interval +from .interval_matching import match_intervals +from .reports import ComparisonReport +from .transcription_matching import match_transcriptions + + +def compare( + gt: Sequence[Interval], ds: Sequence[Interval], *, settings: QualitySettings +) -> ComparisonReport: + return ComparisonReport( + gt=list(gt), + ds=list(ds), + intervals=match_intervals(gt, ds, config=settings.interval_matching), + transcriptions=[match_transcriptions(gt, ds, req=req) for req in settings.transcriptions], + ) diff --git a/packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/reports.py b/packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/reports.py new file mode 100644 index 0000000000..d9c071eec7 --- /dev/null +++ b/packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/reports.py @@ -0,0 +1,165 @@ +# Copyright (C) CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +""" +Output / intermediate result shapes for the audio QE pipeline. + +`compare()` returns a `ComparisonReport`; the rest of the dataclasses +here capture intermediate alignment products used while building it. +The CVAT side (`cvat.apps.quality_control.quality_reports`) consumes +the top-level fields to fill its persisted `QualityReport.data` +JSON. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +from .config import TranscriptionRequirement +from .data import EditOp, Granularity, GroupKey, Interval + +# ============================ Interval matching layer ============================ + + +@dataclass +class IntervalPairMetrics: + gt: Interval + ds: Interval + iou: float + onset_delta: float + offset_delta: float + label_match: bool + + +@dataclass +class BoundaryAgreement: + tp: int + fp: int + fn: int + precision: float + recall: float + f1: float + + +@dataclass +class IntervalReport: + matches: list[IntervalPairMetrics] + label_mismatches: list[IntervalPairMetrics] + gt_unmatched: list[Interval] + ds_unmatched: list[Interval] + iou_threshold: float + low_overlap_threshold: float + boundary_tolerance_ms: float + boundary: BoundaryAgreement | None = None + + @property + def low_overlap(self) -> list[IntervalPairMetrics]: + return [m for m in self.matches if m.iou < self.low_overlap_threshold] + + +# ============================ Transcription layer ================================ + + +# ============================ Transcription alignment ============================ + + +@dataclass +class AlignmentEdit: + op: EditOp + ref_start: int + ref_end: int # exclusive + hyp_start: int + hyp_end: int # exclusive + ref_units: list[str] + hyp_units: list[str] + + +@dataclass +class AlignmentResult: + granularity: Granularity + ref_normalized: str + hyp_normalized: str + ref_units: list[str] + hyp_units: list[str] + edits: list[AlignmentEdit] + error_rate: float # word error rate (WER) / CER (when granularity=character) + + substitutions: int + insertions: int + deletions: int + hits: int + + # Per-token source-interval index (within the group). None for the + # per-pair (filter) path, which aligns a single text pair and has no + # multi-interval origins to track. + ref_origins: list[int] | None = None + hyp_origins: list[int] | None = None + + +# ============================ Transcription report =============================== + + +@dataclass +class GroupAlignment: + key: GroupKey + gt_intervals: list[Interval] + ds_intervals: list[Interval] + alignment: AlignmentResult + + +@dataclass +class FilterPairAlignment: + gt: Interval + ds: Interval + iou: float + onset_delta: float + offset_delta: float + alignment: AlignmentResult + + +@dataclass +class TranscriptionReport: + requirement: TranscriptionRequirement + + # Filter mode outputs + pairs: list[FilterPairAlignment] + pair_gt_unmatched: list[Interval] + pair_ds_unmatched: list[Interval] + + # Join mode outputs + groups: list[GroupAlignment] + missing_groups: list[tuple[GroupKey, list[Interval]]] # only in GT + extra_groups: list[tuple[GroupKey, list[Interval]]] # only in DS + + @property + def alignments(self) -> list[AlignmentResult]: + if self.requirement.grouping.strategy == "join": + return [g.alignment for g in self.groups] + return [p.alignment for p in self.pairs] + + @property + def corpus_rate(self) -> float: + """Headline rate per `requirement.metric` aggregated across all + group / pair alignments. Lazy-imports the aggregator to avoid an + import cycle with `transcription_matching`.""" + from .transcription_matching import aggregate_metric + + _, rate = aggregate_metric( + self.alignments, + metric=self.requirement.metric, + threshold=self.requirement.threshold, + granularity=self.requirement.granularity, + ) + return rate + + +# ============================ Top-level ========================================== + + +@dataclass +class ComparisonReport: + gt: list[Interval] + ds: list[Interval] + intervals: IntervalReport + transcriptions: list[TranscriptionReport] diff --git a/packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/transcription_matching.py b/packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/transcription_matching.py new file mode 100644 index 0000000000..c553f2d9dc --- /dev/null +++ b/packages/examples/cvat/recording-oracle/libs/audio_qe/audio_qe/transcription_matching.py @@ -0,0 +1,1116 @@ +# Copyright (C) CVAT.ai Corporation +# +# SPDX-License-Identifier: MIT + +""" +Transcription quality estimation. + +Tokenize, group, align, score. Owns: + + * Tokenization (word / character) + * Per-chunk cost functions (equality / error-rate / normalized-lev) + and aggregation across alignments + * Levenshtein alignment over tokens, with an overlap-constrained + variant that forbids matches between tokens whose source intervals + don't temporally intersect + * Char-level DP with word-boundary reconstruction (handles N-to-M + boundary disagreements naturally as `boundary` edits) + * Group-level orchestration (join / filter strategies) — entry + point `run_transcription_qe()`. +""" + +from __future__ import annotations + +from collections import defaultdict +from collections.abc import Iterator +from typing import Callable, Sequence + +import numpy as np +import regex + +from .config import TranscriptionRequirement +from .data import ( + AlignMode, + EditOp, + Granularity, + GroupingStrategy, + GroupKey, + Interval, + Metric, +) +from .interval_matching import _two_stage_match, iou +from .normalization import Normalizer +from .reports import ( + AlignmentEdit, + AlignmentResult, + FilterPairAlignment, + GroupAlignment, + TranscriptionReport, +) + +# `\X` matches one extended grapheme cluster — keeps combining marks +# (Indic, Arabic diacritics) and emoji ZWJ sequences as a single unit. +_GRAPHEMES_RE = regex.compile(r"\X") + + +def _iter_graphemes(text: str) -> Iterator[str]: + """Yield each extended grapheme cluster in `text`. Single source of + truth for character-level iteration across char-granularity + tokenization and the char-stream alignment paths.""" + for m in _GRAPHEMES_RE.finditer(text): + yield m.group() + + +# ============================ Tokenization ==================================== + + +def tokenize(text: str, *, granularity: Granularity) -> list[str]: + if granularity == Granularity.WORD: + return text.split() + if granularity == Granularity.CHARACTER: + return list(_iter_graphemes(text)) + raise AssertionError(granularity) + + +# ============================ Per-chunk cost functions ======================= + + +def _char_edit_distance(a: str, b: str) -> int: + """Plain Levenshtein over characters. Short strings (typically <30 chars), + pure-Python is fine.""" + if a == b: + return 0 + if not a: + return len(b) + if not b: + return len(a) + prev = list(range(len(b) + 1)) + for i, ca in enumerate(a, 1): + cur = [i] + for j, cb in enumerate(b, 1): + cur.append( + min( + cur[-1] + 1, # insert + prev[j] + 1, # delete + prev[j - 1] + (0 if ca == cb else 1), # substitute + ) + ) + prev = cur + return prev[-1] + + +def _cost_equality(a: str, b: str) -> float: + return 0.0 if a == b else 1.0 + + +def _cost_error_rate(a: str, b: str) -> float: + """edits / len(ref). Recall-shaped; can exceed 1 when hyp is much longer.""" + + if a == b: + return 0.0 + denom = len(a) or 1 + return _char_edit_distance(a, b) / denom + + +def _cost_normalized_lev(a: str, b: str) -> float: + """ + edits / max(len). Bounded in [0, 1]; symmetric. + Standard name in the literature: normalized Levenshtein distance (NED). + """ + + if a == b: + return 0.0 + denom = max(len(a), len(b)) or 1 + return _char_edit_distance(a, b) / denom + + +_METRIC_FNS: dict[Metric, Callable[[str, str], float]] = { + Metric.EQUALITY: _cost_equality, + Metric.ERROR_RATE: _cost_error_rate, + Metric.NORMALIZED_LEV: _cost_normalized_lev, +} +_METRIC_NAMES = tuple(m.value for m in _METRIC_FNS) + +# A token-pair cost: substitution cost between a ref and a hyp token +TokenCost = Callable[[str, str], float] + + +def _unit_sub_cost(a: str, b: str) -> float: + """Default substitution cost: 1 for any non-identical pair (plain + Levenshtein). Identical pairs are handled as matches (cost 0) by the + caller, so this is only ever invoked for a != b.""" + return 1.0 + + +def make_token_cost(metric: Metric | str, threshold: float | None) -> TokenCost: + try: + metric = Metric(metric) + except ValueError as exc: + raise ValueError(f"unknown metric {metric!r}; choices: {list(_METRIC_NAMES)}") from exc + + metric_fn = _METRIC_FNS[metric] + if threshold is None: + return metric_fn + + def thresholded(a: str, b: str) -> float: + return 1.0 if metric_fn(a, b) > threshold else 0.0 + + return thresholded + + +def _metric_label( + metric: Metric, + granularity: Granularity, + *, + threshold: float | None = None, +) -> str: + """Display label = base unit (WER/CER) ± prefix per metric/threshold combo.""" + + unit = "CER" if granularity == Granularity.CHARACTER else "WER" + if metric == Metric.EQUALITY: + return unit + if threshold is not None: + return f"fuzzy-{unit}" + return f"soft-{unit}" + + +def _chunk_cost( + e: AlignmentEdit, + *, + granularity: Granularity, + token_cost: TokenCost, +) -> tuple[float, int]: + """Returns (errors_in_granularity_units, ref_units_consumed) for one chunk. + + Per-chunk semantics: + * granularity=char → errors = char-edit count, ref = char count + * granularity=word → errors = token_cost(ref, hyp), ref = word count + (boundary chunks: applied to concatenated forms) + Delete/insert chunks: 1 error per granularity unit, ref unchanged for inserts. + """ + + is_char = granularity == Granularity.CHARACTER + + if e.op == EditOp.EQUAL: + if is_char: + return 0.0, sum(len(u) for u in e.ref_units) + return 0.0, len(e.ref_units) + + if e.op == EditOp.SUBSTITUTE: + errs = 0.0 + refs = 0 + for a, b in zip(e.ref_units, e.hyp_units): + if is_char: + errs += _char_edit_distance(a, b) + refs += len(a) + else: + errs += token_cost(a, b) + refs += 1 + return errs, refs + + if e.op == EditOp.BOUNDARY: + ref_concat = " ".join(e.ref_units) + hyp_concat = " ".join(e.hyp_units) + if is_char: + return float(_char_edit_distance(ref_concat, hyp_concat)), len(ref_concat) + return token_cost(ref_concat, hyp_concat), len(e.ref_units) + + if e.op == EditOp.DELETE: + if is_char: + n = sum(len(u) for u in e.ref_units) + return float(n), n + n = len(e.ref_units) + return float(n), n + + if e.op == EditOp.INSERT: + if is_char: + return float(sum(len(u) for u in e.hyp_units)), 0 + return float(len(e.hyp_units)), 0 + + raise ValueError(f"unknown op {e.op}") + + +def aggregate_metric( + alignments: Sequence[AlignmentResult], + *, + metric: Metric | str, + threshold: float | None, + granularity: Granularity, +) -> tuple[str, float]: + """ + Aggregate the chosen metric across multiple alignments. + Returns (display_label, rate). + """ + + token_cost = make_token_cost(metric, threshold) # validates metric + metric = Metric(metric) + total_err: float = 0.0 + total_ref: int = 0 + for a in alignments: + for e in a.edits: + err, ref = _chunk_cost(e, granularity=granularity, token_cost=token_cost) + total_err += err + total_ref += ref + + rate = total_err / total_ref if total_ref else float("nan") + return _metric_label(metric, granularity, threshold=threshold), rate + + +# ============================ Alignment ======================================= + + +def _align_pair( + ref_text: str, + hyp_text: str, + *, + granularity: Granularity, + normalizer: Normalizer, + sub_cost: TokenCost = _unit_sub_cost, +) -> AlignmentResult: + ref_n = normalizer(ref_text) + hyp_n = normalizer(hyp_text) + + ref_units = tokenize(ref_n, granularity=granularity) + hyp_units = tokenize(hyp_n, granularity=granularity) + + # Degenerate cases — deterministic answers, skip the DP. + if not ref_units and not hyp_units: + return AlignmentResult(granularity, ref_n, hyp_n, [], [], [], 0.0, 0, 0, 0, 0) + if not ref_units: + return AlignmentResult( + granularity, + ref_n, + hyp_n, + [], + list(hyp_units), + [AlignmentEdit(EditOp.INSERT, 0, 0, 0, len(hyp_units), [], list(hyp_units))], + float("inf"), + 0, + len(hyp_units), + 0, + 0, + ) + if not hyp_units: + return AlignmentResult( + granularity, + ref_n, + hyp_n, + list(ref_units), + [], + [AlignmentEdit(EditOp.DELETE, 0, len(ref_units), 0, 0, list(ref_units), [])], + 1.0, + 0, + 0, + len(ref_units), + 0, + ) + + # Single pair → one notional interval per side → all-pass overlap. + overlap = np.ones((1, 1), dtype=bool) + edits, hits, subs, ins, dels, total_cost = _overlap_constrained_align( + ref_units, + [0] * len(ref_units), + hyp_units, + [0] * len(hyp_units), + overlap, + sub_cost=sub_cost, + ) + error_rate = total_cost / len(ref_units) + + return AlignmentResult( + granularity=granularity, + ref_normalized=ref_n, + hyp_normalized=hyp_n, + ref_units=list(ref_units), + hyp_units=list(hyp_units), + edits=edits, + error_rate=error_rate, + substitutions=subs, + insertions=ins, + deletions=dels, + hits=hits, + ) + + +# ============================ Transcription grouping + DP ======================== + + +def group_key(interval: Interval, *, attribute: str | None) -> GroupKey: + if attribute is None: + return (interval.label, None) + return (interval.label, interval.extra.get(attribute) or None) + + +def group_intervals( + intervals: Sequence[Interval], *, attribute: str | None +) -> dict[GroupKey, list[Interval]]: + out: dict[GroupKey, list[Interval]] = defaultdict(list) + for iv in intervals: + out[group_key(iv, attribute=attribute)].append(iv) + return out + + +def _join_group_text(intervals: Sequence[Interval], *, text_attr: str, separator: str) -> str: + in_order = sorted(intervals, key=lambda iv: (iv.start, iv.id)) + parts = [iv.extra.get(text_attr, "") for iv in in_order] + return separator.join(p for p in parts if p) + + +def _normalize_tokenize_per_interval( + intervals: Sequence[Interval], + *, + text_attr: str, + normalizer: Normalizer, + granularity: Granularity, + inter_interval_sep: str = " ", +) -> tuple[list[str], list[int], list[Interval], list[str]]: + """Returns (flat_units, per-unit interval index, sorted intervals, normalized text per interval). + + For char granularity, inserts inter_interval_sep tokens between adjacent + non-empty intervals so word boundaries that fall at interval splits stay + visible to the DP. For word granularity no separator is inserted: word DP + doesn't see whitespace between tokens.""" + in_order = sorted(intervals, key=lambda iv: (iv.start, iv.id)) + flat_units: list[str] = [] + origins: list[int] = [] + norm_texts: list[str] = [] + is_char = granularity == Granularity.CHARACTER + for idx, iv in enumerate(in_order): + raw = iv.extra.get(text_attr, "") + normed = normalizer(raw) + norm_texts.append(normed) + if is_char and flat_units and normed and inter_interval_sep: + for ch in _iter_graphemes(inter_interval_sep): + flat_units.append(ch) + origins.append(idx - 1) + for tok in tokenize(normed, granularity=granularity): + flat_units.append(tok) + origins.append(idx) + return flat_units, origins, in_order, norm_texts + + +def _normalize_to_chars_per_interval( + intervals: Sequence[Interval], + *, + text_attr: str, + normalizer: Normalizer, + inter_interval_sep: str = " ", +) -> tuple[list[str], list[int], list[Interval], list[str]]: + """Build flat char streams + per-char interval origin index. Spaces between + consecutive intervals get the previous interval's origin.""" + in_order = sorted(intervals, key=lambda iv: (iv.start, iv.id)) + chars: list[str] = [] + origins: list[int] = [] + norm_texts: list[str] = [] + for idx, interval in enumerate(in_order): + raw = interval.extra.get(text_attr, "") + normed = normalizer(raw) + norm_texts.append(normed) + if chars and inter_interval_sep and normed: + for ch in _iter_graphemes(inter_interval_sep): + chars.append(ch) + origins.append(idx - 1) + for ch in _iter_graphemes(normed): + chars.append(ch) + origins.append(idx) + return chars, origins, in_order, norm_texts + + +def _overlap_matrix( + gt_intervals: Sequence[Interval], + ds_intervals: Sequence[Interval], + *, + tolerance_ms: float = 0.0, +) -> np.ndarray: + """Boolean GTxDS matrix of temporally-overlapping interval pairs. With a + positive `tolerance_ms`, intervals separated by a gap of up to that many + milliseconds still count as overlapping, so the boundary tolerance relaxes + the alignment overlap gate the same way it relaxes interval matching.""" + if not gt_intervals or not ds_intervals: + return np.zeros((len(gt_intervals), len(ds_intervals)), dtype=bool) + g_start = np.array([iv.start for iv in gt_intervals]) + g_stop = np.array([iv.stop for iv in gt_intervals]) + d_start = np.array([iv.start for iv in ds_intervals]) + d_stop = np.array([iv.stop for iv in ds_intervals]) + lo = np.maximum(g_start[:, None], d_start[None, :]) + hi = np.minimum(g_stop[:, None], d_stop[None, :]) + return hi > lo - tolerance_ms + + +def _overlap_constrained_align( + ref_units: Sequence[str], + ref_origins: Sequence[int], + hyp_units: Sequence[str], + hyp_origins: Sequence[int], + overlap: np.ndarray, + *, + sub_cost: TokenCost = _unit_sub_cost, +) -> tuple[list[AlignmentEdit], int, int, int, int, int, float]: + """Levenshtein DP that forbids match/substitute between tokens whose source + intervals do not temporally overlap. + + `sub_cost(ref_token, hyp_token)` is the substitution cost folded into the + DP objective, so the alignment minimizes the *metric* cost rather than the + hard edit count. With the default unit cost this is plain Levenshtein. + Insert / delete stay at cost 1. Identical tokens always cost 0 (a match), + independent of `sub_cost`. + + Returns (edits, hits, substitutions, insertions, deletions, total_cost). + `total_cost` is the optimal DP cost — the metric-weighted error mass, used + to form the rate. The hits / subs / ins / dels are plain edit-op counts + (integers). This DP emits only EQUAL / SUBSTITUTE / INSERT / DELETE; it does + not produce BOUNDARY edits (N-to-M word merges are reconstructed separately + by `_reconstruct_word_edits_from_chars` on a char-level alignment). + """ + + n, m = len(ref_units), len(hyp_units) + INF = float("inf") + DEL, INS, MATCH, SUB = 1, 2, 3, 4 + + cost = np.full((n + 1, m + 1), INF, dtype=np.float32) + back = np.zeros((n + 1, m + 1), dtype=np.int8) + cost[0, 0] = 0.0 + for i in range(1, n + 1): + cost[i, 0] = i + back[i, 0] = DEL + for j in range(1, m + 1): + cost[0, j] = j + back[0, j] = INS + + for i in range(1, n + 1): + ru = ref_units[i - 1] + ri = ref_origins[i - 1] + for j in range(1, m + 1): + del_c = cost[i - 1, j] + 1 + ins_c = cost[i, j - 1] + 1 + best, op = del_c, DEL + if ins_c < best: + best, op = ins_c, INS + hj = hyp_origins[j - 1] + if overlap[ri, hj]: + if ru == hyp_units[j - 1]: + diag = cost[i - 1, j - 1] # exact match, cost 0 + cand_op = MATCH + else: + diag = cost[i - 1, j - 1] + sub_cost(ru, hyp_units[j - 1]) + cand_op = SUB + if diag < best: + best, op = diag, cand_op + + cost[i, j] = best + back[i, j] = op + + ops_rev: list[tuple[str, int, int, int, int]] = [] + i, j = n, m + while i > 0 or j > 0: + op = back[i, j] + if op == MATCH: + ops_rev.append((EditOp.EQUAL, i - 1, i, j - 1, j)) + i -= 1 + j -= 1 + elif op == SUB: + ops_rev.append((EditOp.SUBSTITUTE, i - 1, i, j - 1, j)) + i -= 1 + j -= 1 + elif op == DEL: + ops_rev.append((EditOp.DELETE, i - 1, i, j, j)) + i -= 1 + elif op == INS: + ops_rev.append((EditOp.INSERT, i, i, j - 1, j)) + j -= 1 + else: + break + ops = list(reversed(ops_rev)) + + edits: list[AlignmentEdit] = [] + for op_name, r_s, r_e, h_s, h_e in ops: + if ( + edits + and edits[-1].op == op_name + and op_name in (EditOp.EQUAL, EditOp.SUBSTITUTE) + and edits[-1].ref_end == r_s + and edits[-1].hyp_end == h_s + ): + last = edits[-1] + last.ref_end = r_e + last.hyp_end = h_e + last.ref_units += list(ref_units[r_s:r_e]) + last.hyp_units += list(hyp_units[h_s:h_e]) + elif ( + edits + and edits[-1].op == EditOp.DELETE + and op_name == EditOp.DELETE + and edits[-1].ref_end == r_s + ): + edits[-1].ref_end = r_e + edits[-1].ref_units += list(ref_units[r_s:r_e]) + elif ( + edits + and edits[-1].op == EditOp.INSERT + and op_name == EditOp.INSERT + and edits[-1].hyp_end == h_s + ): + edits[-1].hyp_end = h_e + edits[-1].hyp_units += list(hyp_units[h_s:h_e]) + else: + edits.append( + AlignmentEdit( + op=op_name, + ref_start=r_s, + ref_end=r_e, + hyp_start=h_s, + hyp_end=h_e, + ref_units=list(ref_units[r_s:r_e]), + hyp_units=list(hyp_units[h_s:h_e]), + ) + ) + + hits = sum(e.ref_end - e.ref_start for e in edits if e.op == EditOp.EQUAL) + subs = sum(e.ref_end - e.ref_start for e in edits if e.op == EditOp.SUBSTITUTE) + dels = sum(e.ref_end - e.ref_start for e in edits if e.op == EditOp.DELETE) + ins = sum(e.hyp_end - e.hyp_start for e in edits if e.op == EditOp.INSERT) + total_cost = float(cost[n, m]) + return edits, hits, subs, ins, dels, total_cost + + +def _tokenize_with_ranges(chars: Sequence[str]) -> tuple[list[tuple[int, int, str]], list[int]]: + """Walk a char stream, return word ranges + per-char word index (-1 for spaces).""" + words: list[tuple[int, int, str]] = [] + idx_per_char: list[int] = [-1] * len(chars) + i = 0 + word_idx = 0 + while i < len(chars): + if chars[i] == " ": + i += 1 + continue + start = i + while i < len(chars) and chars[i] != " ": + idx_per_char[i] = word_idx + i += 1 + words.append((start, i, "".join(chars[start:i]))) + word_idx += 1 + return words, idx_per_char + + +def _reconstruct_word_edits_from_chars( + ref_chars: Sequence[str], + hyp_chars: Sequence[str], + char_edits: list[AlignmentEdit], +) -> tuple[list[str], list[str], list[AlignmentEdit]]: + """Convert a char-level alignment into a word-level edit list. + + Strategy: + * Tokenize ref and hyp char streams into word ranges. + * For each ref char that aligned (equal or sub), record which hyp word + it landed in. Use that to choose each ref word's primary hyp + destination (the hyp word containing most of its aligned chars). + * Group consecutive ref words with the same primary destination → N-to-1 + boundary edit. Ref word spanning >1 hyp word → 1-to-N boundary. + * Unconsumed hyp words appear as inserts in their natural order. + """ + + ref_words, ref_w_of_char = _tokenize_with_ranges(ref_chars) + hyp_words, hyp_w_of_char = _tokenize_with_ranges(hyp_chars) + + ref_to_hyp_char: list[int | None] = [None] * len(ref_chars) + for e in char_edits: + if e.op in (EditOp.EQUAL, EditOp.SUBSTITUTE): + for k in range(e.ref_end - e.ref_start): + ref_to_hyp_char[e.ref_start + k] = e.hyp_start + k + + n_ref_words = len(ref_words) + ref_dest_lists: list[list[int]] = [[] for _ in range(n_ref_words)] + ref_primary: list[int | None] = [None] * n_ref_words + for r_idx, (rs, re_, _r_word) in enumerate(ref_words): + counts: dict[int, int] = {} + order: list[int] = [] + for k in range(rs, re_): + h_pos = ref_to_hyp_char[k] + if h_pos is None or h_pos >= len(hyp_w_of_char): + continue + hw = hyp_w_of_char[h_pos] + if hw < 0: + continue + if hw not in counts: + order.append(hw) + counts[hw] = counts.get(hw, 0) + 1 + ref_dest_lists[r_idx] = order + if counts: + ref_primary[r_idx] = max(counts, key=counts.get) + + hyp_will_be_used: list[bool] = [False] * len(hyp_words) + for dests in ref_dest_lists: + for hw in dests: + hyp_will_be_used[hw] = True + + word_edits: list[AlignmentEdit] = [] + hyp_consumed: list[bool] = [False] * len(hyp_words) + next_hyp_emit = 0 + + def emit_pending_inserts(up_to_hyp_idx: int) -> None: + nonlocal next_hyp_emit + while next_hyp_emit < up_to_hyp_idx: + if not hyp_consumed[next_hyp_emit] and not hyp_will_be_used[next_hyp_emit]: + h_word = hyp_words[next_hyp_emit][2] + ref_pos = word_edits[-1].ref_end if word_edits else 0 + word_edits.append( + AlignmentEdit( + op=EditOp.INSERT, + ref_start=ref_pos, + ref_end=ref_pos, + hyp_start=next_hyp_emit, + hyp_end=next_hyp_emit + 1, + ref_units=[], + hyp_units=[h_word], + ) + ) + hyp_consumed[next_hyp_emit] = True + next_hyp_emit += 1 + + r = 0 + while r < n_ref_words: + primary = ref_primary[r] + dests = ref_dest_lists[r] + r_word = ref_words[r][2] + + if primary is None: + hyp_pos = word_edits[-1].hyp_end if word_edits else next_hyp_emit + word_edits.append( + AlignmentEdit( + op=EditOp.DELETE, + ref_start=r, + ref_end=r + 1, + hyp_start=hyp_pos, + hyp_end=hyp_pos, + ref_units=[r_word], + hyp_units=[], + ) + ) + r += 1 + continue + + emit_pending_inserts(primary) + + group_end = r + while ( + group_end + 1 < n_ref_words + and ref_primary[group_end + 1] == primary + and len(ref_dest_lists[group_end + 1]) == 1 + and len(dests) == 1 + ): + group_end += 1 + + if group_end > r: + group_ref_words = [ref_words[k][2] for k in range(r, group_end + 1)] + h_word = hyp_words[primary][2] + hyp_consumed[primary] = True + word_edits.append( + AlignmentEdit( + op=EditOp.BOUNDARY, + ref_start=r, + ref_end=group_end + 1, + hyp_start=primary, + hyp_end=primary + 1, + ref_units=group_ref_words, + hyp_units=[h_word], + ) + ) + r = group_end + 1 + next_hyp_emit = max(next_hyp_emit, primary + 1) + continue + + if len(dests) > 1: + hyp_word_strs = [hyp_words[hw][2] for hw in dests] + first_hw, last_hw = dests[0], dests[-1] + for hw in dests: + hyp_consumed[hw] = True + word_edits.append( + AlignmentEdit( + op=EditOp.BOUNDARY, + ref_start=r, + ref_end=r + 1, + hyp_start=first_hw, + hyp_end=last_hw + 1, + ref_units=[r_word], + hyp_units=hyp_word_strs, + ) + ) + next_hyp_emit = max(next_hyp_emit, last_hw + 1) + else: + h_word = hyp_words[primary][2] + hyp_consumed[primary] = True + op = EditOp.EQUAL if r_word == h_word else EditOp.SUBSTITUTE + word_edits.append( + AlignmentEdit( + op=op, + ref_start=r, + ref_end=r + 1, + hyp_start=primary, + hyp_end=primary + 1, + ref_units=[r_word], + hyp_units=[h_word], + ) + ) + next_hyp_emit = max(next_hyp_emit, primary + 1) + + r += 1 + + emit_pending_inserts(len(hyp_words)) + + merged: list[AlignmentEdit] = [] + for e in word_edits: + if ( + merged + and merged[-1].op == EditOp.DELETE + and e.op == EditOp.DELETE + and merged[-1].ref_end == e.ref_start + ): + merged[-1].ref_end = e.ref_end + merged[-1].ref_units += e.ref_units + elif ( + merged + and merged[-1].op == EditOp.INSERT + and e.op == EditOp.INSERT + and merged[-1].hyp_end == e.hyp_start + ): + merged[-1].hyp_end = e.hyp_end + merged[-1].hyp_units += e.hyp_units + else: + merged.append(e) + + ref_word_strs = [w for _, _, w in ref_words] + hyp_word_strs = [w for _, _, w in hyp_words] + return ref_word_strs, hyp_word_strs, merged + + +def _align_group_via_chars( + gt_intervals: Sequence[Interval], + ds_intervals: Sequence[Interval], + *, + text_attr: str, + normalizer: Normalizer, + separator: str = " ", + sub_cost: TokenCost = _unit_sub_cost, + overlap_tolerance_ms: float = 0.0, +) -> AlignmentResult: + """Char-level overlap-constrained alignment with word-level edit + reconstruction. Resulting AlignmentResult looks like a word-mode + alignment from the caller's perspective. + + The key feature of this alignment mode is that it can allow flexibility + on word boundaries. Spaces are just characters in the char-level DP, so + any N-to-M boundary disagreement is captured naturally and reclassified + as a `boundary` edit when reconstructing the word view. + + The char DP itself stays unit-cost (graphemes are atomic). `sub_cost` + is the word-level cost applied when scoring the reconstructed word edits, + so the reported `error_rate` is consistent with the chosen metric. + """ + + ref_chars, ref_origins, gt_sorted, ref_norm_parts = _normalize_to_chars_per_interval( + gt_intervals, + text_attr=text_attr, + normalizer=normalizer, + inter_interval_sep=separator, + ) + hyp_chars, hyp_origins, ds_sorted, hyp_norm_parts = _normalize_to_chars_per_interval( + ds_intervals, + text_attr=text_attr, + normalizer=normalizer, + inter_interval_sep=separator, + ) + + ref_norm_joined = separator.join(p for p in ref_norm_parts if p) + hyp_norm_joined = separator.join(p for p in hyp_norm_parts if p) + + if not ref_chars and not hyp_chars: + return AlignmentResult( + Granularity.WORD, + ref_norm_joined, + hyp_norm_joined, + [], + [], + [], + 0.0, + 0, + 0, + 0, + 0, + ) + + overlaps = _overlap_matrix(gt_sorted, ds_sorted, tolerance_ms=overlap_tolerance_ms) + + # Char-level DP stays unit-cost: graphemes are atomic, so a word-level + # metric has no meaning here. Word-level cost weighting happens in the + # word-token path (`_align_group_with_overlap`). + char_edits, _ch_hits, _ch_subs, _ch_ins, _ch_dels, _ = _overlap_constrained_align( + ref_chars, + ref_origins, + hyp_chars, + hyp_origins, + overlaps, + ) + + ref_word_strs, hyp_word_strs, word_edits = _reconstruct_word_edits_from_chars( + ref_chars, + hyp_chars, + char_edits, + ) + + hits = sum(e.ref_end - e.ref_start for e in word_edits if e.op == EditOp.EQUAL) + subs = sum(e.ref_end - e.ref_start for e in word_edits if e.op == EditOp.SUBSTITUTE) + dels = sum(e.ref_end - e.ref_start for e in word_edits if e.op == EditOp.DELETE) + ins = sum(e.hyp_end - e.hyp_start for e in word_edits if e.op == EditOp.INSERT) + + # Score the reconstructed word edits with the chosen cost so error_rate + # matches the cost function (BOUNDARY edits handled by _chunk_cost). + total_err = 0.0 + total_ref = 0 + for e in word_edits: + err, ref = _chunk_cost(e, granularity=Granularity.WORD, token_cost=sub_cost) + total_err += err + total_ref += ref + error_rate = total_err / total_ref if total_ref else float("nan") + + ref_word_origins: list[int] = [] + for r_s, r_e, _ in _tokenize_with_ranges(ref_chars)[0]: + ref_word_origins.append(ref_origins[r_s] if r_s < len(ref_origins) else 0) + hyp_word_origins: list[int] = [] + for h_s, h_e, _ in _tokenize_with_ranges(hyp_chars)[0]: + hyp_word_origins.append(hyp_origins[h_s] if h_s < len(hyp_origins) else 0) + + return AlignmentResult( + granularity=Granularity.WORD, + ref_normalized=ref_norm_joined, + hyp_normalized=hyp_norm_joined, + ref_units=ref_word_strs, + hyp_units=hyp_word_strs, + edits=word_edits, + error_rate=error_rate, + substitutions=subs, + insertions=ins, + deletions=dels, + hits=hits, + ref_origins=ref_word_origins, + hyp_origins=hyp_word_origins, + ) + + +def _align_group_with_overlap( + gt_intervals: Sequence[Interval], + ds_intervals: Sequence[Interval], + *, + text_attr: str, + granularity: Granularity, + normalizer: Normalizer, + separator: str, + sub_cost: TokenCost = _unit_sub_cost, + overlap_tolerance_ms: float = 0.0, +) -> AlignmentResult: + """Join mode alignment that forbids token-level matches between intervals + whose time spans don't overlap. Catches the most obvious spurious matches + (e.g. "police" at t=24s ↔ "police" at t=180s). + + `sub_cost` weights the substitution cost inside the DP, so the alignment + minimizes the chosen cost rather than the hard edit count. With the default + unit cost the result matches plain Levenshtein. At char granularity the + tokens are single graphemes, where every metric degenerates to equality, so + the weighting is a no-op there.""" + + ref_units, ref_origins, gt_sorted, ref_norm_parts = _normalize_tokenize_per_interval( + gt_intervals, + text_attr=text_attr, + normalizer=normalizer, + granularity=granularity, + inter_interval_sep=separator, + ) + hyp_units, hyp_origins, ds_sorted, hyp_norm_parts = _normalize_tokenize_per_interval( + ds_intervals, + text_attr=text_attr, + normalizer=normalizer, + granularity=granularity, + inter_interval_sep=separator, + ) + + ref_norm_joined = separator.join(p for p in ref_norm_parts if p) + hyp_norm_joined = separator.join(p for p in hyp_norm_parts if p) + + if not ref_units and not hyp_units: + return AlignmentResult( + granularity, ref_norm_joined, hyp_norm_joined, [], [], [], 0.0, 0, 0, 0, 0 + ) + if not ref_units: + return AlignmentResult( + granularity, + ref_norm_joined, + hyp_norm_joined, + [], + list(hyp_units), + [AlignmentEdit(EditOp.INSERT, 0, 0, 0, len(hyp_units), [], list(hyp_units))], + float("inf"), + 0, + len(hyp_units), + 0, + 0, + ) + if not hyp_units: + return AlignmentResult( + granularity, + ref_norm_joined, + hyp_norm_joined, + list(ref_units), + [], + [AlignmentEdit(EditOp.DELETE, 0, len(ref_units), 0, 0, list(ref_units), [])], + 1.0, + 0, + 0, + len(ref_units), + 0, + ) + + overlap = _overlap_matrix(gt_sorted, ds_sorted, tolerance_ms=overlap_tolerance_ms) + edits, hits, subs, ins, dels, total_cost = _overlap_constrained_align( + ref_units, + ref_origins, + hyp_units, + hyp_origins, + overlap, + sub_cost=sub_cost, + ) + # Rate uses the metric-weighted DP cost (total_cost), not the raw edit + # count, so it is consistent with the objective the alignment minimized. + error_rate = total_cost / len(ref_units) + return AlignmentResult( + granularity=granularity, + ref_normalized=ref_norm_joined, + hyp_normalized=hyp_norm_joined, + ref_units=list(ref_units), + hyp_units=list(hyp_units), + edits=edits, + error_rate=error_rate, + substitutions=subs, + insertions=ins, + deletions=dels, + hits=hits, + ref_origins=list(ref_origins), + hyp_origins=list(hyp_origins), + ) + + +# ============================ Entry point ======================================== + + +def match_transcriptions( + gt: Sequence[Interval], ds: Sequence[Interval], *, req: TranscriptionRequirement +) -> TranscriptionReport: + normalizer = Normalizer(req.normalizer) + token_cost = make_token_cost(req.metric, req.threshold) + + gt_groups = group_intervals(gt, attribute=req.grouping.attribute) + ds_groups = group_intervals(ds, attribute=req.grouping.attribute) + + groups: list[GroupAlignment] = [] + missing: list[tuple[GroupKey, list[Interval]]] = [] + extra: list[tuple[GroupKey, list[Interval]]] = [] + pairs: list[FilterPairAlignment] = [] + pair_gt_unmatched: list[Interval] = [] + pair_ds_unmatched: list[Interval] = [] + + all_keys = set(gt_groups) | set(ds_groups) + + match req.grouping.strategy: + case GroupingStrategy.JOIN: + for key in sorted(all_keys, key=lambda k: (k[0], k[1] or "")): + gt_group = gt_groups.get(key, []) + ds_group = ds_groups.get(key, []) + if not gt_group: + extra.append((key, ds_group)) + continue + if not ds_group: + missing.append((key, gt_group)) + continue + + if req.enforce_overlap: + aligner_params = { + "text_attr": req.text_attribute, + "normalizer": normalizer, + "separator": req.grouping.join_separator, + "sub_cost": token_cost, + "overlap_tolerance_ms": req.overlap_tolerance_ms, + } + + match (req.align, req.granularity): + case (AlignMode.CHAR, Granularity.WORD): + aligner = _align_group_via_chars + case (AlignMode.CHAR, Granularity.CHARACTER | Granularity.WORD) | ( + AlignMode.WORD, + Granularity.WORD, + ): + aligner = _align_group_with_overlap + aligner_params["granularity"] = req.granularity + case (align, granularity): + assert ( + False + ), f"Unknown alignment, granularity combination ({align, granularity})" + + alignment = aligner(gt_group, ds_group, **aligner_params) + else: + ref_text = _join_group_text( + gt_group, + text_attr=req.text_attribute, + separator=req.grouping.join_separator, + ) + hyp_text = _join_group_text( + ds_group, + text_attr=req.text_attribute, + separator=req.grouping.join_separator, + ) + alignment = _align_pair( + ref_text, + hyp_text, + granularity=req.granularity, + normalizer=normalizer, + sub_cost=token_cost, + ) + groups.append(GroupAlignment(key, gt_group, ds_group, alignment)) + + case GroupingStrategy.FILTER: + for key in sorted(all_keys, key=lambda k: (k[0], k[1] or "")): + gt_group = gt_groups.get(key, []) + ds_group = ds_groups.get(key, []) + matches, _, gt_unmatched, ds_unmatched = _two_stage_match( + gt_group, ds_group, iou_thresh=req.iou_threshold + ) + for gt_ann, ds_ann in matches: + ref_text = gt_ann.extra.get(req.text_attribute, "") + hyp_text = ds_ann.extra.get(req.text_attribute, "") + pairs.append( + FilterPairAlignment( + gt=gt_ann, + ds=ds_ann, + iou=iou(gt_ann, ds_ann), + onset_delta=ds_ann.start - gt_ann.start, + offset_delta=ds_ann.stop - gt_ann.stop, + alignment=_align_pair( + ref_text, + hyp_text, + granularity=req.granularity, + normalizer=normalizer, + sub_cost=token_cost, + ), + ) + ) + pair_gt_unmatched.extend(gt_unmatched) + pair_ds_unmatched.extend(ds_unmatched) + case grouping: + assert False, f"Unknown grouping '{grouping}'" + + return TranscriptionReport( + requirement=req, + groups=groups, + missing_groups=missing, + extra_groups=extra, + pairs=pairs, + pair_gt_unmatched=pair_gt_unmatched, + pair_ds_unmatched=pair_ds_unmatched, + ) diff --git a/packages/examples/cvat/recording-oracle/libs/audio_qe/pyproject.toml b/packages/examples/cvat/recording-oracle/libs/audio_qe/pyproject.toml new file mode 100644 index 0000000000..e9f7cf4d49 --- /dev/null +++ b/packages/examples/cvat/recording-oracle/libs/audio_qe/pyproject.toml @@ -0,0 +1,18 @@ +[tool.poetry] +name = "audio-qe" +version = "0.1.0" +description = "Audio transcription quality estimation" +authors = ["CVAT.ai Corporation"] +license = "MIT" +readme = "README.md" +packages = [{include = "audio_qe"}] + +[tool.poetry.dependencies] +python = "^3.10, <3.13" +numpy = "^1.25.2" +regex = "^2024.11.6" +scipy = "^1.13.1" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/packages/examples/cvat/recording-oracle/pyproject.toml b/packages/examples/cvat/recording-oracle/pyproject.toml index e3df56302c..ea55321ff5 100644 --- a/packages/examples/cvat/recording-oracle/pyproject.toml +++ b/packages/examples/cvat/recording-oracle/pyproject.toml @@ -26,6 +26,7 @@ starlette = ">=0.40.0" # avoid the vulnerability with multipart/form-data cvat-sdk = "2.37.0" cryptography = "<44.0.0" # human-protocol-sdk -> pgpy dep requires cryptography < 45 human-protocol-sdk = "^7.3.1" +audio-qe = {path = "libs/audio_qe", develop = true} [tool.poetry.group.dev.dependencies] pytest = "^7.4.0" From 0e97e49a1fe97c3b311efcd0b686d1f3d4c52f04 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Tue, 7 Jul 2026 19:12:07 +0300 Subject: [PATCH 25/53] Use TargetMetrics enum for audio target_metric TranscriptionTaskValidation.target_metric was a plain str; make it the TargetMetrics enum (accuracy/wer/cer) and stop stringifying it in parse_audio_manifest. Applied identically in the exchange and recording oracles. Also correct the stale `# noqa: TC001` to `TCH001` on the runtime-needed pydantic imports. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/core/tasks/audio_transcription/spec.py | 7 ++++--- .../src/core/tasks/audio_transcription/spec.py | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py index f80bfc3344..759a493fdc 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py +++ b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py @@ -6,7 +6,8 @@ from pydantic import AnyUrl, BaseModel, Field -from src.core.manifest.shared import BucketUrl # noqa: TC001 # runtime-needed by pydantic +from src.core.manifest.shared import BucketUrl # noqa: TCH001 # runtime-needed by pydantic +from src.core.manifest.v2 import TargetMetrics # noqa: TCH001 # runtime-needed by pydantic from src.core.tasks.errors import UnsupportedManifestError from src.utils.enums import BetterEnumMeta @@ -25,7 +26,7 @@ class TranscriptionTaskData(BaseModel): class TranscriptionTaskValidation(BaseModel): - target_metric: str + target_metric: TargetMetrics target_score: float @@ -129,7 +130,7 @@ def parse_audio_manifest(manifest: JobManifest) -> TranscriptionTaskSpecificatio shared_attributes=list(annotation.shared_attributes), user_guide=annotation.user_guide_url, validation=TranscriptionTaskValidation( - target_metric=validation.target_metric.value, + target_metric=validation.target_metric, target_score=validation.target_score, ), details=TranscriptionDetails(**details_fields), diff --git a/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/spec.py b/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/spec.py index f80bfc3344..759a493fdc 100644 --- a/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/spec.py +++ b/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/spec.py @@ -6,7 +6,8 @@ from pydantic import AnyUrl, BaseModel, Field -from src.core.manifest.shared import BucketUrl # noqa: TC001 # runtime-needed by pydantic +from src.core.manifest.shared import BucketUrl # noqa: TCH001 # runtime-needed by pydantic +from src.core.manifest.v2 import TargetMetrics # noqa: TCH001 # runtime-needed by pydantic from src.core.tasks.errors import UnsupportedManifestError from src.utils.enums import BetterEnumMeta @@ -25,7 +26,7 @@ class TranscriptionTaskData(BaseModel): class TranscriptionTaskValidation(BaseModel): - target_metric: str + target_metric: TargetMetrics target_score: float @@ -129,7 +130,7 @@ def parse_audio_manifest(manifest: JobManifest) -> TranscriptionTaskSpecificatio shared_attributes=list(annotation.shared_attributes), user_guide=annotation.user_guide_url, validation=TranscriptionTaskValidation( - target_metric=validation.target_metric.value, + target_metric=validation.target_metric, target_score=validation.target_score, ), details=TranscriptionDetails(**details_fields), From 118bd80dd89d833e8be99a9f5691067ce1de286c Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Tue, 7 Jul 2026 19:53:07 +0300 Subject: [PATCH 26/53] Implement audio transcription quality checker Score audio-transcription assignments in the recording oracle (WER/CER, no CVAT calls, no honeypot rotation): download per-assignment TSVs + GT + assignment mapping, align annotator vs GT per presented honeypot via the audio_qe library, and accept/reject against the manifest target score. Supporting shared-core changes (kept identical across both oracles): - TaskResultsLayout with assignment_annotation_filename(); reused by the EO validator and RO checker. - parse_gt_tsv() extracted into meta.py; reused by the EO builder and RO checker (drops the RO-local _GtRow in favour of InputGtRegion). - NormalizerPreset extended to basic + 15 Tier-1 languages. - Rename LowAccuracyError -> LowQualityError (RO). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../core/tasks/audio_transcription/meta.py | 29 ++ .../core/tasks/audio_transcription/spec.py | 18 +- .../job_completion/validators/audio.py | 7 +- .../builders/audio/transcription.py | 15 +- .../core/tasks/audio_transcription/meta.py | 29 ++ .../core/tasks/audio_transcription/spec.py | 18 +- .../src/core/validation_errors.py | 2 +- .../validation/quality_checkers/audio.py | 298 +++++++++++++++++- .../validation/quality_checkers/image.py | 4 +- .../recording-oracle/tests/unit/__init__.py | 0 .../recording-oracle/tests/unit/conftest.py | 7 + 11 files changed, 404 insertions(+), 23 deletions(-) create mode 100644 packages/examples/cvat/recording-oracle/tests/unit/__init__.py create mode 100644 packages/examples/cvat/recording-oracle/tests/unit/conftest.py diff --git a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py index c77df2c7c4..79970d73fe 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py +++ b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py @@ -7,6 +7,8 @@ from __future__ import annotations +import csv +import io import time from datetime import timedelta from enum import Enum @@ -83,6 +85,22 @@ def id(self) -> str: return f"{self.filename}:{self.row_idx}" +def parse_gt_tsv(data: bytes) -> list[InputGtRegion]: + "Parse a GT transcription TSV (columns: filename, start, stop, label, text) into regions." + reader = csv.DictReader(io.StringIO(data.decode("utf-8")), delimiter="\t") + return [ + InputGtRegion( + filename=row["filename"].strip(), + row_idx=row_idx, + start=parse_time(row["start"]), + stop=parse_time(row["stop"]), + label=(row.get("label") or "").strip() or None, + text=(row.get("text") or "").strip(), + ) + for row_idx, row in enumerate(reader) + ] + + class RegionKind(str, Enum, metaclass=BetterEnumMeta): gt = "gt" # ground-truth honeypot region ds = "ds" # regular dataset region to annotate @@ -183,6 +201,17 @@ class TaskMetaLayout: GT_CUTS_DIR = "gt_cuts" +class TaskResultsLayout: + "Layout of the escrow results dir on the oracle bucket." + + ASSIGNMENTS_DIR = "assignments" # per-assignment annotation TSVs (one per CVAT job) + + @classmethod + def assignment_annotation_filename(cls, job_id: int, assignment_id: str) -> str: + "Path (relative to the results dir) of a job's per-assignment annotation TSV." + return f"{cls.ASSIGNMENTS_DIR}/{job_id}-{assignment_id}.tsv" + + class TaskMetaSerializer: def serialize_regions(self, regions: list[PresentedRegion]) -> bytes: return dump_json([_region_to_dict(r) for r in regions]) diff --git a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py index 759a493fdc..c5d33d4770 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py +++ b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py @@ -16,7 +16,23 @@ class NormalizerPreset(str, Enum, metaclass=BetterEnumMeta): - basic = "basic" + basic = "basic" # universal Unicode + case + whitespace fold + # Tier-1 language presets (BASIC plus per-language rules) + en = "en" + es = "es" + fr = "fr" + de = "de" + it = "it" + pt = "pt" + nl = "nl" + pl = "pl" + ru = "ru" + tr = "tr" + zh = "zh" + ja = "ja" + ko = "ko" + hi = "hi" + ar = "ar" class TranscriptionTaskData(BaseModel): diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/validators/audio.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/validators/audio.py index f087ffa589..d80701f0e9 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/validators/audio.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/validators/audio.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING import src.services.cvat as cvat_service -from src.core.tasks.audio_transcription.meta import CVAT_EXPORT_FORMAT +from src.core.tasks.audio_transcription.meta import CVAT_EXPORT_FORMAT, TaskResultsLayout from src.handlers.job_completion.validators.base import JobValidator from src.handlers.job_export.downloading import download_job_annotations from src.handlers.job_export.results import ( @@ -15,9 +15,6 @@ if TYPE_CHECKING: from src.models.cvat import Job -# subdir (in the escrow results dir) for the per-assignment transcription TSVs -ASSIGNMENTS_DIR = "assignments" - class AudioTranscriptionJobValidator(JobValidator): def _save_annotation_results(self) -> None: @@ -52,6 +49,6 @@ def _save_annotation_results(self) -> None: def _make_annotation_descriptor(self, job: Job, annotations: FileDescriptor) -> FileDescriptor: assignment = job.latest_assignment return FileDescriptor( - filename=f"{ASSIGNMENTS_DIR}/{job.cvat_id}-{assignment.id}.tsv", + filename=TaskResultsLayout.assignment_annotation_filename(job.cvat_id, assignment.id), file=annotations.file, ) diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py index ac55a1cca8..3e3a74e607 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py @@ -24,6 +24,7 @@ RegionKind, TaskMetaLayout, TaskMetaSerializer, + parse_gt_tsv, parse_time, ) from src.core.tasks.audio_transcription.spec import parse_audio_manifest @@ -632,18 +633,8 @@ def _parse_regions_tsv(data: bytes) -> dict[str, list[InputRegion]]: def _parse_gt_tsv(data: bytes) -> dict[str, list[InputGtRegion]]: by_file: dict[str, list[InputGtRegion]] = {} - for row_idx, row in enumerate(_read_tsv(data)): - filename = row["filename"].strip() - by_file.setdefault(filename, []).append( - InputGtRegion( - filename=filename, - row_idx=row_idx, - start=parse_time(row["start"]), - stop=parse_time(row["stop"]), - label=(row.get("label") or "").strip() or None, - text=(row.get("text") or "").strip(), - ) - ) + for region in parse_gt_tsv(data): + by_file.setdefault(region.filename, []).append(region) return by_file diff --git a/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/meta.py b/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/meta.py index c77df2c7c4..79970d73fe 100644 --- a/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/meta.py +++ b/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/meta.py @@ -7,6 +7,8 @@ from __future__ import annotations +import csv +import io import time from datetime import timedelta from enum import Enum @@ -83,6 +85,22 @@ def id(self) -> str: return f"{self.filename}:{self.row_idx}" +def parse_gt_tsv(data: bytes) -> list[InputGtRegion]: + "Parse a GT transcription TSV (columns: filename, start, stop, label, text) into regions." + reader = csv.DictReader(io.StringIO(data.decode("utf-8")), delimiter="\t") + return [ + InputGtRegion( + filename=row["filename"].strip(), + row_idx=row_idx, + start=parse_time(row["start"]), + stop=parse_time(row["stop"]), + label=(row.get("label") or "").strip() or None, + text=(row.get("text") or "").strip(), + ) + for row_idx, row in enumerate(reader) + ] + + class RegionKind(str, Enum, metaclass=BetterEnumMeta): gt = "gt" # ground-truth honeypot region ds = "ds" # regular dataset region to annotate @@ -183,6 +201,17 @@ class TaskMetaLayout: GT_CUTS_DIR = "gt_cuts" +class TaskResultsLayout: + "Layout of the escrow results dir on the oracle bucket." + + ASSIGNMENTS_DIR = "assignments" # per-assignment annotation TSVs (one per CVAT job) + + @classmethod + def assignment_annotation_filename(cls, job_id: int, assignment_id: str) -> str: + "Path (relative to the results dir) of a job's per-assignment annotation TSV." + return f"{cls.ASSIGNMENTS_DIR}/{job_id}-{assignment_id}.tsv" + + class TaskMetaSerializer: def serialize_regions(self, regions: list[PresentedRegion]) -> bytes: return dump_json([_region_to_dict(r) for r in regions]) diff --git a/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/spec.py b/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/spec.py index 759a493fdc..c5d33d4770 100644 --- a/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/spec.py +++ b/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/spec.py @@ -16,7 +16,23 @@ class NormalizerPreset(str, Enum, metaclass=BetterEnumMeta): - basic = "basic" + basic = "basic" # universal Unicode + case + whitespace fold + # Tier-1 language presets (BASIC plus per-language rules) + en = "en" + es = "es" + fr = "fr" + de = "de" + it = "it" + pt = "pt" + nl = "nl" + pl = "pl" + ru = "ru" + tr = "tr" + zh = "zh" + ja = "ja" + ko = "ko" + hi = "hi" + ar = "ar" class TranscriptionTaskData(BaseModel): diff --git a/packages/examples/cvat/recording-oracle/src/core/validation_errors.py b/packages/examples/cvat/recording-oracle/src/core/validation_errors.py index 40ad8c1d39..0d05ba7a0f 100644 --- a/packages/examples/cvat/recording-oracle/src/core/validation_errors.py +++ b/packages/examples/cvat/recording-oracle/src/core/validation_errors.py @@ -11,7 +11,7 @@ def __str__(self) -> str: ) -class LowAccuracyError(DatasetValidationError): +class LowQualityError(DatasetValidationError): pass diff --git a/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/audio.py b/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/audio.py index 0e85ddd8c3..6aad32e374 100644 --- a/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/audio.py +++ b/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/audio.py @@ -1,8 +1,304 @@ from __future__ import annotations +import csv +import io +from typing import TYPE_CHECKING + +from audio_qe import ( + Granularity, + GroupingConfig, + GroupingStrategy, + Interval, + Metric, + Normalizer, + NormalizerConfig, + NormalizerMode, + TranscriptionRequirement, + lang_preset, + match_transcriptions, +) +from audio_qe.data import AlignMode +from audio_qe.transcription_matching import aggregate_metric, tokenize + +from src.core.config import Config +from src.core.manifest.v2 import TargetMetrics +from src.core.storage import compose_data_bucket_filename, compose_results_bucket_filename +from src.core.tasks.audio_transcription.meta import ( + InputGtRegion, + RegionKind, + TaskMetaLayout, + TaskMetaSerializer, + TaskResultsLayout, + parse_gt_tsv, + parse_time, +) +from src.core.tasks.audio_transcription.spec import NormalizerPreset, parse_audio_manifest +from src.core.validation_errors import LowQualityError, TooFewGtError +from src.handlers.validation.common import UNKNOWN_QUALITY from src.handlers.validation.quality_checkers.base import TaskQualityChecker +from src.services.cloud import make_client as make_cloud_client +from src.services.cloud.utils import BucketAccessInfo + +if TYPE_CHECKING: + from src.core.annotation_meta import JobMeta + from src.core.tasks.audio_transcription.meta import Assignment, PlacedRegion + from src.services.cloud.client import StorageClient + +# Groups are keyed by the presented honeypot so each honeypot's transcription is aligned in +# isolation (no cross-honeypot / cross-file overlap). +_GROUP_ATTR = "honeypot_id" class AudioTaskQualityChecker(TaskQualityChecker): + """ + Scores audio-transcription assignments. CVAT doesn't support quality computation for such tasks + yet, so the computations are performed in the oracle. + + For each job it reconstructs the annotator's honeypot transcriptions (from the per-assignment + TSV + the assignment/clip mapping), aligns them against the ground truth per presented honeypot, + and computes the WER/CER error rate. + + Honeypot rotation is not used. + """ + + ALLOWED_METRICS = (TargetMetrics.wer, TargetMetrics.cer) + + _METRIC_PARAMS = { + TargetMetrics.wer: { + "granularity": Granularity.WORD, + "align": AlignMode.WORD, + "metric": Metric.EQUALITY, + }, + TargetMetrics.cer: { + "granularity": Granularity.CHARACTER, + "align": AlignMode.CHAR, + "metric": Metric.EQUALITY, + } + } + def _validate_jobs(self) -> None: - raise NotImplementedError("audio_transcription validation not implemented yet") + spec = parse_audio_manifest(self.manifest) + + metric = spec.validation.target_metric + if metric not in self.ALLOWED_METRICS: + raise NotImplementedError( + "Audio transcription validation only supports {} metrics, got '{}'".format( + ', '.join(v.value for v in self.ALLOWED_METRICS), + metric.value + ) + ) + + target = spec.validation.target_score + transcription_attr = spec.details.transcription_attr_name + + req = TranscriptionRequirement( + **self._METRIC_PARAMS[metric], + text_attribute=transcription_attr, + normalizer=self._normalizer_config(spec.details.normalizer), + grouping=GroupingConfig(attribute=_GROUP_ATTR, strategy=GroupingStrategy.JOIN), + ) + normalizer = Normalizer(req.normalizer) + + client = make_cloud_client( + BucketAccessInfo.parse_obj(Config.exchange_oracle_storage_config) + ) + gt_rows = self._load_gt(client) + assignments = self._load_assignments(client) + annotations = self._download_assignment_tsvs(client) + + job_results: dict[int, float] = {} + rejected_jobs: dict[int, object] = {} + + for job_meta in self._meta.jobs: + gt_intervals, hyp_intervals = self._build_intervals( + annotations[job_meta.job_id], assignments, gt_rows, transcription_attr + ) + if not gt_intervals: + job_results[job_meta.job_id] = UNKNOWN_QUALITY + rejected_jobs[job_meta.job_id] = TooFewGtError() + continue + + report = match_transcriptions(gt_intervals, hyp_intervals, req=req) + rate = self._job_error_rate(report, req, normalizer) + job_results[job_meta.job_id] = rate + + # WER/CER are lower-is-better; reject when the error rate exceeds the target. + if rate > target: + rejected_jobs[job_meta.job_id] = LowQualityError() + + self._job_results = job_results + self._rejected_jobs = rejected_jobs + + # No honeypot rotation for audio: empty gt_stats skips the rotation branch in + # process_intermediate_results; the layout maps are unused but must be set for validate(). + self._gt_stats = {} + self._task_id_to_val_layout = {} + self._task_id_to_honeypots_mapping = {} + self._task_id_to_sequence_of_frame_names = {} + self._task_id_to_labels = {} + + @staticmethod + def _normalizer_config(preset: NormalizerPreset | None) -> NormalizerConfig: + # Absent normalizer = passthrough; basic = built-in mode; a language code resolves to its + # preset (BASIC plus per-language rules). + if preset is None: + return NormalizerConfig(mode=NormalizerMode.NONE) + if preset == NormalizerPreset.basic: + return NormalizerConfig(mode=NormalizerMode.BASIC) + return lang_preset(preset.value) + + def _load_gt(self, client: StorageClient) -> dict[str, InputGtRegion]: + data = client.download_file( + compose_data_bucket_filename( + self.escrow_address, self.chain_id, TaskMetaLayout.GT_FILENAME + ) + ) + return {region.id: region for region in parse_gt_tsv(data)} + + def _load_assignments(self, client: StorageClient) -> dict[str, Assignment]: + data = client.download_file( + compose_data_bucket_filename( + self.escrow_address, self.chain_id, TaskMetaLayout.ASSIGNMENTS_FILENAME + ) + ) + return {a.clip_filename: a for a in TaskMetaSerializer().parse_assignments(data)} + + def _build_intervals( + self, + annotation: bytes, + assignments: dict[str, Assignment], + gt_rows: dict[str, InputGtRegion], + attr: str, + ) -> tuple[list[Interval], list[Interval]]: + rows = list(csv.DictReader(io.StringIO(annotation.decode("utf-8")), delimiter="\t")) + + assignment = self._resolve_assignment(rows, assignments) + if assignment is None: + return [], [] + + gt_placements = [p for p in assignment.placed if p.kind == RegionKind.gt] + + # Ground-truth reference intervals for every presented honeypot (built regardless of what + # the annotator produced, so an omitted honeypot becomes a missing group = full errors). + gt_intervals: list[Interval] = [] + counter = 0 + for placed in gt_placements: + honeypot_id = self._honeypot_id(placed) + for region_id in placed.input_ids: + gt = gt_rows.get(region_id) + if gt is None: + continue + gt_intervals.append( + Interval( + id=counter, + start=gt.start.total_seconds() * 1000.0, + stop=gt.stop.total_seconds() * 1000.0, + label=gt.label or "", + extra={attr: gt.text, _GROUP_ATTR: honeypot_id}, + ) + ) + counter += 1 + + # Annotator (hypothesis) intervals: only rows falling inside a honeypot window, mapped from + # clip-time back to source-file time. + hyp_intervals: list[Interval] = [] + for row in rows: + start_s = parse_time(row["start"]).total_seconds() + stop_s = parse_time(row["stop"]).total_seconds() + placed = self._placed_at(assignment, start_s) + if placed is None or placed.kind != RegionKind.gt: + continue + offset = placed.file_start - placed.clip_start + hyp_intervals.append( + Interval( + id=counter, + start=(start_s + offset) * 1000.0, + stop=(stop_s + offset) * 1000.0, + label=row.get("label", "") or "", + extra={ + attr: row.get(attr, "") or "", + _GROUP_ATTR: self._honeypot_id(placed), + }, + ) + ) + counter += 1 + + return gt_intervals, hyp_intervals + + def _download_assignment_tsvs(self, client: StorageClient) -> dict[int, bytes]: + "Download every job's per-assignment annotation TSV up front, keyed by cvat job id." + return { + job_meta.job_id: self._download_assignment_tsv(client, job_meta) + for job_meta in self._meta.jobs + } + + def _download_assignment_tsv(self, client: StorageClient, job_meta: JobMeta) -> bytes: + return client.download_file( + compose_results_bucket_filename( + self.escrow_address, + self.chain_id, + TaskResultsLayout.assignment_annotation_filename( + job_meta.job_id, job_meta.assignment_id + ), + ) + ) + + @staticmethod + def _resolve_assignment( + rows: list[dict], assignments: dict[str, Assignment] + ) -> Assignment | None: + for row in rows: + clip = row["filename"].rsplit("/", 1)[-1] + if clip in assignments: + return assignments[clip] + return None + + @staticmethod + def _honeypot_id(placed: PlacedRegion) -> str: + # A gt placement is always built from >=1 GT input ROI, so input_ids is non-empty. + assert placed.input_ids, f"honeypot placement has no input regions: {placed}" + return ",".join(placed.input_ids) + + @staticmethod + def _placed_at(assignment: Assignment, clip_time_s: float) -> PlacedRegion | None: + return next( + (p for p in assignment.placed if p.clip_start <= clip_time_s < p.clip_stop), + None, + ) + + def _job_error_rate(self, report, req: TranscriptionRequirement, normalizer) -> float: + """ + Micro-averaged error rate over the assignment's honeypots. The library's ``corpus_rate`` + only aggregates matched groups, so missing groups (honeypot the annotator left empty) are + added as full deletions and extra groups as insertions (mirrors the CVAT accumulation). + """ + total_err = 0.0 + total_ref = 0 + + matched_ref = sum(len(a.ref_units) for a in report.alignments) + if matched_ref: + _, matched_rate = aggregate_metric( + report.alignments, + metric=req.metric, + threshold=req.threshold, + granularity=req.granularity, + ) + total_err += matched_rate * matched_ref + total_ref += matched_ref + + for _key, gt_group in report.missing_groups: + units = self._group_units(gt_group, req, normalizer) + total_err += len(units) + total_ref += len(units) + + for _key, ds_group in report.extra_groups: + total_err += len(self._group_units(ds_group, req, normalizer)) + + return total_err / total_ref if total_ref else 0.0 + + @staticmethod + def _group_units(group: list[Interval], req: TranscriptionRequirement, normalizer) -> list[str]: + joined = req.grouping.join_separator.join( + iv.extra.get(req.text_attribute, "") for iv in sorted(group, key=lambda i: i.start) + ) + return tokenize(normalizer(joined), granularity=req.granularity) diff --git a/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/image.py b/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/image.py index 337480d934..4851724c97 100644 --- a/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/image.py +++ b/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/image.py @@ -4,7 +4,7 @@ import src.cvat.api_calls as cvat_api from src.core.gt_stats import GtKey, ValidationFrameStats -from src.core.validation_errors import LowAccuracyError, TooFewGtError +from src.core.validation_errors import LowQualityError, TooFewGtError from src.handlers.validation.common import UNKNOWN_QUALITY from src.handlers.validation.quality_checkers.base import TaskQualityChecker @@ -107,7 +107,7 @@ def _validate_jobs(self) -> None: job_results[cvat_job_id] = accuracy if accuracy < min_quality: - rejected_jobs[cvat_job_id] = LowAccuracyError() + rejected_jobs[cvat_job_id] = LowQualityError() for gt_stat in self._gt_stats.values(): gt_stat.total_uses = max( diff --git a/packages/examples/cvat/recording-oracle/tests/unit/__init__.py b/packages/examples/cvat/recording-oracle/tests/unit/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/examples/cvat/recording-oracle/tests/unit/conftest.py b/packages/examples/cvat/recording-oracle/tests/unit/conftest.py new file mode 100644 index 0000000000..e83292c355 --- /dev/null +++ b/packages/examples/cvat/recording-oracle/tests/unit/conftest.py @@ -0,0 +1,7 @@ +import pytest + + +@pytest.fixture(autouse=True) +def db(): + # Override the root autouse DB fixture: unit tests here need no database. + yield From 9592f8be63de67acbf54a945eec534eb66b5dd41 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Tue, 7 Jul 2026 19:57:07 +0300 Subject: [PATCH 27/53] Set poetry package-mode = false in the CVAT oracle examples The oracle apps run from src/ and are never installed as a package, but pyproject declared packages = [{include = "_oracle"}] pointing at a non-existent top-level dir, so `poetry install` failed/warned with 'does not contain any element'. Mark both projects non-package so install resolves dependencies only. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/examples/cvat/exchange-oracle/pyproject.toml | 3 +-- packages/examples/cvat/recording-oracle/pyproject.toml | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/examples/cvat/exchange-oracle/pyproject.toml b/packages/examples/cvat/exchange-oracle/pyproject.toml index 7d2970cda7..718d9713ea 100644 --- a/packages/examples/cvat/exchange-oracle/pyproject.toml +++ b/packages/examples/cvat/exchange-oracle/pyproject.toml @@ -3,8 +3,7 @@ name = "exchange-oracle" version = "0.1.0" description = "An example of decentralized exchange oracle with CVAT as annotation instrument" authors = ["Sergey Dzeranov "] -readme = "README.md" -packages = [{include = "exchange_oracle"}] +package-mode = false [tool.poetry.dependencies] python = "^3.10,<3.13" diff --git a/packages/examples/cvat/recording-oracle/pyproject.toml b/packages/examples/cvat/recording-oracle/pyproject.toml index ea55321ff5..27659af0ba 100644 --- a/packages/examples/cvat/recording-oracle/pyproject.toml +++ b/packages/examples/cvat/recording-oracle/pyproject.toml @@ -3,8 +3,7 @@ name = "recording-oracle" version = "0.1.0" description = "An example of recording with with CVAT as an annotation instrument" authors = ["Sergey Dzeranov ", "Marius Hamacher "] -readme = "README.md" -packages = [{include = "recording_oracle"}] +package-mode = false [tool.poetry.dependencies] python = "^3.10, <3.13" From c79509124b23daecd8a7456ed25579362d4b090e Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Tue, 7 Jul 2026 20:17:32 +0300 Subject: [PATCH 28/53] Move RO entrypoints under src/entrypoints; use in-project venvs Mirror the exchange oracle layout in the recording oracle: relocate the root run.py / debug.py to src/entrypoints/ and launch them via 'python -m src.entrypoints.{run,debug}' in the start scripts. Recommend in-project virtualenvs (poetry config virtualenvs.in-project true) in both READMEs so editors/debuggers pick up the .venv interpreter (needed for the RO's local audio-qe path dependency), and ignore /.venv in the exchange oracle .gitignore. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/examples/cvat/exchange-oracle/.gitignore | 2 ++ packages/examples/cvat/exchange-oracle/README.md | 2 +- packages/examples/cvat/recording-oracle/.gitignore | 1 + packages/examples/cvat/recording-oracle/README.md | 7 +++---- packages/examples/cvat/recording-oracle/bin/start_debug.sh | 2 +- packages/examples/cvat/recording-oracle/bin/start_dev.sh | 2 +- .../cvat/recording-oracle/src/entrypoints/__init__.py | 0 .../cvat/recording-oracle/{ => src/entrypoints}/debug.py | 0 .../cvat/recording-oracle/{ => src/entrypoints}/run.py | 0 9 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 packages/examples/cvat/recording-oracle/src/entrypoints/__init__.py rename packages/examples/cvat/recording-oracle/{ => src/entrypoints}/debug.py (100%) rename packages/examples/cvat/recording-oracle/{ => src/entrypoints}/run.py (100%) diff --git a/packages/examples/cvat/exchange-oracle/.gitignore b/packages/examples/cvat/exchange-oracle/.gitignore index 12f5b4f876..bc7f61aa0e 100644 --- a/packages/examples/cvat/exchange-oracle/.gitignore +++ b/packages/examples/cvat/exchange-oracle/.gitignore @@ -1,3 +1,5 @@ +/.venv + # Byte-compiled / optimized / DLL files __pycache__/ .pytest_cache diff --git a/packages/examples/cvat/exchange-oracle/README.md b/packages/examples/cvat/exchange-oracle/README.md index ba04d13e11..d0165e2a63 100644 --- a/packages/examples/cvat/exchange-oracle/README.md +++ b/packages/examples/cvat/exchange-oracle/README.md @@ -4,7 +4,7 @@ Prerequisites: ``` -1. poetry shell +1. poetry config virtualenvs.in-project true # create .venv inside the project so editors/debuggers pick it up 2. poetry install 3. pre-commit install 4. Make sure you have postgres-devel packages installed on your OS. It is required for psycopg2 build phase. diff --git a/packages/examples/cvat/recording-oracle/.gitignore b/packages/examples/cvat/recording-oracle/.gitignore index a8457398f9..f54bfa1040 100644 --- a/packages/examples/cvat/recording-oracle/.gitignore +++ b/packages/examples/cvat/recording-oracle/.gitignore @@ -1,4 +1,5 @@ /.venv + # Byte-compiled / optimized / DLL files __pycache__/ .pytest_cache diff --git a/packages/examples/cvat/recording-oracle/README.md b/packages/examples/cvat/recording-oracle/README.md index 77bee0b36d..d32bdf67ec 100644 --- a/packages/examples/cvat/recording-oracle/README.md +++ b/packages/examples/cvat/recording-oracle/README.md @@ -4,14 +4,13 @@ Prerequisites: ``` -1. poetry shell +1. poetry config virtualenvs.in-project true # create .venv inside the project so editors/debuggers pick it up 2. poetry install 3. pre-commit install 4. Make sure you have postgres-devel packages installed on your OS. It is required for psycopg2 build phase. `libpq-dev` in Debian/Ubuntu, `libpq-devel` on Centos/Fedora/Cygwin/Babun.) `postgres` package in the homebrew for macOS -``` - +``` For deployment it is required to have PostgreSQL(v14.4) @@ -30,7 +29,7 @@ docker compose -f docker-compose.dev.yml up -d ./bin/start_debug.sh ``` -When running service from `./bin/start_debug.sh` (`debug.py`), simplified development flow is available: +When running service from `./bin/start_debug.sh`, simplified development flow is available: - When webhook signature is required, `{oracle_name}:unique_string` can be used - You can upload manifest.json to minio `manifests` bucket and use its filename as an escrow_address diff --git a/packages/examples/cvat/recording-oracle/bin/start_debug.sh b/packages/examples/cvat/recording-oracle/bin/start_debug.sh index ff93f4765c..a7d31fdc4f 100755 --- a/packages/examples/cvat/recording-oracle/bin/start_debug.sh +++ b/packages/examples/cvat/recording-oracle/bin/start_debug.sh @@ -1,4 +1,4 @@ export ENVIRONMENT=development alembic upgrade head -python debug.py \ No newline at end of file +python -m src.entrypoints.debug \ No newline at end of file diff --git a/packages/examples/cvat/recording-oracle/bin/start_dev.sh b/packages/examples/cvat/recording-oracle/bin/start_dev.sh index 64f2bd6fe1..66f642002c 100755 --- a/packages/examples/cvat/recording-oracle/bin/start_dev.sh +++ b/packages/examples/cvat/recording-oracle/bin/start_dev.sh @@ -1,4 +1,4 @@ export ENVIRONMENT=development alembic upgrade head -python run.py \ No newline at end of file +python -m src.entrypoints.run \ No newline at end of file diff --git a/packages/examples/cvat/recording-oracle/src/entrypoints/__init__.py b/packages/examples/cvat/recording-oracle/src/entrypoints/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/examples/cvat/recording-oracle/debug.py b/packages/examples/cvat/recording-oracle/src/entrypoints/debug.py similarity index 100% rename from packages/examples/cvat/recording-oracle/debug.py rename to packages/examples/cvat/recording-oracle/src/entrypoints/debug.py diff --git a/packages/examples/cvat/recording-oracle/run.py b/packages/examples/cvat/recording-oracle/src/entrypoints/run.py similarity index 100% rename from packages/examples/cvat/recording-oracle/run.py rename to packages/examples/cvat/recording-oracle/src/entrypoints/run.py From 6af734c813ccb20d7a2162db4bb9644df5165cce Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Wed, 8 Jul 2026 13:59:00 +0300 Subject: [PATCH 29/53] Add audio final-result export in the recording oracle Restructure the escrow_recorded/export flow into a completion/ package (handlers + final_results + task_exporters/), mirroring validation/. Add a task-dispatched TaskExporter family: the image exporter wraps the existing datumaro GT merge; the audio exporter appends the ground-truth rows from gt.tsv to the exchange oracle's annotations.tsv (GT copied into the output), sorted per file+time with reassigned ids and a 'source' column marking each row as 'annotation' or 'gt', packed into the standard resulting_annotations.zip. Share the merged-annotations filename via TaskResultsLayout.ANNOTATIONS_FILENAME across both oracles. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../core/tasks/audio_transcription/meta.py | 1 + .../handlers/job_export/exporters/audio.py | 8 +- .../core/tasks/audio_transcription/meta.py | 1 + .../src/handlers/completion/__init__.py | 3 + .../src/handlers/completion/final_results.py | 109 ++++++++++++++++ .../{completion.py => completion/handlers.py} | 51 ++------ .../completion/task_exporters/__init__.py | 0 .../completion/task_exporters/audio.py | 75 +++++++++++ .../completion/task_exporters/base.py | 49 +++++++ .../completion/task_exporters/factory.py | 40 ++++++ .../task_exporters/image.py} | 120 ++---------------- .../src/handlers/validation/__init__.py | 2 - .../services/test_validation_service.py | 42 +++--- 13 files changed, 329 insertions(+), 172 deletions(-) create mode 100644 packages/examples/cvat/recording-oracle/src/handlers/completion/__init__.py create mode 100644 packages/examples/cvat/recording-oracle/src/handlers/completion/final_results.py rename packages/examples/cvat/recording-oracle/src/handlers/{completion.py => completion/handlers.py} (76%) create mode 100644 packages/examples/cvat/recording-oracle/src/handlers/completion/task_exporters/__init__.py create mode 100644 packages/examples/cvat/recording-oracle/src/handlers/completion/task_exporters/audio.py create mode 100644 packages/examples/cvat/recording-oracle/src/handlers/completion/task_exporters/base.py create mode 100644 packages/examples/cvat/recording-oracle/src/handlers/completion/task_exporters/factory.py rename packages/examples/cvat/recording-oracle/src/handlers/{validation/final_results.py => completion/task_exporters/image.py} (67%) diff --git a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py index 79970d73fe..8c39abd5d2 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py +++ b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py @@ -205,6 +205,7 @@ class TaskResultsLayout: "Layout of the escrow results dir on the oracle bucket." ASSIGNMENTS_DIR = "assignments" # per-assignment annotation TSVs (one per CVAT job) + ANNOTATIONS_FILENAME = "annotations.tsv" # merged final annotations (DS rows + GT) @classmethod def assignment_annotation_filename(cls, job_id: int, assignment_id: str) -> str: diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/audio.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/audio.py index 761e6f117e..5d45da510d 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/audio.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/audio.py @@ -12,6 +12,7 @@ CVAT_EXPORT_FORMAT, TaskMetaLayout, TaskMetaSerializer, + TaskResultsLayout, format_time, parse_time, ) @@ -30,9 +31,6 @@ from src.core.tasks.audio_transcription.meta import Assignment, PlacedRegion -# merged resulting annotations file (in the escrow results dir), GT-annotations structure -RESULTING_ANNOTATIONS_FILE = "annotations.tsv" - _LEADING_COLUMNS = ["id", "input_region_ids", "filename", "start", "stop", "label"] @@ -69,7 +67,9 @@ def export(self) -> None: self.logger.debug(f"Uploading merged annotations for the escrow ({self.escrow_address=})") upload_escrow_results( files=[ - FileDescriptor(filename=RESULTING_ANNOTATIONS_FILE, file=io.BytesIO(merged)), + FileDescriptor( + filename=TaskResultsLayout.ANNOTATIONS_FILENAME, file=io.BytesIO(merged) + ), prepare_annotation_metafile(jobs=jobs), ], chain_id=self.chain_id, diff --git a/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/meta.py b/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/meta.py index 79970d73fe..e612a2c0a0 100644 --- a/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/meta.py +++ b/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/meta.py @@ -205,6 +205,7 @@ class TaskResultsLayout: "Layout of the escrow results dir on the oracle bucket." ASSIGNMENTS_DIR = "assignments" # per-assignment annotation TSVs (one per CVAT job) + ANNOTATIONS_FILENAME = "annotations.tsv" # merged annotations (without honeypots or GT) @classmethod def assignment_annotation_filename(cls, job_id: int, assignment_id: str) -> str: diff --git a/packages/examples/cvat/recording-oracle/src/handlers/completion/__init__.py b/packages/examples/cvat/recording-oracle/src/handlers/completion/__init__.py new file mode 100644 index 0000000000..6f744a8a50 --- /dev/null +++ b/packages/examples/cvat/recording-oracle/src/handlers/completion/__init__.py @@ -0,0 +1,3 @@ +from src.handlers.completion.handlers import export_results + +__all__ = ["export_results"] diff --git a/packages/examples/cvat/recording-oracle/src/handlers/completion/final_results.py b/packages/examples/cvat/recording-oracle/src/handlers/completion/final_results.py new file mode 100644 index 0000000000..4af77047b3 --- /dev/null +++ b/packages/examples/cvat/recording-oracle/src/handlers/completion/final_results.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np + +import src.services.validation as db_service +from src.core.validation_meta import JobMeta, ResultMeta, ValidationMeta +from src.core.validation_results import FinalResult +from src.db.utils import ForUpdateParams +from src.handlers.completion.task_exporters.factory import create_exporter +from src.handlers.validation.common import UNKNOWN_QUALITY, _JobResults + +if TYPE_CHECKING: + import logging + + from sqlalchemy.orm import Session + + from src.core.annotation_meta import AnnotationMeta + from src.core.manifest import ManifestBase + + +def process_final_results( + session: Session, + *, + escrow_address: str, + chain_id: int, + meta: AnnotationMeta, + manifest: ManifestBase, + logger: logging.Logger, +) -> FinalResult: + assert logger # unused + + task = db_service.get_task_by_escrow_address( + session, + escrow_address, + for_update=ForUpdateParams( + nowait=True + ), # should not happen, but waiting should not block processing + ) + if not task: + raise AssertionError(f"Validation results for escrow {escrow_address} not found") + + exporter = create_exporter(manifest, escrow_address, chain_id, session) + resulting_annotations = exporter.export() + + job_final_result_ids: dict[str, str] = {} + + for job_meta in meta.jobs: + job = db_service.get_job_by_cvat_id(session, job_meta.job_id) + if not job: + raise AssertionError( + f"Can't find validation results for job " f"{job_meta.job_id} ({escrow_address=})" + ) + + assignment_validation_result = db_service.get_validation_result_by_assignment_id( + session, job_meta.assignment_id + ) + if not assignment_validation_result: + raise AssertionError( + f"Can't find validation results for assignments " + f"{job_meta.assignment_id} ({escrow_address=})" + ) + + job_final_result_ids[job.id] = assignment_validation_result.id + + task_jobs = task.jobs + + task_validation_results = db_service.get_task_validation_results(session, task.id) + + job_id_to_meta_id = {job.id: i for i, job in enumerate(task_jobs)} + + validation_result_id_to_meta_id = {r.id: i for i, r in enumerate(task_validation_results)} + + validation_meta = ValidationMeta( + jobs=[ + JobMeta( + job_id=job_id_to_meta_id[job.id], + final_result_id=validation_result_id_to_meta_id[job_final_result_ids[job.id]], + ) + for job in task_jobs + ], + results=[ + ResultMeta( + id=validation_result_id_to_meta_id[r.id], + job_id=job_id_to_meta_id[r.job.id], + annotator_wallet_address=r.annotator_wallet_address, + annotation_quality=r.annotation_quality, + ) + for r in task_validation_results + ], + ) + + # Include final results for all jobs + job_results: _JobResults = { + job.cvat_id: task_validation_results[ + validation_result_id_to_meta_id[job_final_result_ids[job.id]] + ].annotation_quality + for job in task_jobs + } + + return FinalResult( + job_results=job_results, + validation_meta=validation_meta, + resulting_annotations=resulting_annotations, + average_quality=np.mean( + [v for v in job_results.values() if v != UNKNOWN_QUALITY and v >= 0] or [0] + ), + ) diff --git a/packages/examples/cvat/recording-oracle/src/handlers/completion.py b/packages/examples/cvat/recording-oracle/src/handlers/completion/handlers.py similarity index 76% rename from packages/examples/cvat/recording-oracle/src/handlers/completion.py rename to packages/examples/cvat/recording-oracle/src/handlers/completion/handlers.py index 00efffdb25..ef51d1d71f 100644 --- a/packages/examples/cvat/recording-oracle/src/handlers/completion.py +++ b/packages/examples/cvat/recording-oracle/src/handlers/completion/handlers.py @@ -10,19 +10,14 @@ from src.chain import escrow from src.core.config import Config from src.core.manifest import ManifestBase, parse_manifest -from src.core.oracle_events import ( - RecordingOracleEvent_JobCompleted, -) +from src.core.oracle_events import RecordingOracleEvent_JobCompleted from src.core.storage import ( compose_results_bucket_filename as compose_annotation_results_bucket_filename, ) from src.core.types import OracleWebhookTypes from src.core.validation_results import FinalResult -from src.handlers.validation import ( - parse_annotation_metafile, - process_final_results, - serialize_validation_meta, -) +from src.handlers.completion.final_results import process_final_results +from src.handlers.validation import parse_annotation_metafile, serialize_validation_meta from src.log import ROOT_LOGGER_NAME from src.services.cloud import make_client as make_cloud_client from src.services.cloud.utils import BucketAccessInfo @@ -32,7 +27,7 @@ module_logger_name = f"{ROOT_LOGGER_NAME}.cron.webhook" -class _TaskUploader: +class _EscrowExporter: def __init__( self, escrow_address: str, chain_id: int, manifest: ManifestBase, db_session: Session ) -> None: @@ -45,7 +40,6 @@ def __init__( self.data_bucket = BucketAccessInfo.parse_obj(Config.exchange_oracle_storage_config) self.annotation_meta: annotation.AnnotationMeta | None = None - self.merged_annotations: bytes | None = None def set_logger(self, logger: Logger): self.logger = logger @@ -61,55 +55,32 @@ def _download_results_meta(self): annotation_metafile_data = data_bucket_client.download_file(annotation_meta_path) self.annotation_meta = parse_annotation_metafile(io.BytesIO(annotation_metafile_data)) - def _download_annotations(self): - assert self.annotation_meta is not None - - data_bucket_client = make_cloud_client(self.data_bucket) - - exchange_oracle_merged_annotation_path = compose_annotation_results_bucket_filename( - self.escrow_address, - self.chain_id, - annotation.RESULTING_ANNOTATIONS_FILE, - ) - merged_annotations = data_bucket_client.download_file( - exchange_oracle_merged_annotation_path - ) - - self.merged_annotations = merged_annotations - def _download_results(self): self._download_results_meta() - self._download_annotations() def _process_annotation_results(self) -> FinalResult: assert self.annotation_meta is not None - assert self.merged_annotations is not None return process_final_results( session=self.db_session, escrow_address=self.escrow_address, chain_id=self.chain_id, meta=self.annotation_meta, - merged_annotations=io.BytesIO(self.merged_annotations), manifest=self.manifest, logger=self.logger, ) - def upload(self): + def export(self): self._download_results() - validation_result = self._process_annotation_results() + export_result = self._process_annotation_results() - self._handle_validation_result(validation_result) + self._handle_result(export_result) def _compose_validation_results_bucket_filename(self, filename: str) -> str: return f"{self.escrow_address}@{self.chain_id}/{filename}" - _LOW_QUALITY_REASON_MESSAGE_TEMPLATE = ( - "Annotation quality ({}) is below the required threshold ({})" - ) - - def _handle_validation_result(self, export_result: FinalResult): + def _handle_result(self, export_result: FinalResult): logger = self.logger escrow_address = self.escrow_address chain_id = self.chain_id @@ -166,8 +137,8 @@ def export_results( manifest = parse_manifest(escrow.get_escrow_manifest(chain_id, escrow_address)) - uploader = _TaskUploader( + exporter = _EscrowExporter( escrow_address=escrow_address, chain_id=chain_id, manifest=manifest, db_session=db_session ) - uploader.set_logger(logger) - uploader.upload() + exporter.set_logger(logger) + exporter.export() diff --git a/packages/examples/cvat/recording-oracle/src/handlers/completion/task_exporters/__init__.py b/packages/examples/cvat/recording-oracle/src/handlers/completion/task_exporters/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/examples/cvat/recording-oracle/src/handlers/completion/task_exporters/audio.py b/packages/examples/cvat/recording-oracle/src/handlers/completion/task_exporters/audio.py new file mode 100644 index 0000000000..b16c51e137 --- /dev/null +++ b/packages/examples/cvat/recording-oracle/src/handlers/completion/task_exporters/audio.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +import csv +import io +import zipfile + +from src.core.tasks.audio_transcription.meta import ( + TaskMetaLayout, + TaskResultsLayout, + format_time, + parse_gt_tsv, + parse_time, +) +from src.core.tasks.audio_transcription.spec import parse_audio_manifest +from src.handlers.completion.task_exporters.base import TaskExporter + +# Column marking each merged row's origin: annotator annotation vs ground truth. +_SOURCE_COLUMN = "source" +_ANNOTATION_SOURCE = "annotation" +_GT_SOURCE = "gt" + + +class AudioTaskExporter(TaskExporter): + def export(self) -> bytes: + tsv = self._build_merged_tsv() + + archive = io.BytesIO() + with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED) as zf: + zf.writestr(TaskResultsLayout.ANNOTATIONS_FILENAME, tsv) + + return archive.getvalue() + + def _build_merged_tsv(self) -> bytes: + spec = parse_audio_manifest(self.manifest) + attr = spec.details.transcription_attr_name + + annotations = self._download_annotation_result_file( + TaskResultsLayout.ANNOTATIONS_FILENAME + ).decode("utf-8") + reader = csv.DictReader(io.StringIO(annotations), delimiter="\t") + columns = list(reader.fieldnames or []) + if _SOURCE_COLUMN not in columns: + # place it right after "label" when possible, otherwise append + insert_at = columns.index("label") + 1 if "label" in columns else len(columns) + columns.insert(insert_at, _SOURCE_COLUMN) + + ds_rows = list(reader) + for row in ds_rows: + row[_SOURCE_COLUMN] = _ANNOTATION_SOURCE + + gt_regions = parse_gt_tsv(self._download_task_data_file(TaskMetaLayout.GT_FILENAME)) + gt_rows = [ + { + "filename": r.filename, + "start": format_time(r.start), + "stop": format_time(r.stop), + "label": r.label or "", + "input_region_ids": str(r.row_idx), + _SOURCE_COLUMN: _GT_SOURCE, + attr: r.text, + } + for r in gt_regions + ] + + rows = ds_rows + gt_rows + rows.sort(key=lambda r: (r["filename"], parse_time(r["start"]).total_seconds())) + + buffer = io.StringIO() + writer = csv.DictWriter( + buffer, fieldnames=columns, delimiter="\t", extrasaction="ignore" + ) + writer.writeheader() + for global_id, row in enumerate(rows): + writer.writerow({**{c: "" for c in columns}, **row, "id": global_id}) + return buffer.getvalue().encode("utf-8") diff --git a/packages/examples/cvat/recording-oracle/src/handlers/completion/task_exporters/base.py b/packages/examples/cvat/recording-oracle/src/handlers/completion/task_exporters/base.py new file mode 100644 index 0000000000..2643a54e97 --- /dev/null +++ b/packages/examples/cvat/recording-oracle/src/handlers/completion/task_exporters/base.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from abc import ABCMeta, abstractmethod +from typing import TYPE_CHECKING + +from src.core.config import Config +from src.core.storage import compose_data_bucket_filename, compose_results_bucket_filename +from src.handlers.validation.common import _TaskHandler +from src.services.cloud import make_client as make_cloud_client +from src.services.cloud.utils import BucketAccessInfo + +if TYPE_CHECKING: + from sqlalchemy.orm import Session + + from src.core.manifest import ManifestBase + + +class TaskExporter(_TaskHandler, metaclass=ABCMeta): + """ + Produces the task final annotations file. The Ground Truth annotations should be copied + into the output. + """ + + def __init__( + self, + escrow_address: str, + chain_id: int, + manifest: ManifestBase, + session: Session, + ) -> None: + super().__init__(escrow_address=escrow_address, chain_id=chain_id, manifest=manifest) + self.session = session + + self._eo_bucket = BucketAccessInfo.parse_obj(Config.exchange_oracle_storage_config) + + def _download_annotation_result_file(self, filename: str) -> bytes: + "Download a file from the exchange oracle's escrow results dir." + return make_cloud_client(self._eo_bucket).download_file( + compose_results_bucket_filename(self.escrow_address, self.chain_id, filename) + ) + + def _download_task_data_file(self, filename: str) -> bytes: + "Download a file from the exchange oracle's escrow data dir." + return make_cloud_client(self._eo_bucket).download_file( + compose_data_bucket_filename(self.escrow_address, self.chain_id, filename) + ) + + @abstractmethod + def export(self) -> bytes: ... diff --git a/packages/examples/cvat/recording-oracle/src/handlers/completion/task_exporters/factory.py b/packages/examples/cvat/recording-oracle/src/handlers/completion/task_exporters/factory.py new file mode 100644 index 0000000000..748b1ee3ce --- /dev/null +++ b/packages/examples/cvat/recording-oracle/src/handlers/completion/task_exporters/factory.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from src.core.manifest import get_manifest_task_type +from src.core.tasks import TaskTypes +from src.handlers.completion.task_exporters.audio import AudioTaskExporter +from src.handlers.completion.task_exporters.image import ImageTaskExporter + +if TYPE_CHECKING: + from sqlalchemy.orm import Session + + from src.core.manifest import ManifestBase + from src.handlers.completion.task_exporters.base import TaskExporter + + +def create_exporter( + manifest: ManifestBase, + escrow_address: str, + chain_id: int, + session: Session, +) -> TaskExporter: + task_type = get_manifest_task_type(manifest) + + match task_type: + case TaskTypes.audio_transcription: + exporter_type = AudioTaskExporter + case ( + TaskTypes.image_label_binary + | TaskTypes.image_boxes + | TaskTypes.image_polygons + | TaskTypes.image_points + | TaskTypes.image_boxes_from_points + | TaskTypes.image_skeletons_from_boxes + ): + exporter_type = ImageTaskExporter + case _: + raise NotImplementedError(f"Unsupported task type '{task_type}'") + + return exporter_type(escrow_address, chain_id, manifest, session) diff --git a/packages/examples/cvat/recording-oracle/src/handlers/validation/final_results.py b/packages/examples/cvat/recording-oracle/src/handlers/completion/task_exporters/image.py similarity index 67% rename from packages/examples/cvat/recording-oracle/src/handlers/validation/final_results.py rename to packages/examples/cvat/recording-oracle/src/handlers/completion/task_exporters/image.py index 0f6a303579..43e5a06463 100644 --- a/packages/examples/cvat/recording-oracle/src/handlers/validation/final_results.py +++ b/packages/examples/cvat/recording-oracle/src/handlers/completion/task_exporters/image.py @@ -7,28 +7,32 @@ from typing import TYPE_CHECKING import datumaro as dm -import numpy as np -import src.services.validation as db_service +from src.core.annotation_meta import RESULTING_ANNOTATIONS_FILE from src.core.manifest import get_manifest_task_type from src.core.tasks import TaskTypes from src.core.tasks.cvat_formats import DM_DATASET_FORMAT_MAPPING, DM_GT_DATASET_FORMAT_MAPPING -from src.core.validation_meta import JobMeta, ResultMeta, ValidationMeta -from src.core.validation_results import FinalResult -from src.db.utils import ForUpdateParams -from src.handlers.validation.common import UNKNOWN_QUALITY, _JobResults, _TaskHandler +from src.handlers.completion.task_exporters.base import TaskExporter +from src.handlers.validation.common import _TaskHandler from src.services.cloud import make_client as make_cloud_client from src.services.cloud.utils import BucketAccessInfo from src.utils.annotations import ProjectLabels from src.utils.zip_archive import extract_zip_archive, write_dir_to_zip_archive if TYPE_CHECKING: - import logging + from src.core.manifest import ManifestBase - from sqlalchemy.orm import Session - from src.core.annotation_meta import AnnotationMeta - from src.core.manifest import ManifestBase +class ImageTaskExporter(TaskExporter): + def export(self) -> bytes: + merged = self._download_annotation_result_file(RESULTING_ANNOTATIONS_FILE) + merger = _TaskAnnotationMerger( + escrow_address=self.escrow_address, + chain_id=self.chain_id, + manifest=self.manifest, + merged_annotations=io.BytesIO(merged), + ) + return merger.merge_results().read() class _TaskAnnotationMerger(_TaskHandler): @@ -184,99 +188,3 @@ def merge_results(self) -> io.IOBase: self._prepare_merged_dataset() return self._require_field(self._updated_merged_dataset_archive) - - -def process_final_results( - session: Session, - *, - escrow_address: str, - chain_id: int, - meta: AnnotationMeta, - merged_annotations: io.RawIOBase, - manifest: ManifestBase, - logger: logging.Logger, -) -> FinalResult: - assert logger # unused - - task = db_service.get_task_by_escrow_address( - session, - escrow_address, - for_update=ForUpdateParams( - nowait=True - ), # should not happen, but waiting should not block processing - ) - if not task: - raise AssertionError(f"Validation results for escrow {escrow_address} not found") - - merger = _TaskAnnotationMerger( - escrow_address=escrow_address, - chain_id=chain_id, - manifest=manifest, - merged_annotations=merged_annotations, - ) - - merged_annotations = merger.merge_results() - - job_final_result_ids: dict[str, str] = {} - - for job_meta in meta.jobs: - job = db_service.get_job_by_cvat_id(session, job_meta.job_id) - if not job: - raise AssertionError( - f"Can't find validation results for job " f"{job_meta.job_id} ({escrow_address=})" - ) - - assignment_validation_result = db_service.get_validation_result_by_assignment_id( - session, job_meta.assignment_id - ) - if not assignment_validation_result: - raise AssertionError( - f"Can't find validation results for assignments " - f"{job_meta.assignment_id} ({escrow_address=})" - ) - - job_final_result_ids[job.id] = assignment_validation_result.id - - task_jobs = task.jobs - - task_validation_results = db_service.get_task_validation_results(session, task.id) - - job_id_to_meta_id = {job.id: i for i, job in enumerate(task_jobs)} - - validation_result_id_to_meta_id = {r.id: i for i, r in enumerate(task_validation_results)} - - validation_meta = ValidationMeta( - jobs=[ - JobMeta( - job_id=job_id_to_meta_id[job.id], - final_result_id=validation_result_id_to_meta_id[job_final_result_ids[job.id]], - ) - for job in task_jobs - ], - results=[ - ResultMeta( - id=validation_result_id_to_meta_id[r.id], - job_id=job_id_to_meta_id[r.job.id], - annotator_wallet_address=r.annotator_wallet_address, - annotation_quality=r.annotation_quality, - ) - for r in task_validation_results - ], - ) - - # Include final results for all jobs - job_results: _JobResults = { - job.cvat_id: task_validation_results[ - validation_result_id_to_meta_id[job_final_result_ids[job.id]] - ].annotation_quality - for job in task_jobs - } - - return FinalResult( - job_results=job_results, - validation_meta=validation_meta, - resulting_annotations=merged_annotations.read(), - average_quality=np.mean( - [v for v in job_results.values() if v != UNKNOWN_QUALITY and v >= 0] or [0] - ), - ) diff --git a/packages/examples/cvat/recording-oracle/src/handlers/validation/__init__.py b/packages/examples/cvat/recording-oracle/src/handlers/validation/__init__.py index 03e1f4040e..54ba1eb45e 100644 --- a/packages/examples/cvat/recording-oracle/src/handlers/validation/__init__.py +++ b/packages/examples/cvat/recording-oracle/src/handlers/validation/__init__.py @@ -1,11 +1,9 @@ -from src.handlers.validation.final_results import process_final_results from src.handlers.validation.handlers import validate_results from src.handlers.validation.intermediate_results import process_intermediate_results from src.handlers.validation.meta import parse_annotation_metafile, serialize_validation_meta __all__ = [ "parse_annotation_metafile", - "process_final_results", "process_intermediate_results", "serialize_validation_meta", "validate_results", diff --git a/packages/examples/cvat/recording-oracle/tests/integration/services/test_validation_service.py b/packages/examples/cvat/recording-oracle/tests/integration/services/test_validation_service.py index 13453cc81b..6531a526f1 100644 --- a/packages/examples/cvat/recording-oracle/tests/integration/services/test_validation_service.py +++ b/packages/examples/cvat/recording-oracle/tests/integration/services/test_validation_service.py @@ -24,8 +24,8 @@ from src.core.validation_results import ValidationFailure, ValidationSuccess from src.cvat import api_calls as cvat_api from src.db import SessionLocal +from src.handlers.completion.final_results import process_final_results from src.handlers.validation import ( - process_final_results, process_intermediate_results, ) from src.services.validation import ( @@ -129,7 +129,7 @@ def test_can_handle_lowered_quality_requirements_in_manifest(self, session: Sess logger = mock.Mock(Logger) common_lock_es.enter_context( - mock.patch("src.handlers.validation.final_results.BucketAccessInfo.parse_obj") + mock.patch("src.handlers.completion.task_exporters.image.BucketAccessInfo.parse_obj") ) common_lock_es.enter_context( @@ -137,7 +137,7 @@ def test_can_handle_lowered_quality_requirements_in_manifest(self, session: Sess ) mock_make_cloud_client = common_lock_es.enter_context( - mock.patch("src.handlers.validation.final_results.make_cloud_client") + mock.patch("src.handlers.completion.task_exporters.image.make_cloud_client") ) mock_make_cloud_client.return_value.download_file = mock.Mock(return_value=b"") @@ -333,11 +333,11 @@ def test_can_change_bad_honeypots_in_jobs(self, session: Session, seed: int): logger = mock.Mock(Logger) common_lock_es.enter_context( - mock.patch("src.handlers.validation.final_results.BucketAccessInfo.parse_obj") + mock.patch("src.handlers.completion.task_exporters.image.BucketAccessInfo.parse_obj") ) mock_make_cloud_client = common_lock_es.enter_context( - mock.patch("src.handlers.validation.final_results.make_cloud_client") + mock.patch("src.handlers.completion.task_exporters.image.make_cloud_client") ) mock_make_cloud_client.return_value.download_file = mock.Mock(return_value=b"") @@ -568,11 +568,11 @@ def test_can_stop_on_slow_annotation_after_warmup_iterations(self, session: Sess logger = mock.Mock(Logger) common_lock_es.enter_context( - mock.patch("src.handlers.validation.final_results.BucketAccessInfo.parse_obj") + mock.patch("src.handlers.completion.task_exporters.image.BucketAccessInfo.parse_obj") ) mock_make_cloud_client = common_lock_es.enter_context( - mock.patch("src.handlers.validation.final_results.make_cloud_client") + mock.patch("src.handlers.completion.task_exporters.image.make_cloud_client") ) mock_make_cloud_client.return_value.download_file = mock.Mock(return_value=b"") @@ -685,11 +685,11 @@ def test_can_exclude_bad_gt_for_each_label_separately(self, session: Session): logger = mock.Mock(Logger) common_lock_es.enter_context( - mock.patch("src.handlers.validation.final_results.BucketAccessInfo.parse_obj") + mock.patch("src.handlers.completion.task_exporters.image.BucketAccessInfo.parse_obj") ) mock_make_cloud_client = common_lock_es.enter_context( - mock.patch("src.handlers.validation.final_results.make_cloud_client") + mock.patch("src.handlers.completion.task_exporters.image.make_cloud_client") ) mock_make_cloud_client.return_value.download_file = mock.Mock(return_value=b"") @@ -866,11 +866,11 @@ def test_can_complete_if_not_enough_gt_left_in_task_with_several_jobs( logger = logging.getLogger() common_lock_es.enter_context( - mock.patch("src.handlers.validation.final_results.BucketAccessInfo.parse_obj") + mock.patch("src.handlers.completion.task_exporters.image.BucketAccessInfo.parse_obj") ) mock_make_cloud_client = common_lock_es.enter_context( - mock.patch("src.handlers.validation.final_results.make_cloud_client") + mock.patch("src.handlers.completion.task_exporters.image.make_cloud_client") ) mock_make_cloud_client.return_value.download_file = mock.Mock(return_value=b"") @@ -1020,11 +1020,11 @@ def test_can_complete_if_not_enough_gt_left_in_task_with_one_job( logger = logging.getLogger() common_lock_es.enter_context( - mock.patch("src.handlers.validation.final_results.BucketAccessInfo.parse_obj") + mock.patch("src.handlers.completion.task_exporters.image.BucketAccessInfo.parse_obj") ) mock_make_cloud_client = common_lock_es.enter_context( - mock.patch("src.handlers.validation.final_results.make_cloud_client") + mock.patch("src.handlers.completion.task_exporters.image.make_cloud_client") ) mock_make_cloud_client.return_value.download_file = mock.Mock(return_value=b"") @@ -1174,15 +1174,18 @@ def patched_prepare_merged_dataset(self): self._updated_merged_dataset_archive = io.BytesIO(b"test") with ( - mock.patch("src.handlers.validation.final_results.BucketAccessInfo.parse_obj"), + # base downloads the exchange-oracle result file; the merger (image) downloads GT + mock.patch("src.handlers.completion.task_exporters.base.BucketAccessInfo.parse_obj"), + mock.patch("src.handlers.completion.task_exporters.base.make_cloud_client"), + mock.patch("src.handlers.completion.task_exporters.image.BucketAccessInfo.parse_obj"), mock.patch( - "src.handlers.validation.final_results.make_cloud_client" + "src.handlers.completion.task_exporters.image.make_cloud_client" ) as mock_make_cloud_client, - mock.patch("src.handlers.validation.final_results.dm.Dataset.import_from"), - mock.patch("src.handlers.validation.final_results.extract_zip_archive"), - mock.patch("src.handlers.validation.final_results.write_dir_to_zip_archive"), + mock.patch("src.handlers.completion.task_exporters.image.dm.Dataset.import_from"), + mock.patch("src.handlers.completion.task_exporters.image.extract_zip_archive"), + mock.patch("src.handlers.completion.task_exporters.image.write_dir_to_zip_archive"), mock.patch( - "src.handlers.validation.final_results._TaskAnnotationMerger._prepare_merged_dataset", + "src.handlers.completion.task_exporters.image._TaskAnnotationMerger._prepare_merged_dataset", patched_prepare_merged_dataset, ), ): @@ -1215,7 +1218,6 @@ def patched_prepare_merged_dataset(self): chain_id=chain_id, meta=annotation_meta, manifest=manifest, - merged_annotations=io.BytesIO(), logger=mock.Mock(Logger), ) From a9f352f73b1a56d15cc32ecf88bb7c35d9022b31 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Thu, 9 Jul 2026 13:50:05 +0300 Subject: [PATCH 30/53] Add gt min span requirement, fix assignment composition, migrate to timedelta in the impl --- .../exchange-oracle/src/core/manifest/v2.py | 7 + .../core/tasks/audio_transcription/meta.py | 56 ++-- .../core/tasks/audio_transcription/spec.py | 11 + .../builders/audio/transcription.py | 241 ++++++++++++------ .../handlers/job_export/exporters/audio.py | 14 +- .../cvat/exchange-oracle/src/utils/audio.py | 16 +- 6 files changed, 225 insertions(+), 120 deletions(-) diff --git a/packages/examples/cvat/exchange-oracle/src/core/manifest/v2.py b/packages/examples/cvat/exchange-oracle/src/core/manifest/v2.py index 76ac94fade..80e277e76d 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/manifest/v2.py +++ b/packages/examples/cvat/exchange-oracle/src/core/manifest/v2.py @@ -78,6 +78,13 @@ class AudioJobDetails(DetailsInfoBase): max_segment_duration: int | None = None "Maximum audio segment duration, seconds" + min_gt_span_duration: int | None = None + """ + Minimum duration of densely-annotated GT regions, in seconds. + Each is expected to contain 1 or more annotated intervals inside. + A span is identified by the span_id field. + """ + min_composition: MinComposition | None = None "Minimal composition of a job" diff --git a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py index 8c39abd5d2..e89f27ab98 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py +++ b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py @@ -76,9 +76,10 @@ class InputGtRegion: filename: str start: timedelta stop: timedelta - label: str | None = None - text: str = "" + label: str + text: str row_idx: int + span_id: str @property def id(self) -> str: @@ -86,7 +87,6 @@ def id(self) -> str: def parse_gt_tsv(data: bytes) -> list[InputGtRegion]: - "Parse a GT transcription TSV (columns: filename, start, stop, label, text) into regions." reader = csv.DictReader(io.StringIO(data.decode("utf-8")), delimiter="\t") return [ InputGtRegion( @@ -94,8 +94,9 @@ def parse_gt_tsv(data: bytes) -> list[InputGtRegion]: row_idx=row_idx, start=parse_time(row["start"]), stop=parse_time(row["stop"]), - label=(row.get("label") or "").strip() or None, - text=(row.get("text") or "").strip(), + label=row["label"].strip(), + text=row["text"].strip(), + span_id=row["span_id"].strip(), ) for row_idx, row in enumerate(reader) ] @@ -127,17 +128,17 @@ class PlacedRegion: index: int # 0-based position within the assignment clip source_filename: str - file_start: float - file_stop: float + source_start: float + source_stop: float clip_start: float clip_stop: float - kind: RegionKind = RegionKind.ds + kind: RegionKind input_ids: list[str] = Factory(list) # ids of the input ROIs this bundle was built from @frozen(kw_only=True) -class Assignment: - """One CVAT task: a single concatenated clip built from ``placed`` regions.""" +class Clip: + """A single concatenated audio clip built from ``placed`` regions (one per CVAT task).""" id: str clip_filename: str @@ -169,8 +170,8 @@ def _placed_to_dict(r: PlacedRegion) -> dict: return { "index": r.index, "source_filename": r.source_filename, - "file_start": r.file_start, - "file_stop": r.file_stop, + "source_start": r.source_start, + "source_stop": r.source_stop, "clip_start": r.clip_start, "clip_stop": r.clip_stop, "kind": r.kind.value, @@ -182,8 +183,8 @@ def _placed_from_dict(d: dict) -> PlacedRegion: return PlacedRegion( index=d["index"], source_filename=d["source_filename"], - file_start=d["file_start"], - file_stop=d["file_stop"], + source_start=d["source_start"], + source_stop=d["source_stop"], clip_start=d["clip_start"], clip_stop=d["clip_stop"], kind=RegionKind(d["kind"]), @@ -196,7 +197,8 @@ class TaskMetaLayout: GT_FILENAME = "gt.tsv" REGIONS_TSV_FILENAME = "regions.tsv" REGIONS_FILENAME = "regions.json" - ASSIGNMENTS_FILENAME = "assignments.json" + CLIPS_FILENAME = "assignments.json" + TASK_CLIPS_FILENAME = "task_clips.json" DS_CUTS_DIR = "ds_cuts" GT_CUTS_DIR = "gt_cuts" @@ -205,7 +207,7 @@ class TaskResultsLayout: "Layout of the escrow results dir on the oracle bucket." ASSIGNMENTS_DIR = "assignments" # per-assignment annotation TSVs (one per CVAT job) - ANNOTATIONS_FILENAME = "annotations.tsv" # merged final annotations (DS rows + GT) + ANNOTATIONS_FILENAME = "annotations.tsv" # merged annotations (without honeypots or GT) @classmethod def assignment_annotation_filename(cls, job_id: int, assignment_id: str) -> str: @@ -220,22 +222,22 @@ def serialize_regions(self, regions: list[PresentedRegion]) -> bytes: def parse_regions(self, data: bytes) -> list[PresentedRegion]: return [_region_from_dict(d) for d in parse_json(data)] - def serialize_assignments(self, assignments: list[Assignment]) -> bytes: + def serialize_clips(self, clips: list[Clip]) -> bytes: return dump_json( [ { - "id": a.id, - "clip_filename": a.clip_filename, - "clip_dur": a.clip_duration, - "placed": [_placed_to_dict(p) for p in a.placed], + "id": clip.id, + "clip_filename": clip.clip_filename, + "clip_dur": clip.clip_duration, + "placed": [_placed_to_dict(p) for p in clip.placed], } - for a in assignments + for clip in clips ] ) - def parse_assignments(self, data: bytes) -> list[Assignment]: + def parse_clips(self, data: bytes) -> list[Clip]: return [ - Assignment( + Clip( id=d["id"], clip_filename=d["clip_filename"], clip_duration=d["clip_dur"], @@ -243,3 +245,9 @@ def parse_assignments(self, data: bytes) -> list[Assignment]: ) for d in parse_json(data) ] + + def serialize_task_clips(self, task_clips: dict[int, str]) -> bytes: + return dump_json({str(task_id): clip_id for task_id, clip_id in task_clips.items()}) + + def parse_task_clips(self, data: bytes) -> dict[int, str]: + return {int(task_id): clip_id for task_id, clip_id in parse_json(data).items()} diff --git a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py index c5d33d4770..b35d9a3416 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py +++ b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py @@ -59,6 +59,13 @@ class TranscriptionDetails(BaseModel): min_composition: tuple[int, int] = (2, 2) "(gt, ds): minimum number of honeypot and DS regions per assignment" + min_gt_span_duration: timedelta = timedelta(seconds=30) + """ + Minimum duration of densely-annotated GT regions, in seconds. + Each is expected to contain 1 or more annotated intervals inside. + A span is identified by the span_id field. + """ + roi_min_duration: timedelta = timedelta(seconds=5) "Minimum presented region duration. Smaller segments are bundled together" @@ -125,6 +132,10 @@ def parse_audio_manifest(manifest: JobManifest) -> TranscriptionTaskSpecificatio details_fields["roi_max_duration"] = timedelta( seconds=manifest_details.max_segment_duration ) + if manifest_details.min_gt_span_duration is not None: + details_fields["min_gt_span_duration"] = timedelta( + seconds=manifest_details.min_gt_span_duration + ) if manifest_details.validation_overhead is not None: details_fields["validation_overhead"] = manifest_details.validation_overhead / 100.0 if manifest_details.min_composition is not None: diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py index 3e3a74e607..03cb103230 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py @@ -2,13 +2,15 @@ import csv import io -import random import uuid from collections import Counter +from datetime import timedelta from math import ceil from pathlib import Path, PurePosixPath from tempfile import TemporaryDirectory -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, TypeVar + +import numpy as np import src.cvat.api_calls as cvat_api import src.services.cvat as db_service @@ -16,7 +18,7 @@ from src.core.storage import compose_data_bucket_filename from src.core.tasks import TaskTypes from src.core.tasks.audio_transcription.meta import ( - Assignment, + Clip, InputGtRegion, InputRegion, PlacedRegion, @@ -47,41 +49,54 @@ from src.utils.logging import format_sequence if TYPE_CHECKING: + from collections.abc import Callable + from src.core.manifest import ManifestBase # --------------------------------------------------------------------------- # -# Assignment construction +# Clip construction # --------------------------------------------------------------------------- # +_RegionT = TypeVar("_RegionT", bound=InputRegion | InputGtRegion) + + def _bundle_regions( - regions: list[InputRegion], *, min_duration: float, max_duration: float -) -> list[tuple[float, float, list[InputRegion]]]: + regions: list[_RegionT], + *, + min_duration: timedelta, + max_duration: timedelta, + can_bundle: Callable[[_RegionT, _RegionT], bool] = lambda _a, _b: True +) -> list[tuple[timedelta, timedelta, list[_RegionT]]]: """ Bundle consecutive input regions (sorted by start) into groups >= min_duration; - a region already >= min_duration stays alone. Returns (start_s, stop_s, members) per bundle. + a region already >= min_duration stays alone. Returns (start, stop, members) per bundle. A bundle's span (first ROI start .. last ROI stop) includes the gaps between its members, so two limits keep bundles from ballooning over sparse input: - max_duration caps the presented span (a longer bundle would fail input validation anyway); - the join gap is capped at min_duration, so we never bridge a long silence between ROIs. + + A custom ``can_bundle`` callback can be passed to specify additional constraints on the ROIs + that can be joined. """ - def span(group: list[InputRegion]) -> float: - return group[-1].stop.total_seconds() - group[0].start.total_seconds() + def can_append_region(bundle: list[_RegionT], region: _RegionT) -> bool: + if not can_bundle(bundle[-1], region): + return False - def joinable(group: list[InputRegion], region: InputRegion) -> bool: - gap = region.start.total_seconds() - group[-1].stop.total_seconds() - reach = region.stop.total_seconds() - group[0].start.total_seconds() - return span(group) < min_duration and gap <= min_duration and reach <= max_duration + gap = region.start - bundle[-1].stop + reach = region.stop - bundle[0].start + span = _get_span_duration(bundle) + return span < min_duration and gap <= min_duration and reach <= max_duration - out: list[list[InputRegion]] = [] - bundle: list[InputRegion] | None = None + out: list[list[_RegionT]] = [] + bundle: list[_RegionT] | None = None for region in regions: if bundle is None: bundle = [region] - elif joinable(bundle, region): + elif can_append_region(bundle, region): bundle.append(region) else: out.append(bundle) @@ -90,7 +105,7 @@ def joinable(group: list[InputRegion], region: InputRegion) -> bool: if bundle is not None: # trailing bundle may stay shorter than min_duration out.append(bundle) - return [(g[0].start.total_seconds(), g[-1].stop.total_seconds(), g) for g in out] + return [(g[0].start, g[-1].stop, g) for g in out] def select_honeypots( @@ -98,11 +113,13 @@ def select_honeypots( *, source_filename: str, val_fraction: float, - min_duration: float, - max_duration: float, + min_duration: timedelta, + max_duration: timedelta, random_seed: int = 0, ) -> list[PresentedRegion]: """ + Pick GT regions for the honeypot pool. + Bundle GT cues into >= min_duration groups, then pick ~val_fraction of the bundles (by count) as honeypots. A bundle spans its first cue start to its last cue stop (including inter-cue gaps); we never pad into unannotated media, so a bundle may stay shorter than min_duration when @@ -110,9 +127,17 @@ def select_honeypots( """ regions = sorted(regions, key=lambda r: r.start) - bundles = _bundle_regions(regions, min_duration=min_duration, max_duration=max_duration) + bundles = _bundle_regions( + regions, + min_duration=min_duration, + max_duration=max_duration, + can_bundle=( + # we can only bundle regions if they belong to the same span + lambda a, b: a.span_id == b.span_id + ) + ) - rng = random.Random(random_seed) # noqa: S311 # not cryptographic + rng = np.random.Generator(np.random.MT19937(random_seed)) # pin the rng used for stable results idxs = list(range(len(bundles))) rng.shuffle(idxs) n_select = ceil(val_fraction * len(bundles)) @@ -124,8 +149,8 @@ def select_honeypots( honeypots.append( PresentedRegion( source_filename=source_filename, - start=s, - stop=e, + start=s.total_seconds(), + stop=e.total_seconds(), kind=RegionKind.gt, input_ids=[m.id for m in members], ) @@ -137,61 +162,70 @@ def plan_assignments( ds_regions: list[PresentedRegion], honeypots: list[PresentedRegion], *, - composition: tuple[int, int], + min_composition: tuple[int, int], validation_overhead: float, - max_audio_duration: float, + max_audio_duration: timedelta, random_seed: int = 0, ) -> list[list[PresentedRegion]]: """ Mix DS and honeypots into assignments. Returns ordered region lists. """ - # 1. shuffle DS - rng = random.Random(random_seed) # noqa: S311 # not cryptographic - ds_order = list(range(len(ds_regions))) - rng.shuffle(ds_order) + # 1. shuffle inputs + rng = np.random.Generator(np.random.MT19937(random_seed)) # pin the rng used for stable results + + def sort_key(r: PresentedRegion) -> tuple: + return (r.source_filename, r.start) + + ds_regions = sorted(ds_regions, key=sort_key) + rng.shuffle(ds_regions) + + honeypots = sorted(honeypots, key=sort_key) + rng.shuffle(honeypots) # 2. Reservoir-pack each assignment up to the DS budget (>= K_min), inject least-used honeypots # up to the validation overhead (>= H_min), then shuffle region order. Deterministic. - ds_budget = max_audio_duration * (1.0 - validation_overhead) - hp_target = validation_overhead * max_audio_duration - h_min, k_min = composition + ds_budget = max_audio_duration.total_seconds() * (1.0 - validation_overhead) + hp_target = validation_overhead * max_audio_duration.total_seconds() + h_min, k_min = min_composition hp_use: Counter = Counter() assignments: list[list[PresentedRegion]] = [] - di = 0 - while di < len(ds_order): - chosen: list[PresentedRegion] = [] - ds_dur = 0.0 - while di < len(ds_order): - r = ds_regions[ds_order[di]] - d = r.duration - if len(chosen) >= k_min and ds_dur + d > ds_budget: + + ds_idx = 0 + while ds_idx < len(ds_regions): + pick: list[PresentedRegion] = [] + pick_duration = 0.0 + while ds_idx < len(ds_regions): + ds_region = ds_regions[ds_idx] + if len(pick) >= k_min and pick_duration + ds_region.duration > ds_budget: break - chosen.append(r) - ds_dur += d - di += 1 - if len(chosen) < k_min: - break # DS stream exhausted - can't form a full assignment - hp_dur = 0.0 + pick.append(ds_region) + pick_duration += ds_region.duration + ds_idx += 1 + + if not pick: + break # DS stream exhausted - nothing left to place + # least-used honeypots first, randomized within each use-count tier + hp_duration = 0.0 hp_ranked = sorted(range(len(honeypots)), key=lambda i: (hp_use[i], rng.random())) for n_hp, i in enumerate(hp_ranked): - if (hp_dur >= hp_target and n_hp >= h_min) or n_hp >= len(honeypots): + if (hp_duration >= hp_target and n_hp >= h_min) or n_hp >= len(honeypots): break - chosen.append(honeypots[i]) - hp_dur += honeypots[i].duration + pick.append(honeypots[i]) + hp_duration += honeypots[i].duration hp_use[i] += 1 - rng.shuffle(chosen) - assignments.append(chosen) + rng.shuffle(pick) + assignments.append(pick) return assignments def place_regions( - regions: list[PresentedRegion], *, pause_s: float + regions: list[PresentedRegion], *, pause: timedelta ) -> tuple[list[PlacedRegion], float]: """Clip-time placement of concatenated regions (region durations + inter-region pauses).""" placed: list[PlacedRegion] = [] @@ -203,8 +237,8 @@ def place_regions( PlacedRegion( index=i, source_filename=region.source_filename, - file_start=region.start, - file_stop=region.stop, + source_start=region.start, + source_stop=region.stop, clip_start=t, clip_stop=t + dur, kind=region.kind, @@ -213,7 +247,7 @@ def place_regions( ) t += dur if i < n - 1: - t += pause_s + t += pause.total_seconds() return placed, t @@ -240,7 +274,7 @@ def __init__(self, manifest: ManifestBase, escrow_address: str, chain_id: int) - self._gt_tsv_data: MaybeUnset[bytes] = unset # raw input GT TSV self._ds_regions: MaybeUnset[list[PresentedRegion]] = unset self._honeypots: MaybeUnset[list[PresentedRegion]] = unset - self._assignments: MaybeUnset[list[Assignment]] = unset + self._assignments: MaybeUnset[list[Clip]] = unset self._excluded_gt_info: MaybeUnset[ExcludedAnnotationsInfo] = unset self._meta_serializer = TaskMetaSerializer() @@ -298,10 +332,11 @@ def _build_gt_regions(self) -> None: file_duration = probe_duration(self._media_paths[filename]) valid_regions = _validate_gt_regions( self._gt_by_file[filename], - filename, - file_duration, - self.spec.details.roi_max_duration.total_seconds(), - excluded_gt, + filename=filename, + file_duration=file_duration, + max_duration=self.spec.details.roi_max_duration, + min_span_duration=self.spec.details.min_gt_span_duration, + excluded=excluded_gt, ) if not valid_regions: continue @@ -310,9 +345,9 @@ def _build_gt_regions(self) -> None: select_honeypots( valid_regions, source_filename=filename, - val_fraction=1.0, # use all GT as honeypots (rotation is a future feature) - min_duration=self.spec.details.roi_min_duration.total_seconds(), - max_duration=self.spec.details.roi_max_duration.total_seconds(), + val_fraction=1.0, # use all spans, honeypot rotation is a future feature + min_duration=self.spec.details.roi_min_duration, + max_duration=self.spec.details.roi_max_duration, random_seed=self.spec.details.random_seed, ) ) @@ -370,15 +405,15 @@ def _build_ds_regions(self) -> None: ) bundled = _bundle_regions( rows, - min_duration=self.spec.details.roi_min_duration.total_seconds(), - max_duration=self.spec.details.roi_max_duration.total_seconds(), + min_duration=self.spec.details.roi_min_duration, + max_duration=self.spec.details.roi_max_duration, ) for start, stop, members in bundled: ds_regions.append( PresentedRegion( source_filename=filename, - start=start, - stop=stop, + start=start.total_seconds(), + stop=stop.total_seconds(), kind=RegionKind.ds, input_ids=[m.id for m in members], ) @@ -403,9 +438,9 @@ def _prepare_assignments(self) -> None: region_lists = plan_assignments( self._ds_regions, self._honeypots, - composition=self.spec.details.min_composition, + min_composition=self.spec.details.min_composition, validation_overhead=self.spec.details.validation_overhead, - max_audio_duration=self.spec.details.standard_assignment_duration.total_seconds(), + max_audio_duration=self.spec.details.standard_assignment_duration, random_seed=self.spec.details.random_seed, ) if not region_lists: @@ -415,23 +450,27 @@ def _prepare_assignments(self) -> None: pause = self.spec.details.roi_join_pause pause_ms = round(pause.total_seconds() * 1000) - assignments: list[Assignment] = [] + assignments: list[Clip] = [] for regions in region_lists: assignment_id = uuid.uuid4().hex clip_filename = f"{assignment_id}.wav" clip_path = self._media_dir / clip_filename cuts = [ - RegionCut(media=self._media_paths[r.source_filename], start=r.start, stop=r.stop) + RegionCut( + media=self._media_paths[r.source_filename], + start=timedelta(seconds=r.start), + stop=timedelta(seconds=r.stop), + ) for r in regions ] cut_and_concat( cuts, clip_path, pause_ms=pause_ms, sample_rate=self.spec.details.sample_rate ) - placed, clip_duration = place_regions(regions, pause_s=pause.total_seconds()) + placed, clip_duration = place_regions(regions, pause=pause) assignments.append( - Assignment( + Clip( id=assignment_id, clip_filename=clip_filename, clip_duration=clip_duration, @@ -487,9 +526,9 @@ def _upload_clips_and_meta(self) -> None: ) storage_client.create_file( compose_data_bucket_filename( - self.escrow_address, self.chain_id, self._meta_layout.ASSIGNMENTS_FILENAME + self.escrow_address, self.chain_id, self._meta_layout.CLIPS_FILENAME ), - self._meta_serializer.serialize_assignments(self._assignments), + self._meta_serializer.serialize_clips(self._assignments), ) if Config.debug: @@ -514,8 +553,8 @@ def _upload_roi_cuts(self, storage_client) -> None: [ RegionCut( media=self._media_paths[region.source_filename], - start=region.start, - stop=region.stop, + start=timedelta(seconds=region.start), + stop=timedelta(seconds=region.stop), ) ], cut_path, @@ -576,6 +615,7 @@ def _create_on_cvat(self) -> None: ) db_service.get_project_by_id(session, project_id, for_update=True) # lock the row + task_clips: dict[int, str] = {} # CVAT task id -> clip id for assignment in self._assignments: clip_key = compose_data_bucket_filename( escrow_address, @@ -591,8 +631,20 @@ def _create_on_cvat(self) -> None: cvat_api.put_task_data(cvat_task.id, cloud_storage.id, filenames=[clip_key]) db_service.create_data_upload(session, cvat_task.id) + task_clips[cvat_task.id] = assignment.id + db_service.touch(session, Project, [project_id]) + # Persist the CVAT task -> clip mapping so the recording oracle can join each annotated job + # (keyed by its CVAT task id) back to its clip metadata for scoring. + storage_client = self._make_cloud_storage_client(self._oracle_data_bucket) + storage_client.create_file( + compose_data_bucket_filename( + escrow_address, chain_id, self._meta_layout.TASK_CLIPS_FILENAME + ), + self._meta_serializer.serialize_task_clips(task_clips), + ) + # -- orchestration ----------------------------------------------------- # def build(self) -> None: @@ -644,9 +696,11 @@ def _parse_gt_tsv(data: bytes) -> dict[str, list[InputGtRegion]]: def _validate_gt_regions( regions: list[InputGtRegion], + *, filename: str, - file_duration: float, - max_duration: float, + file_duration: timedelta, + max_duration: timedelta, + min_span_duration: timedelta, excluded: ExcludedAnnotationsInfo, ) -> list[InputGtRegion]: valid: list[InputGtRegion] = [] @@ -656,7 +710,7 @@ def _validate_gt_regions( excluded.total_count += 1 - if not (0 <= start < stop <= file_duration + 1e-6): + if not (0 <= start < stop <= file_duration.total_seconds() + 1e-6): excluded.excluded_count += 1 excluded.add_message( f"GT region '{region.row_idx}' ({region.label}) [{start:.3f}, {stop:.3f}] " @@ -666,11 +720,11 @@ def _validate_gt_regions( ) continue - if stop - start > max_duration: + if stop - start > max_duration.total_seconds(): excluded.excluded_count += 1 excluded.add_message( f"GT region '{region.row_idx}' ({region.label}) [{start:.3f}, {stop:.3f}] " - f"- longer than the maximum region duration ({max_duration:.3f}s)", + f"- longer than the maximum region duration ({max_duration.total_seconds():.3f}s)", sample_id=filename, sample_subset="", ) @@ -678,6 +732,26 @@ def _validate_gt_regions( valid.append(region) + by_span: dict[str, list[InputGtRegion]] = {} + for region in valid: + by_span.setdefault(region.span_id, []).append(region) + + valid.clear() + for span_id, span_members in by_span.items(): + span_duration = _get_span_duration(span_members) + + if span_duration > min_span_duration: + excluded.excluded_count += len(span_members) + excluded.add_message( + f"GT span '{span_id}' ({span_duration:.3f}s) - shorter " + f"than the minimum span duration ({min_span_duration.total_seconds():.3f}s)", + sample_id=filename, + sample_subset="", + ) + continue + + valid.extend(span_members) + return valid @@ -724,3 +798,6 @@ def _shared_attr_spec(attr) -> dict: } ) return label_config + +def _get_span_duration(group: list[InputRegion | InputGtRegion]) -> timedelta: + return group[-1].stop - group[0].start diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/audio.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/audio.py index 5d45da510d..3fa578f445 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/audio.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/audio.py @@ -28,7 +28,7 @@ from src.services.cloud.utils import BucketAccessInfo if TYPE_CHECKING: - from src.core.tasks.audio_transcription.meta import Assignment, PlacedRegion + from src.core.tasks.audio_transcription.meta import Clip, PlacedRegion _LEADING_COLUMNS = ["id", "input_region_ids", "filename", "start", "stop", "label"] @@ -79,12 +79,12 @@ def export(self) -> None: self._emit_escrow_recorded() self.logger.info(f"The escrow ({self.escrow_address=}) is completed, annotations merged") - def _load_assignments(self) -> dict[str, Assignment]: + def _load_assignments(self) -> dict[str, Clip]: storage_client = make_cloud_client(BucketAccessInfo.parse_obj(Config.storage_config)) - assignments = TaskMetaSerializer().parse_assignments( + assignments = TaskMetaSerializer().parse_clips( storage_client.download_file( compose_data_bucket_filename( - self.escrow_address, self.chain_id, TaskMetaLayout().ASSIGNMENTS_FILENAME + self.escrow_address, self.chain_id, TaskMetaLayout().CLIPS_FILENAME ) ) ) @@ -93,7 +93,7 @@ def _load_assignments(self) -> dict[str, Assignment]: def _annotation_rows( self, annotations: FileDescriptor, - assignments_by_clip: dict[str, Assignment], + assignments_by_clip: dict[str, Clip], attr_names: list[str], ) -> list[dict]: annotations.file.seek(0) @@ -115,7 +115,7 @@ def _annotation_rows( # outside any region, or a honeypot -> dropped continue - offset = placed.file_start - placed.clip_start + offset = placed.source_start - placed.clip_start rows.append( { "filename": placed.source_filename, @@ -130,7 +130,7 @@ def _annotation_rows( return rows @staticmethod - def _placed_at(assignment: Assignment, clip_time_s: float) -> PlacedRegion | None: + def _placed_at(assignment: Clip, clip_time_s: float) -> PlacedRegion | None: return next( ( p diff --git a/packages/examples/cvat/exchange-oracle/src/utils/audio.py b/packages/examples/cvat/exchange-oracle/src/utils/audio.py index e627269f26..68ba456246 100644 --- a/packages/examples/cvat/exchange-oracle/src/utils/audio.py +++ b/packages/examples/cvat/exchange-oracle/src/utils/audio.py @@ -8,6 +8,7 @@ import subprocess from dataclasses import dataclass +from datetime import timedelta from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -20,18 +21,18 @@ class FfmpegError(RuntimeError): @dataclass(frozen=True) class RegionCut: - """A slice [start, stop) (seconds) to take from a source media file.""" + """A slice [start, stop) to take from a source media file.""" media: Path - start: float - stop: float + start: timedelta + stop: timedelta @property - def duration(self) -> float: + def duration(self) -> timedelta: return self.stop - self.start -def probe_duration(path: Path) -> float: +def probe_duration(path: Path) -> timedelta: """Return media duration in seconds""" cmd = [ @@ -48,7 +49,7 @@ def probe_duration(path: Path) -> float: if result.returncode != 0: raise FfmpegError(f"ffprobe failed for {path}:\n{result.stderr[-800:]}") - return float(result.stdout.strip()) + return timedelta(seconds=float(result.stdout.strip())) def cut_and_concat( @@ -86,7 +87,8 @@ def cut_and_concat( for i, region in enumerate(regions): inp = idx_of[str(region.media)] filter_parts.append( - f"[{inp}:a]atrim={region.start:.3f}:{region.stop:.3f},asetpts=PTS-STARTPTS," + f"[{inp}:a]atrim={region.start.total_seconds():.3f}:{region.stop.total_seconds():.3f}," + "asetpts=PTS-STARTPTS," f"aresample={sample_rate},aformat=channel_layouts=mono[s{i}]" ) segment_labels.append(f"[s{i}]") From 5269b6f74583b4aff3ecb1d520cd7588c76680d5 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Thu, 9 Jul 2026 13:51:16 +0300 Subject: [PATCH 31/53] Use assignment timeouts from manifest --- .../exchange-oracle/src/services/exchange.py | 22 +++---------------- .../exchange-oracle/src/utils/assignments.py | 21 +++++++++++++++++- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/packages/examples/cvat/exchange-oracle/src/services/exchange.py b/packages/examples/cvat/exchange-oracle/src/services/exchange.py index 70d57a1320..3ece001f65 100644 --- a/packages/examples/cvat/exchange-oracle/src/services/exchange.py +++ b/packages/examples/cvat/exchange-oracle/src/services/exchange.py @@ -5,12 +5,11 @@ import src.services.cvat as cvat_service from src.chain.escrow import get_escrow_manifest from src.core.manifest import parse_manifest -from src.core.tasks import TaskTypes -from src.core.types import JobStatuses, Networks, ProjectStatuses +from src.core.types import JobStatuses, Networks from src.db import SessionLocal from src.db.utils import ForUpdateParams from src.models.cvat import Job -from src.utils.assignments import get_default_assignment_timeout +from src.utils.assignments import get_assignment_timeout from src.utils.requests import get_or_404 from src.utils.time import utcnow @@ -53,16 +52,6 @@ def create_assignment( "The user already has an unfinished assignment in this project" ) - # TODO: Try to put into 1 request. SQLAlchemy generates 2 queries with simple - # .options(selectinload(Job.project)) - project = get_or_404( - cvat_service.get_project_by_escrow_address( - session, escrow_address, status_in=[ProjectStatuses.annotation] - ), - escrow_address, - object_type_name="job", - ) - unassigned_job = cvat_service.get_free_job( session, escrow_address=escrow_address, @@ -82,12 +71,7 @@ def create_assignment( wallet_address=user.wallet_address, cvat_job_id=unassigned_job.cvat_id, expires_at=utcnow() - + timedelta( - seconds=get_default_assignment_timeout( - TaskTypes(project.job_type) - # TODO: need to update this if we have multiple job types per escrow - ) - ), + + timedelta(seconds=get_assignment_timeout(manifest)), ) cvat_service.update_job_status(session, unassigned_job.id, status=JobStatuses.in_progress) diff --git a/packages/examples/cvat/exchange-oracle/src/utils/assignments.py b/packages/examples/cvat/exchange-oracle/src/utils/assignments.py index 9aa1d8fac5..23292bdf78 100644 --- a/packages/examples/cvat/exchange-oracle/src/utils/assignments.py +++ b/packages/examples/cvat/exchange-oracle/src/utils/assignments.py @@ -1,8 +1,15 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING from urllib.parse import urljoin from src.core.config import Config +from src.core.manifest import get_manifest_task_type from src.core.tasks import TaskTypes, skeletons_from_boxes -from src.models.cvat import Project + +if TYPE_CHECKING: + from src.core.manifest import ManifestBase + from src.models.cvat import Project def compose_assignment_url(task_id: int, job_id: int, *, project: Project) -> str: @@ -28,3 +35,15 @@ def get_default_assignment_timeout(task_type: TaskTypes) -> int: timeout_seconds *= skeletons_from_boxes.DEFAULT_ASSIGNMENT_SIZE_MULTIPLIER return timeout_seconds + + +def get_assignment_timeout(manifest: ManifestBase) -> int: + """ + Get assignment expiration timeout, in seconds. + """ + details = getattr(getattr(manifest, "annotation", None), "details", None) + max_time = getattr(details, "max_time", None) + if max_time is not None: + return max_time + + return get_default_assignment_timeout(get_manifest_task_type(manifest)) From e2082f0dba89717168644619e143c588a1bae57f Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Thu, 9 Jul 2026 13:57:02 +0300 Subject: [PATCH 32/53] build(exchange-oracle): declare numpy as a direct dependency The audio transcription task builder imports numpy (seeded RNG for honeypot/assignment mixing) but numpy was only pulled in transitively. Declare it explicitly so the import can't break on a dependency reshuffle. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/examples/cvat/exchange-oracle/pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/examples/cvat/exchange-oracle/pyproject.toml b/packages/examples/cvat/exchange-oracle/pyproject.toml index 718d9713ea..f1b82ac5f4 100644 --- a/packages/examples/cvat/exchange-oracle/pyproject.toml +++ b/packages/examples/cvat/exchange-oracle/pyproject.toml @@ -19,6 +19,7 @@ sqlalchemy = "^2.0.16" apscheduler = "^3.10.1" xmltodict = "^0.13.0" datumaro = {git = "https://github.com/cvat-ai/datumaro.git", rev = "ff83c00c2c1bc4b8fdfcc55067fcab0a9b5b6b11"} +numpy = "^1.25.2" boto3 = "^1.28.33" google-cloud-storage = "^2.14.0" pyinstrument = "^4.6.2" From ab082883ae0d3febfdf681b4582c84f492bad1c2 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Thu, 9 Jul 2026 14:01:18 +0300 Subject: [PATCH 33/53] fix condition --- .../src/handlers/job_creation/builders/audio/transcription.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py index 03cb103230..2af8610d9b 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py @@ -740,7 +740,7 @@ def _validate_gt_regions( for span_id, span_members in by_span.items(): span_duration = _get_span_duration(span_members) - if span_duration > min_span_duration: + if span_duration < min_span_duration: excluded.excluded_count += len(span_members) excluded.add_message( f"GT span '{span_id}' ({span_duration:.3f}s) - shorter " From 6e6787563da781546df07293367a3b1fbc3d1048 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Thu, 9 Jul 2026 16:09:44 +0300 Subject: [PATCH 34/53] feat(cvat-audio): span-based GT honeypots + configurable boundary tolerance Validation-side improvements for audio transcription, shared across the exchange and recording oracles: - GT is organized into densely-annotated spans via a `span_id` column; honeypot cues are joined only within a span, never across gaps. - Add `min_gt_span_duration` detail: spans shorter than it are excluded. - Add `boundary_tolerance` detail (default 200ms), forwarded to the audio validator so annotator/GT interval boundary drift within the join pause no longer drops a honeypot. - Recording-oracle audio checker: resolve each job to its clip via the EO-recorded task->clip mapping; extend the honeypot inclusion window by half the adjacent pause on each side. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../exchange-oracle/src/core/manifest/v2.py | 3 + .../core/tasks/audio_transcription/spec.py | 7 + .../cvat/recording-oracle/pyproject.toml | 2 + .../recording-oracle/src/core/manifest/v2.py | 10 ++ .../core/tasks/audio_transcription/meta.py | 54 ++++--- .../core/tasks/audio_transcription/spec.py | 18 +++ .../src/handlers/completion/handlers.py | 2 +- .../src/handlers/validation/handlers.py | 2 +- .../validation/quality_checkers/audio.py | 135 +++++++++++------- 9 files changed, 158 insertions(+), 75 deletions(-) diff --git a/packages/examples/cvat/exchange-oracle/src/core/manifest/v2.py b/packages/examples/cvat/exchange-oracle/src/core/manifest/v2.py index 80e277e76d..010cdd4c89 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/manifest/v2.py +++ b/packages/examples/cvat/exchange-oracle/src/core/manifest/v2.py @@ -91,6 +91,9 @@ class AudioJobDetails(DetailsInfoBase): validation_overhead: int | None = None "Extra validation samples added on top of the required amount" + boundary_tolerance: int | None = None + "Allowed annotation/GT interval boundary misalignment at honeypots, milliseconds" + DetailsInfo = ImageJobDetails | AudioJobDetails diff --git a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py index b35d9a3416..1b5f12894d 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py +++ b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py @@ -75,6 +75,9 @@ class TranscriptionDetails(BaseModel): roi_join_pause: timedelta = timedelta(milliseconds=700) "Silence inserted between concatenated regions in an assignment clip" + boundary_tolerance: timedelta = timedelta(milliseconds=200) + "Allowed joined annotation/GT interval boundary misalignment when matching transcriptions" + standard_assignment_duration: timedelta = timedelta(seconds=60) "Target audio duration per assignment" @@ -136,6 +139,10 @@ def parse_audio_manifest(manifest: JobManifest) -> TranscriptionTaskSpecificatio details_fields["min_gt_span_duration"] = timedelta( seconds=manifest_details.min_gt_span_duration ) + if manifest_details.boundary_tolerance is not None: + details_fields["boundary_tolerance"] = timedelta( + milliseconds=manifest_details.boundary_tolerance + ) if manifest_details.validation_overhead is not None: details_fields["validation_overhead"] = manifest_details.validation_overhead / 100.0 if manifest_details.min_composition is not None: diff --git a/packages/examples/cvat/recording-oracle/pyproject.toml b/packages/examples/cvat/recording-oracle/pyproject.toml index 27659af0ba..c07840b9f1 100644 --- a/packages/examples/cvat/recording-oracle/pyproject.toml +++ b/packages/examples/cvat/recording-oracle/pyproject.toml @@ -121,6 +121,8 @@ ignore = [ "ANN001", # | "ANN003", # | "ARG001", # | + "ARG002", # Allow unused args in test doubles mirroring real signatures + "RET504", # Allow assign-before-return for readability in tests "FBT001", # Allow bool-annotated positional args in functions "SLF001", # Allow private attrs access "PLR2004", # Allow magic values diff --git a/packages/examples/cvat/recording-oracle/src/core/manifest/v2.py b/packages/examples/cvat/recording-oracle/src/core/manifest/v2.py index 76ac94fade..010cdd4c89 100644 --- a/packages/examples/cvat/recording-oracle/src/core/manifest/v2.py +++ b/packages/examples/cvat/recording-oracle/src/core/manifest/v2.py @@ -78,12 +78,22 @@ class AudioJobDetails(DetailsInfoBase): max_segment_duration: int | None = None "Maximum audio segment duration, seconds" + min_gt_span_duration: int | None = None + """ + Minimum duration of densely-annotated GT regions, in seconds. + Each is expected to contain 1 or more annotated intervals inside. + A span is identified by the span_id field. + """ + min_composition: MinComposition | None = None "Minimal composition of a job" validation_overhead: int | None = None "Extra validation samples added on top of the required amount" + boundary_tolerance: int | None = None + "Allowed annotation/GT interval boundary misalignment at honeypots, milliseconds" + DetailsInfo = ImageJobDetails | AudioJobDetails diff --git a/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/meta.py b/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/meta.py index e612a2c0a0..e89f27ab98 100644 --- a/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/meta.py +++ b/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/meta.py @@ -76,9 +76,10 @@ class InputGtRegion: filename: str start: timedelta stop: timedelta - label: str | None = None - text: str = "" + label: str + text: str row_idx: int + span_id: str @property def id(self) -> str: @@ -86,7 +87,6 @@ def id(self) -> str: def parse_gt_tsv(data: bytes) -> list[InputGtRegion]: - "Parse a GT transcription TSV (columns: filename, start, stop, label, text) into regions." reader = csv.DictReader(io.StringIO(data.decode("utf-8")), delimiter="\t") return [ InputGtRegion( @@ -94,8 +94,9 @@ def parse_gt_tsv(data: bytes) -> list[InputGtRegion]: row_idx=row_idx, start=parse_time(row["start"]), stop=parse_time(row["stop"]), - label=(row.get("label") or "").strip() or None, - text=(row.get("text") or "").strip(), + label=row["label"].strip(), + text=row["text"].strip(), + span_id=row["span_id"].strip(), ) for row_idx, row in enumerate(reader) ] @@ -127,17 +128,17 @@ class PlacedRegion: index: int # 0-based position within the assignment clip source_filename: str - file_start: float - file_stop: float + source_start: float + source_stop: float clip_start: float clip_stop: float - kind: RegionKind = RegionKind.ds + kind: RegionKind input_ids: list[str] = Factory(list) # ids of the input ROIs this bundle was built from @frozen(kw_only=True) -class Assignment: - """One CVAT task: a single concatenated clip built from ``placed`` regions.""" +class Clip: + """A single concatenated audio clip built from ``placed`` regions (one per CVAT task).""" id: str clip_filename: str @@ -169,8 +170,8 @@ def _placed_to_dict(r: PlacedRegion) -> dict: return { "index": r.index, "source_filename": r.source_filename, - "file_start": r.file_start, - "file_stop": r.file_stop, + "source_start": r.source_start, + "source_stop": r.source_stop, "clip_start": r.clip_start, "clip_stop": r.clip_stop, "kind": r.kind.value, @@ -182,8 +183,8 @@ def _placed_from_dict(d: dict) -> PlacedRegion: return PlacedRegion( index=d["index"], source_filename=d["source_filename"], - file_start=d["file_start"], - file_stop=d["file_stop"], + source_start=d["source_start"], + source_stop=d["source_stop"], clip_start=d["clip_start"], clip_stop=d["clip_stop"], kind=RegionKind(d["kind"]), @@ -196,7 +197,8 @@ class TaskMetaLayout: GT_FILENAME = "gt.tsv" REGIONS_TSV_FILENAME = "regions.tsv" REGIONS_FILENAME = "regions.json" - ASSIGNMENTS_FILENAME = "assignments.json" + CLIPS_FILENAME = "assignments.json" + TASK_CLIPS_FILENAME = "task_clips.json" DS_CUTS_DIR = "ds_cuts" GT_CUTS_DIR = "gt_cuts" @@ -220,22 +222,22 @@ def serialize_regions(self, regions: list[PresentedRegion]) -> bytes: def parse_regions(self, data: bytes) -> list[PresentedRegion]: return [_region_from_dict(d) for d in parse_json(data)] - def serialize_assignments(self, assignments: list[Assignment]) -> bytes: + def serialize_clips(self, clips: list[Clip]) -> bytes: return dump_json( [ { - "id": a.id, - "clip_filename": a.clip_filename, - "clip_dur": a.clip_duration, - "placed": [_placed_to_dict(p) for p in a.placed], + "id": clip.id, + "clip_filename": clip.clip_filename, + "clip_dur": clip.clip_duration, + "placed": [_placed_to_dict(p) for p in clip.placed], } - for a in assignments + for clip in clips ] ) - def parse_assignments(self, data: bytes) -> list[Assignment]: + def parse_clips(self, data: bytes) -> list[Clip]: return [ - Assignment( + Clip( id=d["id"], clip_filename=d["clip_filename"], clip_duration=d["clip_dur"], @@ -243,3 +245,9 @@ def parse_assignments(self, data: bytes) -> list[Assignment]: ) for d in parse_json(data) ] + + def serialize_task_clips(self, task_clips: dict[int, str]) -> bytes: + return dump_json({str(task_id): clip_id for task_id, clip_id in task_clips.items()}) + + def parse_task_clips(self, data: bytes) -> dict[int, str]: + return {int(task_id): clip_id for task_id, clip_id in parse_json(data).items()} diff --git a/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/spec.py b/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/spec.py index c5d33d4770..1b5f12894d 100644 --- a/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/spec.py +++ b/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/spec.py @@ -59,6 +59,13 @@ class TranscriptionDetails(BaseModel): min_composition: tuple[int, int] = (2, 2) "(gt, ds): minimum number of honeypot and DS regions per assignment" + min_gt_span_duration: timedelta = timedelta(seconds=30) + """ + Minimum duration of densely-annotated GT regions, in seconds. + Each is expected to contain 1 or more annotated intervals inside. + A span is identified by the span_id field. + """ + roi_min_duration: timedelta = timedelta(seconds=5) "Minimum presented region duration. Smaller segments are bundled together" @@ -68,6 +75,9 @@ class TranscriptionDetails(BaseModel): roi_join_pause: timedelta = timedelta(milliseconds=700) "Silence inserted between concatenated regions in an assignment clip" + boundary_tolerance: timedelta = timedelta(milliseconds=200) + "Allowed joined annotation/GT interval boundary misalignment when matching transcriptions" + standard_assignment_duration: timedelta = timedelta(seconds=60) "Target audio duration per assignment" @@ -125,6 +135,14 @@ def parse_audio_manifest(manifest: JobManifest) -> TranscriptionTaskSpecificatio details_fields["roi_max_duration"] = timedelta( seconds=manifest_details.max_segment_duration ) + if manifest_details.min_gt_span_duration is not None: + details_fields["min_gt_span_duration"] = timedelta( + seconds=manifest_details.min_gt_span_duration + ) + if manifest_details.boundary_tolerance is not None: + details_fields["boundary_tolerance"] = timedelta( + milliseconds=manifest_details.boundary_tolerance + ) if manifest_details.validation_overhead is not None: details_fields["validation_overhead"] = manifest_details.validation_overhead / 100.0 if manifest_details.min_composition is not None: diff --git a/packages/examples/cvat/recording-oracle/src/handlers/completion/handlers.py b/packages/examples/cvat/recording-oracle/src/handlers/completion/handlers.py index ef51d1d71f..d48355ba10 100644 --- a/packages/examples/cvat/recording-oracle/src/handlers/completion/handlers.py +++ b/packages/examples/cvat/recording-oracle/src/handlers/completion/handlers.py @@ -88,7 +88,7 @@ def _handle_result(self, export_result: FinalResult): logger.info( f"Result uploading for escrow_address={escrow_address}: successful, " - f"average annotation quality is {export_result.average_quality * 100:.2f}%" + f"average quality score is {export_result.average_quality * 100:.2f}%" ) recor_merged_annotations_path = self._compose_validation_results_bucket_filename( diff --git a/packages/examples/cvat/recording-oracle/src/handlers/validation/handlers.py b/packages/examples/cvat/recording-oracle/src/handlers/validation/handlers.py index 536e4dcbcd..750a5438e3 100644 --- a/packages/examples/cvat/recording-oracle/src/handlers/validation/handlers.py +++ b/packages/examples/cvat/recording-oracle/src/handlers/validation/handlers.py @@ -100,7 +100,7 @@ def _handle_validation_result(self, validation_result: ValidationResult): if isinstance(validation_result, ValidationSuccess): logger.info( f"Validation for escrow_address={escrow_address}: successful, " - f"average annotation quality is {validation_result.average_quality * 100:.2f}%" + f"average quality score is {validation_result.average_quality * 100:.2f}%" ) recor_validation_meta_path = self._compose_validation_results_bucket_filename( diff --git a/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/audio.py b/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/audio.py index 6aad32e374..27320ed9a2 100644 --- a/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/audio.py +++ b/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/audio.py @@ -41,7 +41,7 @@ if TYPE_CHECKING: from src.core.annotation_meta import JobMeta - from src.core.tasks.audio_transcription.meta import Assignment, PlacedRegion + from src.core.tasks.audio_transcription.meta import Clip, PlacedRegion from src.services.cloud.client import StorageClient # Groups are keyed by the presented honeypot so each honeypot's transcription is aligned in @@ -54,9 +54,7 @@ class AudioTaskQualityChecker(TaskQualityChecker): Scores audio-transcription assignments. CVAT doesn't support quality computation for such tasks yet, so the computations are performed in the oracle. - For each job it reconstructs the annotator's honeypot transcriptions (from the per-assignment - TSV + the assignment/clip mapping), aligns them against the ground truth per presented honeypot, - and computes the WER/CER error rate. + Each assignment annotations are scored per their honeypots, computing the WER/CER metrics. Honeypot rotation is not used. """ @@ -96,24 +94,36 @@ def _validate_jobs(self) -> None: text_attribute=transcription_attr, normalizer=self._normalizer_config(spec.details.normalizer), grouping=GroupingConfig(attribute=_GROUP_ATTR, strategy=GroupingStrategy.JOIN), + overlap_tolerance_ms=round(spec.details.boundary_tolerance.total_seconds() * 1000), ) normalizer = Normalizer(req.normalizer) client = make_cloud_client( BucketAccessInfo.parse_obj(Config.exchange_oracle_storage_config) ) - gt_rows = self._load_gt(client) - assignments = self._load_assignments(client) - annotations = self._download_assignment_tsvs(client) + gt_annotations = self._load_gt(client) + clips_by_id = self._load_clips_meta(client) + task_clips = self._load_task_clips(client) + annotation_bytes = self._download_assignment_annotations(client) job_results: dict[int, float] = {} rejected_jobs: dict[int, object] = {} for job_meta in self._meta.jobs: + ds_annotations = self._parse_annotations(annotation_bytes[job_meta.job_id]) + # Each CVAT task holds exactly one clip; join the job to its clip via the EO-recorded + # task -> clip mapping. An empty submission still resolves here, so its honeypots score + # as full deletions (rate 1.0) instead of being treated as unverifiable. + clip_meta = clips_by_id[task_clips[job_meta.task_id]] + gt_intervals, hyp_intervals = self._build_intervals( - annotations[job_meta.job_id], assignments, gt_rows, transcription_attr + ds_annotations, + gt_annotations, + clip_meta=clip_meta, + transcription_attr=transcription_attr, ) if not gt_intervals: + # No honeypots available - not expected by construction. job_results[job_meta.job_id] = UNKNOWN_QUALITY rejected_jobs[job_meta.job_id] = TooFewGtError() continue @@ -155,28 +165,39 @@ def _load_gt(self, client: StorageClient) -> dict[str, InputGtRegion]: ) return {region.id: region for region in parse_gt_tsv(data)} - def _load_assignments(self, client: StorageClient) -> dict[str, Assignment]: + def _load_clips_meta(self, client: StorageClient) -> dict[str, Clip]: + "Returns clip id -> clip meta mapping." + data = client.download_file( compose_data_bucket_filename( - self.escrow_address, self.chain_id, TaskMetaLayout.ASSIGNMENTS_FILENAME + self.escrow_address, self.chain_id, TaskMetaLayout.CLIPS_FILENAME ) ) - return {a.clip_filename: a for a in TaskMetaSerializer().parse_assignments(data)} + return {a.id: a for a in TaskMetaSerializer().parse_clips(data)} + + def _load_task_clips(self, client: StorageClient) -> dict[int, str]: + "Returns CVAT task id -> clip id mapping." + + data = client.download_file( + compose_data_bucket_filename( + self.escrow_address, self.chain_id, TaskMetaLayout.TASK_CLIPS_FILENAME + ) + ) + return TaskMetaSerializer().parse_task_clips(data) + + @staticmethod + def _parse_annotations(annotation: bytes) -> list[dict]: + return list(csv.DictReader(io.StringIO(annotation.decode("utf-8")), delimiter="\t")) def _build_intervals( self, - annotation: bytes, - assignments: dict[str, Assignment], - gt_rows: dict[str, InputGtRegion], - attr: str, + ds_annotations: list[dict], + gt_annotations: dict[str, InputGtRegion], + *, + clip_meta: Clip, + transcription_attr: str, ) -> tuple[list[Interval], list[Interval]]: - rows = list(csv.DictReader(io.StringIO(annotation.decode("utf-8")), delimiter="\t")) - - assignment = self._resolve_assignment(rows, assignments) - if assignment is None: - return [], [] - - gt_placements = [p for p in assignment.placed if p.kind == RegionKind.gt] + gt_placements = [p for p in clip_meta.placed if p.kind == RegionKind.gt] # Ground-truth reference intervals for every presented honeypot (built regardless of what # the annotator produced, so an omitted honeypot becomes a missing group = full errors). @@ -185,16 +206,20 @@ def _build_intervals( for placed in gt_placements: honeypot_id = self._honeypot_id(placed) for region_id in placed.input_ids: - gt = gt_rows.get(region_id) - if gt is None: - continue + try: + gt = gt_annotations[region_id] + except KeyError as e: + raise AssertionError( + f"Honeypot region '{region_id}' is missing from gt.tsv" + ) from e + gt_intervals.append( Interval( id=counter, start=gt.start.total_seconds() * 1000.0, stop=gt.stop.total_seconds() * 1000.0, - label=gt.label or "", - extra={attr: gt.text, _GROUP_ATTR: honeypot_id}, + label=gt.label, + extra={transcription_attr: gt.text, _GROUP_ATTR: honeypot_id}, ) ) counter += 1 @@ -202,21 +227,21 @@ def _build_intervals( # Annotator (hypothesis) intervals: only rows falling inside a honeypot window, mapped from # clip-time back to source-file time. hyp_intervals: list[Interval] = [] - for row in rows: + for row in ds_annotations: start_s = parse_time(row["start"]).total_seconds() stop_s = parse_time(row["stop"]).total_seconds() - placed = self._placed_at(assignment, start_s) - if placed is None or placed.kind != RegionKind.gt: + placed = self._honeypot_at(clip_meta, start_s, stop_s) + if placed is None: continue - offset = placed.file_start - placed.clip_start + offset = placed.source_start - placed.clip_start hyp_intervals.append( Interval( id=counter, start=(start_s + offset) * 1000.0, stop=(stop_s + offset) * 1000.0, - label=row.get("label", "") or "", + label=row["label"], extra={ - attr: row.get(attr, "") or "", + transcription_attr: row[transcription_attr], _GROUP_ATTR: self._honeypot_id(placed), }, ) @@ -225,7 +250,7 @@ def _build_intervals( return gt_intervals, hyp_intervals - def _download_assignment_tsvs(self, client: StorageClient) -> dict[int, bytes]: + def _download_assignment_annotations(self, client: StorageClient) -> dict[int, bytes]: "Download every job's per-assignment annotation TSV up front, keyed by cvat job id." return { job_meta.job_id: self._download_assignment_tsv(client, job_meta) @@ -243,16 +268,6 @@ def _download_assignment_tsv(self, client: StorageClient, job_meta: JobMeta) -> ) ) - @staticmethod - def _resolve_assignment( - rows: list[dict], assignments: dict[str, Assignment] - ) -> Assignment | None: - for row in rows: - clip = row["filename"].rsplit("/", 1)[-1] - if clip in assignments: - return assignments[clip] - return None - @staticmethod def _honeypot_id(placed: PlacedRegion) -> str: # A gt placement is always built from >=1 GT input ROI, so input_ids is non-empty. @@ -260,11 +275,31 @@ def _honeypot_id(placed: PlacedRegion) -> str: return ",".join(placed.input_ids) @staticmethod - def _placed_at(assignment: Assignment, clip_time_s: float) -> PlacedRegion | None: - return next( - (p for p in assignment.placed if p.clip_start <= clip_time_s < p.clip_stop), - None, - ) + def _honeypot_at(clip: Clip, start_s: float, stop_s: float) -> PlacedRegion | None: + """ + Return the honeypot placement that fully contains the annotator interval ``[start_s, + stop_s]`` within its inclusion window. + + Each honeypot window is extended by half of the join pause to the adjacent region on each + side (``[hp_start - pause/2, hp_stop + pause/2]``); an interval counts for the honeypot only + if both its endpoints fall inside. Annotator segment boundaries don't align with the + synthetic cut/pause boundaries: the half-pause margin keeps a segment that drifts into the + surrounding pause, while requiring both endpoints inside excludes one that spills into an + adjacent region. + """ + regions = sorted(clip.placed, key=lambda p: p.clip_start) + for idx, p in enumerate(regions): + if p.kind != RegionKind.gt: + continue + lo = p.clip_start + hi = p.clip_stop + if idx > 0: + lo -= (p.clip_start - regions[idx - 1].clip_stop) / 2 + if idx < len(regions) - 1: + hi += (regions[idx + 1].clip_start - p.clip_stop) / 2 + if lo <= start_s and stop_s <= hi: + return p + return None def _job_error_rate(self, report, req: TranscriptionRequirement, normalizer) -> float: """ @@ -299,6 +334,6 @@ def _job_error_rate(self, report, req: TranscriptionRequirement, normalizer) -> @staticmethod def _group_units(group: list[Interval], req: TranscriptionRequirement, normalizer) -> list[str]: joined = req.grouping.join_separator.join( - iv.extra.get(req.text_attribute, "") for iv in sorted(group, key=lambda i: i.start) + iv.extra[req.text_attribute] for iv in sorted(group, key=lambda i: i.start) ) return tokenize(normalizer(joined), granularity=req.granularity) From 7c6ce118d4f3e3908ff241bd9ee2fd4376509d58 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Mon, 13 Jul 2026 14:05:02 +0300 Subject: [PATCH 35/53] chore(cvat): relock poetry for the audio feature deps Reconcile lockfiles on top of the lib-bump base (cvat-sdk 2.69, audio-qe, numpy) after rebasing the audio work onto the library-bump + blockchain-node-drop branch. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../examples/cvat/exchange-oracle/poetry.lock | 6729 +++++++++-------- .../cvat/recording-oracle/poetry.lock | 105 +- 2 files changed, 3720 insertions(+), 3114 deletions(-) diff --git a/packages/examples/cvat/exchange-oracle/poetry.lock b/packages/examples/cvat/exchange-oracle/poetry.lock index 720a2f24dd..48e1008447 100644 --- a/packages/examples/cvat/exchange-oracle/poetry.lock +++ b/packages/examples/cvat/exchange-oracle/poetry.lock @@ -22,150 +22,202 @@ redis = ["redis (>=4.2.0)"] [[package]] name = "aiohappyeyeballs" -version = "2.4.6" +version = "2.7.1" description = "Happy Eyeballs for asyncio" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "aiohappyeyeballs-2.4.6-py3-none-any.whl", hash = "sha256:147ec992cf873d74f5062644332c539fcd42956dc69453fe5204195e560517e1"}, - {file = "aiohappyeyeballs-2.4.6.tar.gz", hash = "sha256:9b05052f9042985d32ecbe4b59a77ae19c006a78f1344d7fdad69d28ded3d0b0"}, + {file = "aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472"}, + {file = "aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d"}, ] [[package]] name = "aiohttp" -version = "3.11.12" +version = "3.14.1" description = "Async http client/server framework (asyncio)" optional = false -python-versions = ">=3.9" -files = [ - {file = "aiohttp-3.11.12-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:aa8a8caca81c0a3e765f19c6953416c58e2f4cc1b84829af01dd1c771bb2f91f"}, - {file = "aiohttp-3.11.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84ede78acde96ca57f6cf8ccb8a13fbaf569f6011b9a52f870c662d4dc8cd854"}, - {file = "aiohttp-3.11.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:584096938a001378484aa4ee54e05dc79c7b9dd933e271c744a97b3b6f644957"}, - {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:392432a2dde22b86f70dd4a0e9671a349446c93965f261dbaecfaf28813e5c42"}, - {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:88d385b8e7f3a870146bf5ea31786ef7463e99eb59e31db56e2315535d811f55"}, - {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b10a47e5390c4b30a0d58ee12581003be52eedd506862ab7f97da7a66805befb"}, - {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b5263dcede17b6b0c41ef0c3ccce847d82a7da98709e75cf7efde3e9e3b5cae"}, - {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50c5c7b8aa5443304c55c262c5693b108c35a3b61ef961f1e782dd52a2f559c7"}, - {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1c031a7572f62f66f1257db37ddab4cb98bfaf9b9434a3b4840bf3560f5e788"}, - {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:7e44eba534381dd2687be50cbd5f2daded21575242ecfdaf86bbeecbc38dae8e"}, - {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:145a73850926018ec1681e734cedcf2716d6a8697d90da11284043b745c286d5"}, - {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:2c311e2f63e42c1bf86361d11e2c4a59f25d9e7aabdbdf53dc38b885c5435cdb"}, - {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ea756b5a7bac046d202a9a3889b9a92219f885481d78cd318db85b15cc0b7bcf"}, - {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:526c900397f3bbc2db9cb360ce9c35134c908961cdd0ac25b1ae6ffcaa2507ff"}, - {file = "aiohttp-3.11.12-cp310-cp310-win32.whl", hash = "sha256:b8d3bb96c147b39c02d3db086899679f31958c5d81c494ef0fc9ef5bb1359b3d"}, - {file = "aiohttp-3.11.12-cp310-cp310-win_amd64.whl", hash = "sha256:7fe3d65279bfbee8de0fb4f8c17fc4e893eed2dba21b2f680e930cc2b09075c5"}, - {file = "aiohttp-3.11.12-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:87a2e00bf17da098d90d4145375f1d985a81605267e7f9377ff94e55c5d769eb"}, - {file = "aiohttp-3.11.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b34508f1cd928ce915ed09682d11307ba4b37d0708d1f28e5774c07a7674cac9"}, - {file = "aiohttp-3.11.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:936d8a4f0f7081327014742cd51d320296b56aa6d324461a13724ab05f4b2933"}, - {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de1378f72def7dfb5dbd73d86c19eda0ea7b0a6873910cc37d57e80f10d64e1"}, - {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9d45dbb3aaec05cf01525ee1a7ac72de46a8c425cb75c003acd29f76b1ffe94"}, - {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:930ffa1925393381e1e0a9b82137fa7b34c92a019b521cf9f41263976666a0d6"}, - {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8340def6737118f5429a5df4e88f440746b791f8f1c4ce4ad8a595f42c980bd5"}, - {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4016e383f91f2814e48ed61e6bda7d24c4d7f2402c75dd28f7e1027ae44ea204"}, - {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c0600bcc1adfaaac321422d615939ef300df81e165f6522ad096b73439c0f58"}, - {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:0450ada317a65383b7cce9576096150fdb97396dcfe559109b403c7242faffef"}, - {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:850ff6155371fd802a280f8d369d4e15d69434651b844bde566ce97ee2277420"}, - {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8fd12d0f989c6099e7b0f30dc6e0d1e05499f3337461f0b2b0dadea6c64b89df"}, - {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:76719dd521c20a58a6c256d058547b3a9595d1d885b830013366e27011ffe804"}, - {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:97fe431f2ed646a3b56142fc81d238abcbaff08548d6912acb0b19a0cadc146b"}, - {file = "aiohttp-3.11.12-cp311-cp311-win32.whl", hash = "sha256:e10c440d142fa8b32cfdb194caf60ceeceb3e49807072e0dc3a8887ea80e8c16"}, - {file = "aiohttp-3.11.12-cp311-cp311-win_amd64.whl", hash = "sha256:246067ba0cf5560cf42e775069c5d80a8989d14a7ded21af529a4e10e3e0f0e6"}, - {file = "aiohttp-3.11.12-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e392804a38353900c3fd8b7cacbea5132888f7129f8e241915e90b85f00e3250"}, - {file = "aiohttp-3.11.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8fa1510b96c08aaad49303ab11f8803787c99222288f310a62f493faf883ede1"}, - {file = "aiohttp-3.11.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dc065a4285307607df3f3686363e7f8bdd0d8ab35f12226362a847731516e42c"}, - {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddb31f8474695cd61fc9455c644fc1606c164b93bff2490390d90464b4655df"}, - {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9dec0000d2d8621d8015c293e24589d46fa218637d820894cb7356c77eca3259"}, - {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3552fe98e90fdf5918c04769f338a87fa4f00f3b28830ea9b78b1bdc6140e0d"}, - {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dfe7f984f28a8ae94ff3a7953cd9678550dbd2a1f9bda5dd9c5ae627744c78e"}, - {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a481a574af914b6e84624412666cbfbe531a05667ca197804ecc19c97b8ab1b0"}, - {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1987770fb4887560363b0e1a9b75aa303e447433c41284d3af2840a2f226d6e0"}, - {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a4ac6a0f0f6402854adca4e3259a623f5c82ec3f0c049374133bcb243132baf9"}, - {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c96a43822f1f9f69cc5c3706af33239489a6294be486a0447fb71380070d4d5f"}, - {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a5e69046f83c0d3cb8f0d5bd9b8838271b1bc898e01562a04398e160953e8eb9"}, - {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:68d54234c8d76d8ef74744f9f9fc6324f1508129e23da8883771cdbb5818cbef"}, - {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9fd9dcf9c91affe71654ef77426f5cf8489305e1c66ed4816f5a21874b094b9"}, - {file = "aiohttp-3.11.12-cp312-cp312-win32.whl", hash = "sha256:0ed49efcd0dc1611378beadbd97beb5d9ca8fe48579fc04a6ed0844072261b6a"}, - {file = "aiohttp-3.11.12-cp312-cp312-win_amd64.whl", hash = "sha256:54775858c7f2f214476773ce785a19ee81d1294a6bedc5cc17225355aab74802"}, - {file = "aiohttp-3.11.12-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:413ad794dccb19453e2b97c2375f2ca3cdf34dc50d18cc2693bd5aed7d16f4b9"}, - {file = "aiohttp-3.11.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a93d28ed4b4b39e6f46fd240896c29b686b75e39cc6992692e3922ff6982b4c"}, - {file = "aiohttp-3.11.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d589264dbba3b16e8951b6f145d1e6b883094075283dafcab4cdd564a9e353a0"}, - {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5148ca8955affdfeb864aca158ecae11030e952b25b3ae15d4e2b5ba299bad2"}, - {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:525410e0790aab036492eeea913858989c4cb070ff373ec3bc322d700bdf47c1"}, - {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bd8695be2c80b665ae3f05cb584093a1e59c35ecb7d794d1edd96e8cc9201d7"}, - {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0203433121484b32646a5f5ea93ae86f3d9559d7243f07e8c0eab5ff8e3f70e"}, - {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40cd36749a1035c34ba8d8aaf221b91ca3d111532e5ccb5fa8c3703ab1b967ed"}, - {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a7442662afebbf7b4c6d28cb7aab9e9ce3a5df055fc4116cc7228192ad6cb484"}, - {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8a2fb742ef378284a50766e985804bd6adb5adb5aa781100b09befdbfa757b65"}, - {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cee3b117a8d13ab98b38d5b6bdcd040cfb4181068d05ce0c474ec9db5f3c5bb"}, - {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f6a19bcab7fbd8f8649d6595624856635159a6527861b9cdc3447af288a00c00"}, - {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e4cecdb52aaa9994fbed6b81d4568427b6002f0a91c322697a4bfcc2b2363f5a"}, - {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:30f546358dfa0953db92ba620101fefc81574f87b2346556b90b5f3ef16e55ce"}, - {file = "aiohttp-3.11.12-cp313-cp313-win32.whl", hash = "sha256:ce1bb21fc7d753b5f8a5d5a4bae99566386b15e716ebdb410154c16c91494d7f"}, - {file = "aiohttp-3.11.12-cp313-cp313-win_amd64.whl", hash = "sha256:f7914ab70d2ee8ab91c13e5402122edbc77821c66d2758abb53aabe87f013287"}, - {file = "aiohttp-3.11.12-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7c3623053b85b4296cd3925eeb725e386644fd5bc67250b3bb08b0f144803e7b"}, - {file = "aiohttp-3.11.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:67453e603cea8e85ed566b2700efa1f6916aefbc0c9fcb2e86aaffc08ec38e78"}, - {file = "aiohttp-3.11.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6130459189e61baac5a88c10019b21e1f0c6d00ebc770e9ce269475650ff7f73"}, - {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9060addfa4ff753b09392efe41e6af06ea5dd257829199747b9f15bfad819460"}, - {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34245498eeb9ae54c687a07ad7f160053911b5745e186afe2d0c0f2898a1ab8a"}, - {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8dc0fba9a74b471c45ca1a3cb6e6913ebfae416678d90529d188886278e7f3f6"}, - {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a478aa11b328983c4444dacb947d4513cb371cd323f3845e53caeda6be5589d5"}, - {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c160a04283c8c6f55b5bf6d4cad59bb9c5b9c9cd08903841b25f1f7109ef1259"}, - {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:edb69b9589324bdc40961cdf0657815df674f1743a8d5ad9ab56a99e4833cfdd"}, - {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4ee84c2a22a809c4f868153b178fe59e71423e1f3d6a8cd416134bb231fbf6d3"}, - {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:bf4480a5438f80e0f1539e15a7eb8b5f97a26fe087e9828e2c0ec2be119a9f72"}, - {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:e6b2732ef3bafc759f653a98881b5b9cdef0716d98f013d376ee8dfd7285abf1"}, - {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f752e80606b132140883bb262a457c475d219d7163d996dc9072434ffb0784c4"}, - {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ab3247d58b393bda5b1c8f31c9edece7162fc13265334217785518dd770792b8"}, - {file = "aiohttp-3.11.12-cp39-cp39-win32.whl", hash = "sha256:0d5176f310a7fe6f65608213cc74f4228e4f4ce9fd10bcb2bb6da8fc66991462"}, - {file = "aiohttp-3.11.12-cp39-cp39-win_amd64.whl", hash = "sha256:74bd573dde27e58c760d9ca8615c41a57e719bff315c9adb6f2a4281a28e8798"}, - {file = "aiohttp-3.11.12.tar.gz", hash = "sha256:7603ca26d75b1b86160ce1bbe2787a0b706e592af5b2504e12caa88a217767b0"}, +python-versions = ">=3.10" +files = [ + {file = "aiohttp-3.14.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8f6bb621e5863cfe8fe5ff5468002d200ec31f30f1280b259dc505b02595099e"}, + {file = "aiohttp-3.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f7215cb3933784f79ed20e5f050e15984f390424339b22375d5a53c933a0491"}, + {file = "aiohttp-3.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9d4e294455b23a68c9b8f042d0e8e377a265bcb15332753695f6e5b6819e0ce"}, + {file = "aiohttp-3.14.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b238af795833d5731d049d82bc84b768ae6f8f97f0495963b3ed9935c5901cc3"}, + {file = "aiohttp-3.14.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4e5e0ae56914ecdbf446493addefc0159053dd53962cef37d7839f37f73d505"}, + {file = "aiohttp-3.14.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:092e4ce3619a7c6dee52a6bdabda973d9b34b66781f840ce93c7e0cec30cf521"}, + {file = "aiohttp-3.14.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb33777ea21e8b7ecde0e6fc84f598be0a1192eab1a63bc746d75aa75d38e7bd"}, + {file = "aiohttp-3.14.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23119f8fd4f5d16902ed459b63b100bcd269628075162bddac56cc7b5273b3fb"}, + {file = "aiohttp-3.14.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:57fc6745a4b7d0f5a9eb4f40a69718be6c0bc1b8368cc9fe89e90118719f4f42"}, + {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6fd35beba67c4183b09375c5fff9accb47524191a244a99f95fd4472f5402c2b"}, + {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:672b9d65f42eb877f5c3f234a4547e4e1a226ca8c2eed879bb34670a0ce51192"}, + {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:24ba13339fed9251d9b1a1bec8c7ab84c0d1675d79d33501e11f94f8b9a84e05"}, + {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:94da27378da0610e341c4d30de29a191672683cc82b8f9556e8f7c7212a020fe"}, + {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:52cdac9432d8b4a719f35094a818d95adcae0f0b4fe9b9b921909e0c87de9e7d"}, + {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:672ac254412a24d0d0cf00a9e6c238877e4be5e5fa2d188832c1244f45f31966"}, + {file = "aiohttp-3.14.1-cp310-cp310-win32.whl", hash = "sha256:2fe3607e71acc6ebb0ec8e492a247bf7a291226192dc0084236dfc12478916f6"}, + {file = "aiohttp-3.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:30099eda75a53c32efb0920e9c33c195314d2cc1c680fbfd30894932ac5f27df"}, + {file = "aiohttp-3.14.1-cp310-cp310-win_arm64.whl", hash = "sha256:5a837f49d901f9e368651b676912bff1104ed8c1a83b280bcd7b29adccef5c9c"}, + {file = "aiohttp-3.14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa00140699487bd435fde4342d85c94cb256b7cd3a5b9c3396c67f19922afda2"}, + {file = "aiohttp-3.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c1af67559445498b502030c35c59db59966f47041ca9de5b4e707f86bd10b5f"}, + {file = "aiohttp-3.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d44ec478e713ee7f29b439f7eb8dc2b9d4079e11ae114d2c2ac3d5daf30516c8"}, + {file = "aiohttp-3.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3b1a184a9a8f548a6b73f1e26b96b052193e4b3175ed7342aaf1151a1f00a04"}, + {file = "aiohttp-3.14.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5f2504bc0322437c9a1ff6d3333ca56c7477b727c995f036b976ae17b98372c8"}, + {file = "aiohttp-3.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73f05ea02013e02512c3bf42714f1208c57168c779cc6fe23516e4543089d0a6"}, + {file = "aiohttp-3.14.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:797457503c2d426bee06eef808d07b31ede30b65e054444e7de64cad0061b7af"}, + {file = "aiohttp-3.14.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b821a1f7dedf7e37450654e620038ac3b2e81e8fa6ea269337e97101978ec730"}, + {file = "aiohttp-3.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4cd96b5ba05d67ed0cf00b5b405c8cd99586d8e3481e8ee0a831057591af7621"}, + {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d459b98a932296c6f0e94f87511a0b1b90a8a02c30a50e60a297619cd5a58ee"}, + {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:764457a7be60825fb770a644852ff717bcbb5042f189f2bd16df61a81b3f6573"}, + {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f7a16ef45b081454ef844502d87a848876c490c4cb5c650c230f6ec79ed2c1e7"}, + {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2fbc3ed048b3475b9f0cbcb9978e9d2d3511acd91ead203af26ed9f0056004cf"}, + {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bedb0cd073cc2dc035e30aeb99444389d3cd2113afe4ef9fcd23d439f5bade85"}, + {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b6feea921016eb3d4e04d65fc4e9ca402d1a3801f562aef94989f54694917af3"}, + {file = "aiohttp-3.14.1-cp311-cp311-win32.whl", hash = "sha256:313701e488100074ce99850404ee36e741abf6330179fec908a1944ecf570126"}, + {file = "aiohttp-3.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:03ab4530fdcb3a543a122ba4b65ac9919da9fe9f78a03d328a6e38ff962f7aa5"}, + {file = "aiohttp-3.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:486f7d16ed54c39c2cbd7ca71fd8ba2b8bb7860df65bd7b6ed640bab96a38a8b"}, + {file = "aiohttp-3.14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480"}, + {file = "aiohttp-3.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d"}, + {file = "aiohttp-3.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2"}, + {file = "aiohttp-3.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2"}, + {file = "aiohttp-3.14.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a"}, + {file = "aiohttp-3.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264"}, + {file = "aiohttp-3.14.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842"}, + {file = "aiohttp-3.14.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c"}, + {file = "aiohttp-3.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95"}, + {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199"}, + {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817"}, + {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a"}, + {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4"}, + {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087"}, + {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3"}, + {file = "aiohttp-3.14.1-cp312-cp312-win32.whl", hash = "sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4"}, + {file = "aiohttp-3.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271"}, + {file = "aiohttp-3.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847"}, + {file = "aiohttp-3.14.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178"}, + {file = "aiohttp-3.14.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf"}, + {file = "aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd"}, + {file = "aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe"}, + {file = "aiohttp-3.14.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4"}, + {file = "aiohttp-3.14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876"}, + {file = "aiohttp-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da"}, + {file = "aiohttp-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6"}, + {file = "aiohttp-3.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96"}, + {file = "aiohttp-3.14.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f"}, + {file = "aiohttp-3.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296"}, + {file = "aiohttp-3.14.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa"}, + {file = "aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451"}, + {file = "aiohttp-3.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c"}, + {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca"}, + {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09"}, + {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397"}, + {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080"}, + {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345"}, + {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588"}, + {file = "aiohttp-3.14.1-cp313-cp313-win32.whl", hash = "sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780"}, + {file = "aiohttp-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a"}, + {file = "aiohttp-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8"}, + {file = "aiohttp-3.14.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15"}, + {file = "aiohttp-3.14.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8"}, + {file = "aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3"}, + {file = "aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba"}, + {file = "aiohttp-3.14.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397"}, + {file = "aiohttp-3.14.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448"}, + {file = "aiohttp-3.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004"}, + {file = "aiohttp-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983"}, + {file = "aiohttp-3.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe"}, + {file = "aiohttp-3.14.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333"}, + {file = "aiohttp-3.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0"}, + {file = "aiohttp-3.14.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a"}, + {file = "aiohttp-3.14.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602"}, + {file = "aiohttp-3.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca"}, + {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35"}, + {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844"}, + {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496"}, + {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5"}, + {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95"}, + {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444"}, + {file = "aiohttp-3.14.1-cp314-cp314-win32.whl", hash = "sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0"}, + {file = "aiohttp-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719"}, + {file = "aiohttp-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec"}, + {file = "aiohttp-3.14.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2"}, + {file = "aiohttp-3.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340"}, + {file = "aiohttp-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d"}, + {file = "aiohttp-3.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94"}, + {file = "aiohttp-3.14.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc"}, + {file = "aiohttp-3.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251"}, + {file = "aiohttp-3.14.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1"}, + {file = "aiohttp-3.14.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3"}, + {file = "aiohttp-3.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c"}, + {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203"}, + {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1"}, + {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665"}, + {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1"}, + {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d"}, + {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c"}, + {file = "aiohttp-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365"}, + {file = "aiohttp-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9"}, + {file = "aiohttp-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6"}, + {file = "aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035"}, ] [package.dependencies] -aiohappyeyeballs = ">=2.3.0" -aiosignal = ">=1.1.2" +aiohappyeyeballs = ">=2.5.0" +aiosignal = ">=1.4.0" async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} attrs = ">=17.3.0" frozenlist = ">=1.1.1" multidict = ">=4.5,<7.0" propcache = ">=0.2.0" +typing_extensions = {version = ">=4.4", markers = "python_version < \"3.13\""} yarl = ">=1.17.0,<2.0" [package.extras] -speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] +speedups = ["Brotli (>=1.2)", "aiodns (>=3.3.0)", "backports.zstd", "brotlicffi (>=1.2)"] [[package]] name = "aiosignal" -version = "1.3.2" +version = "1.4.0" description = "aiosignal: a list of registered asynchronous callbacks" optional = false python-versions = ">=3.9" files = [ - {file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"}, - {file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"}, + {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, + {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, ] [package.dependencies] frozenlist = ">=1.1.0" +typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} [[package]] name = "alembic" -version = "1.13.1" +version = "1.18.5" description = "A database migration tool for SQLAlchemy." optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" files = [ - {file = "alembic-1.13.1-py3-none-any.whl", hash = "sha256:2edcc97bed0bd3272611ce3a98d98279e9c209e7186e43e75bbb1b2bdfdbcc43"}, - {file = "alembic-1.13.1.tar.gz", hash = "sha256:4932c8558bf68f2ee92b9bbcb8218671c627064d5b08939437af6d77dc05e595"}, + {file = "alembic-1.18.5-py3-none-any.whl", hash = "sha256:06d8ba9d04558022f5395e9317de03d270f3dced49cee01f89fe7a13c26f14bc"}, + {file = "alembic-1.18.5.tar.gz", hash = "sha256:1554982221dd17e9a749b53902407578eb305e453f71999e8c7f0a48389fff8e"}, ] [package.dependencies] Mako = "*" -SQLAlchemy = ">=1.3.0" -typing-extensions = ">=4" +SQLAlchemy = ">=1.4.23" +tomli = {version = "*", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.12" [package.extras] -tz = ["backports.zoneinfo"] +tz = ["tzdata"] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +description = "Document parameters, class attributes, return types, and variables inline, with Annotated." +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320"}, + {file = "annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4"}, +] [[package]] name = "annotated-types" @@ -180,264 +232,222 @@ files = [ [[package]] name = "anyio" -version = "4.2.0" -description = "High level compatibility layer for multiple asynchronous event loop implementations" +version = "4.14.1" +description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" files = [ - {file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"}, - {file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"}, + {file = "anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72"}, + {file = "anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e"}, ] [package.dependencies] exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" -sniffio = ">=1.1" -typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (>=0.23)"] +trio = ["trio (>=0.32.0)"] [[package]] name = "apscheduler" -version = "3.10.4" +version = "3.11.3" description = "In-process task scheduler with Cron-like capabilities" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "APScheduler-3.10.4-py3-none-any.whl", hash = "sha256:fb91e8a768632a4756a585f79ec834e0e27aad5860bac7eaa523d9ccefd87661"}, - {file = "APScheduler-3.10.4.tar.gz", hash = "sha256:e6df071b27d9be898e486bc7940a7be50b4af2e9da7c08f0744a96d4bd4cef4a"}, + {file = "apscheduler-3.11.3-py3-none-any.whl", hash = "sha256:bbeb2ec02d23d3c06a6c07ed7f0f3939ada6680eb121fae809a69bb42c537a30"}, + {file = "apscheduler-3.11.3.tar.gz", hash = "sha256:cd2fcc9330039a81a5893472ad49facf23a6d5604cbe1d918c835c6de7834d5a"}, ] [package.dependencies] -pytz = "*" -six = ">=1.4.0" -tzlocal = ">=2.0,<3.dev0 || >=4.dev0" +tzlocal = ">=3.0" [package.extras] -doc = ["sphinx", "sphinx-rtd-theme"] +doc = ["packaging", "sphinx", "sphinx-rtd-theme (>=1.3.0)"] +etcd = ["etcd3", "protobuf (<=3.21.0)"] gevent = ["gevent"] mongodb = ["pymongo (>=3.0)"] redis = ["redis (>=3.0)"] rethinkdb = ["rethinkdb (>=2.4.0)"] sqlalchemy = ["sqlalchemy (>=1.4)"] -testing = ["pytest", "pytest-asyncio", "pytest-cov", "pytest-tornado5"] +test = ["APScheduler[etcd,mongodb,redis,rethinkdb,sqlalchemy,tornado,zookeeper]", "PySide6", "anyio (>=4.5.2)", "gevent", "pytest", "pytest-timeout", "pytz", "twisted"] tornado = ["tornado (>=4.3)"] twisted = ["twisted"] zookeeper = ["kazoo"] [[package]] name = "async-timeout" -version = "4.0.3" +version = "5.0.1" description = "Timeout context manager for asyncio programs" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, - {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, + {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, + {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, ] [[package]] name = "attrs" -version = "23.2.0" +version = "26.1.0" description = "Classes Without Boilerplate" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, + {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, + {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, ] -[package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] -tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] - [[package]] name = "bitarray" -version = "3.1.0" +version = "3.8.2" description = "efficient arrays of booleans -- C extension" optional = false python-versions = "*" files = [ - {file = "bitarray-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3c1fad6456993f604726dcb21d1f003430988d5138f858d79e5b8682f56dfad6"}, - {file = "bitarray-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0dc0f28340929fffa17fbd3eca5bfae5c1827f3000c0bd9312999d8c5b1464a0"}, - {file = "bitarray-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84de57891a80944e259956fd72bd542290b76c063182f0e85ce5380c49cfcfb6"}, - {file = "bitarray-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3f4be402e7c611a0a7048f3d042b9e4d697ca6f721d08098118d1ae8bb8a69c"}, - {file = "bitarray-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eabaee3c7d411f6ed48f92752e61a409530a0ebf65498bd408432800a804e0b8"}, - {file = "bitarray-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cb6c270c49d6779af97586a955977493146626ae34710a4c1ec84cb0115f4d4"}, - {file = "bitarray-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:481a00d710c7811b38563f6fe22da20be2e6f722196f68873e12d23f4fdf82c1"}, - {file = "bitarray-3.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b81bbefe9d205af66a15a3097f8b66f29fe767fa9cb5dd5a7881f53d2505c2d5"}, - {file = "bitarray-3.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:506df7b12e1380f56cfa20b9c203518afe585f0b86e850a96e3691cf0175e4c0"}, - {file = "bitarray-3.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:43ec740770723de6c99f0a858807ef905664f31cf254e0b803f3dd2797537d7a"}, - {file = "bitarray-3.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:4deac3da54c3df136d6615a2410d5a6170353b4d08142273266296c376fb1889"}, - {file = "bitarray-3.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c58cbd0cc9f6347fffa36a6870fac695cd6e98c39fcfd8002d44a70fba03cd8c"}, - {file = "bitarray-3.1.0-cp310-cp310-win32.whl", hash = "sha256:e092a5c4cc9ae6bc757bc1fee8894cd0275f0a0689c8f57555b00e198cdbcecc"}, - {file = "bitarray-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:453395201790f16b22092c25bed6ecf3bafee7f36db840d4cfc07cd9b6f9628f"}, - {file = "bitarray-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a1cb97bdc51e3db8def039607ca42d12c6e55fb134a8d211cca2fcf0a7b3a986"}, - {file = "bitarray-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:92d8130efa6bebd4903b70f6d98832aabbd78c1031bc64c7c9d4e39b9db3bfa6"}, - {file = "bitarray-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37a9559ab050618902f8b8055c5b0b78dfc2a7974656fddee26b033ec56945f7"}, - {file = "bitarray-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2c57a4e8f0d9eb5cef7c83c8a4fd8bf06c35d641fc3cb24bf9511adb54e2d8a"}, - {file = "bitarray-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d9bcaa1764d0d302050a31b0f6879ea20e3900b2e73b4ea647ab570950c2062b"}, - {file = "bitarray-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:769184a57abfdfc24c37bb5d0f3836ffe65c7c5cdbeec431766520cd246a27c6"}, - {file = "bitarray-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f247387bb58b8c626088be2fa4ca5869fb12638b45acd29fab21aee48529560"}, - {file = "bitarray-3.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1303604e4069699cef3e969f31d6c3e8e5a0bc637569d90b3647469eff91dc7b"}, - {file = "bitarray-3.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f238c2eb7a0a6a50537e5d182edb629901af94746854108483f0b11b0e3d1a78"}, - {file = "bitarray-3.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a96b9b63348d9e5cca70fc31b5d0f07a420a70ef89b376aa4148b87d24b39"}, - {file = "bitarray-3.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5255fb9e4e4879adea5fb61cbde347767d346e29822a51ca21816f28340f4cdb"}, - {file = "bitarray-3.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:68bab51a827abc6f037ea76bb374ec28804a47b51716f9eb3f7da3d754bc8431"}, - {file = "bitarray-3.1.0-cp311-cp311-win32.whl", hash = "sha256:7ff061c714ff62357ff3be23d96c73944ad9181b8b21d49c29ed82fb2fb0efa4"}, - {file = "bitarray-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:b2f8aed62de760be852e0dee0ad507e0fe17fc12f83b36cc0bfe4900e0a0e915"}, - {file = "bitarray-3.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4c298fefda9dfe2ef24aee8fe48acc3a1a063b266791a814330c6932248785c4"}, - {file = "bitarray-3.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9211fb3a109ba76cb8bd0f76645d256f8a46e733693de22d0d7857941603cee"}, - {file = "bitarray-3.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b6bfe0a618301f0afed9004b8230a413ad399b58fef50a744a623461b96d8e3"}, - {file = "bitarray-3.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2db21d733e06d0d5b86fd3c848a84b8930e71250542aa7602e2c048b96fce163"}, - {file = "bitarray-3.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3807c38b37a500583eafd4ae866d2a051834acd4e11cd8557af9cf5379fa7e21"}, - {file = "bitarray-3.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcd15f43e846bfdaac8e5420006eba2d6adce29b6336492cf9d651e4a313ea86"}, - {file = "bitarray-3.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc2266e15f02d3192d1c2d29a790a4eccb230e23de0de6136815b32f9cdd5b4d"}, - {file = "bitarray-3.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:54a6a8525bc66228ed9cc69d562979319ef151dba2ed302bf7a21c7e124c137b"}, - {file = "bitarray-3.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ffb6b3a6efdabe6a4f3042b16a68499711a7835e950847a5643f60a43de3335f"}, - {file = "bitarray-3.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:430302c3ca7ed0dbb629db87e24a3be082851d71777787d4c8ad424624363780"}, - {file = "bitarray-3.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d4277999453fa63e96ed8a31698c9ac64ee5f38bd5c63205d69d9b73780fa152"}, - {file = "bitarray-3.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8fab5053ff281954718dc56c44ee7d1462ccc7e296b84da504b72b01c6c0c41"}, - {file = "bitarray-3.1.0-cp312-cp312-win32.whl", hash = "sha256:598072dbe456cc57270c6f34fa6b9aafd56bb5291e0d14f8509c1522f6d77f32"}, - {file = "bitarray-3.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:4fed8491a77f9fa5303bf6a24a76fcafbf1b496bb22d72b213b673ef8ce6e201"}, - {file = "bitarray-3.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ea4a888e45fdabc89705837b10030a990ec4879d14fccc3b9450ab56ca9d7ec"}, - {file = "bitarray-3.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2695c23b9857105631e9d4f8ebbcc8a4e55c6c9f31bc80d9e5e018e377d650d8"}, - {file = "bitarray-3.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:577a1ffc6318cdcbf91e8492596004bb1572ce3c703aa42c16c8ac1188625b8d"}, - {file = "bitarray-3.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1221d5d1255a74397f938ff822d8df4b0cc416b74e5659d21c3fe5f3d06590a1"}, - {file = "bitarray-3.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91e9309912700ab085cf78d162ad0bf0d3de35e5e084868400672b4ce3befbb0"}, - {file = "bitarray-3.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8c319f77118bac6aded58a0d2f172ac1b06e00bfa5391033848864e39bfd771"}, - {file = "bitarray-3.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:08004c22af11a234a73a8edf744524fb67e89ab4c80791f647c522687ddcb4b9"}, - {file = "bitarray-3.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:afb4d8876fff11cced4377f055498fd3c179e6e2bf7165fd055dabc494005252"}, - {file = "bitarray-3.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7c228c78bd20f2a46a2f3f2e349292c604ae26210dad56afe4b64ea65f72db75"}, - {file = "bitarray-3.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:139115998ffcffd8b883e1f21ced6f1d96947c5ace51f9656ceccedea5765557"}, - {file = "bitarray-3.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7e3ade382c9a5d5d77635041dd2aaa4e59326c8f62e1fb33ce28ddaeaa6ae7b6"}, - {file = "bitarray-3.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5efdb6403d206068d434caf9deb7ab936732c06e5617a63e213c94ac750303f4"}, - {file = "bitarray-3.1.0-cp313-cp313-win32.whl", hash = "sha256:69721fef0427a7f56ed7eef16ff9ddec96c64cee4d7abb5e7511c9aedebafc0f"}, - {file = "bitarray-3.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:34d75765b5c558e7bffa247493412831c6edb5c40e639a6ac94999bc6f40a6c8"}, - {file = "bitarray-3.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:6b85d01d8cf30937d9f55d2029a0f7575e9b559d606e6fff17bb79c2830e0afb"}, - {file = "bitarray-3.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acdac94fa2853e74503248b4079a30f3c09ab993f1b2a393fd88071824bdb0de"}, - {file = "bitarray-3.1.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f851a69521240418fc1ab6b313ef5ef6533e451a46184077b1f5e410441c7d9a"}, - {file = "bitarray-3.1.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf6be47880210416d3cd2405fd9a5f28b8fd1896b77acf729024f64b4ecd2676"}, - {file = "bitarray-3.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf4d0759d431953324de2ab251b954123bd4dfa83c990370814a88306f1ffc53"}, - {file = "bitarray-3.1.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0e1e3836468b62c079a69dbbfcdb8f0abb8dc8540f5cfca48147a8178cc95ea"}, - {file = "bitarray-3.1.0-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:0b380164b3b2ff5aa833ee2cf00855645308f14694546ca02beca6b9069e3c75"}, - {file = "bitarray-3.1.0-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:de8ce30fb9e9d2fa063abf8f9443d7bdf77b5de52a5cffbfd007f6150e6bee96"}, - {file = "bitarray-3.1.0-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:72b0f7cbc5c0bf008fde06b7777474a942cc0aa9c743dafc583a1dc430a07122"}, - {file = "bitarray-3.1.0-cp36-cp36m-musllinux_1_2_s390x.whl", hash = "sha256:ccd384e435d9c540c7e54c9149abbf462691e8a6c9287448d44da9f22230e09b"}, - {file = "bitarray-3.1.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:37d8da058472b2fedd27903781d5ea27264cde11dcb81a3237724427de83f241"}, - {file = "bitarray-3.1.0-cp36-cp36m-win32.whl", hash = "sha256:f78e2a4fd88b6fa383f44ca866cf6b5e1c0370de7ace7a0b4af7b3f11d806558"}, - {file = "bitarray-3.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a394cfcd6d9f7fac71429df8e8c4b93b1f0db3ef7a06eb84d9f69d901b862ee4"}, - {file = "bitarray-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:756da43ac44b1cbf576a02b3e9710fa24f14823b38df95868ea8a3fea840c725"}, - {file = "bitarray-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:638026c90fb135f0014a47bc5e9d5ed8acff8bbc3283f05e811e9f84a1f32a0e"}, - {file = "bitarray-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f90b57ea9aa9c6159c4a9110e8f787194583ede08b6ff5a6ea36c17b8973b75"}, - {file = "bitarray-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:93cb9b8e2cda9006d489bfa8e7f4fe5680860b6ff45fa9081a6554a9e661fa97"}, - {file = "bitarray-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2225ce0383a2e6950865f178513d8939c89782e909cbcfe4dd7c4f572b1e5a1"}, - {file = "bitarray-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bf22a95774a3d04200605bef8f436867df3ec87a13033fc482cce7ce17f62644"}, - {file = "bitarray-3.1.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:226297bb0a9f6636c3b74cfa39a1feec7fbd26c6b59fd9d5b78c4066a87aa571"}, - {file = "bitarray-3.1.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f4c28344774fadb554489f3a8c7a9c75237a85996a8a6d5d91ffa781bdd7b1ff"}, - {file = "bitarray-3.1.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:ce05239a8422ca3ebb1f00cf3577a1d255f07f7dd4c2ab20e8e009da393b86f3"}, - {file = "bitarray-3.1.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:9ce0f80904644ddbdad42eb1eb3945c19bc274b8b0e0844c7f7b9effafd279d0"}, - {file = "bitarray-3.1.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:c467bf00939b162144862d167dd55a651e44c256805f25e44edbd547fc0b0922"}, - {file = "bitarray-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:90b714bc59a9dd11d1bba0d68dd395a3e51f275c8547e9b68af561763a61196e"}, - {file = "bitarray-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4cbbe82edbe2f93f2598042d208caa87969025537ddce34cf647e4da6c4f6af4"}, - {file = "bitarray-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:34dd123c69a7f8520c20b45351df1d169429b9e636425c3c8278fdde80151fe6"}, - {file = "bitarray-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:03ae745110d4e343d78f5c3836058d510222b17469b1eb64362cdba7cbdfa26a"}, - {file = "bitarray-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfef13ba01363e185f3c7ffd8dc84c109a2be7da4d035b289c8f8a94b02c8183"}, - {file = "bitarray-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:798a71af5e1cded785dabc2f03bf65cd021a7e0ea54d8874a49cbb64669694e2"}, - {file = "bitarray-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d5eb9d2389c76469c4f10d636a79bcd13142d863a5d07a962738ae55fa60dfa"}, - {file = "bitarray-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a0a9c6a9fe679cd48204c719329de0ce72dede6f8e862744adcaf85b1d0c4c6"}, - {file = "bitarray-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a97cb1a6cf3538ecda5ce537111f246f468919ca38ff6cfe188f99ee3353c8f2"}, - {file = "bitarray-3.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6d49b0748e9aa0b3bcec4074675405d6a1b19288fb0e8ad8bc6db95c66840747"}, - {file = "bitarray-3.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:b57959f7538b93cb058aa8c467cfd8b82c0af88a2168a4f382ff77e3a90274ee"}, - {file = "bitarray-3.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:752e2de8b0094d56a6e98516609f4d2b9a2027d1a9038de3f021a7d1d13d0599"}, - {file = "bitarray-3.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:5b90b46cc73fc2db871985ae93132c7945f264aea6000348a124ba84b48af980"}, - {file = "bitarray-3.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:491d5c5b34b00c971be98b36736e090c847b0e2904a1b097a32bb02226fb11ba"}, - {file = "bitarray-3.1.0-cp38-cp38-win32.whl", hash = "sha256:515d954219d9b81b6442ab270cb8142100f32eac06f00f0c588da6c1416c9a1f"}, - {file = "bitarray-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:40c3f234b515aa8cfce0adc1df2b9c3b45e4307f8b756c9f3069cbc3effb5acb"}, - {file = "bitarray-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6fdd73f8f563b1f0d106c09949bd90b1104464b5cd148c02b7dae4e3efdb1dcd"}, - {file = "bitarray-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0c609504ce985754867a91db78fb1d5110df589447233badb5a1d453fb2e1714"}, - {file = "bitarray-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff398a6850da105e5cf13e4297f1bd181d90c16a4db9ce20e0608e1edf9c81b0"}, - {file = "bitarray-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39f32b9c1b03f2c2cec002513ef91a9818cf85e8da94069e430d71256c0b858c"}, - {file = "bitarray-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9d6e940dfefe604ecbd77849b107319c73f51dfb24ae18c80710dad9bdb59244"}, - {file = "bitarray-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19c3a7f80b286bd4df0fcbddbe97fd8b48028c0497678ee1f0bab820978295bd"}, - {file = "bitarray-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47b6cbbecd24d3b943cef3e704dbf9b510dec310251e1c279d10105cb40a33d8"}, - {file = "bitarray-3.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ab0063f8264cae001fc24238bf8b90a424be25b952951b749b647fd615011428"}, - {file = "bitarray-3.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:86544ffdf8d0cb7271acb5c3a560f4c7850f4c11057cc9c9acdadbea125aa68a"}, - {file = "bitarray-3.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:56ce6219922715fa8f9c65a40a504d4a5e64504ed7c4f3fac64071117559ef33"}, - {file = "bitarray-3.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:56fac415c19689b327f22ca6c02ffa908009f21aab931230d292d5239de40094"}, - {file = "bitarray-3.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e47bee13f43dfc5c5c3661e70b350a5dd37f62f25e196c9a20f504ca02774ac6"}, - {file = "bitarray-3.1.0-cp39-cp39-win32.whl", hash = "sha256:df4ea0d3035298716550b20396d831bc5871efbba7f9cc8e84eabd0906c0163e"}, - {file = "bitarray-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:7c96ecf342bf6093523477cf1eacd958df5206564aef347d3d2d8d4541a1c6a0"}, - {file = "bitarray-3.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c0790bbbe08b141d6a93061e68d1954c86374ca4982e5586becdf7dc0d61e95c"}, - {file = "bitarray-3.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dde6268797d6e3f0639b1a5048eee5e42aa63fd00c4477741f9a5b1720f1aa8f"}, - {file = "bitarray-3.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea7791224248fe8ad6f5533877f94850b5636cf957f245a2f4854da21d0be766"}, - {file = "bitarray-3.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea7319e68ad1cb081dd7d1b2fe1242c12fe8f9ee3c78812db010b18e9753f7bb"}, - {file = "bitarray-3.1.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3aab1395cd4a23b1815f87120e59e1a66a5f3e92cbe42e4653b9fae1320e0eeb"}, - {file = "bitarray-3.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:729ede2a88c0b379080606f2dd6ff4b4fc1798683f1fb244d79e929b298784c4"}, - {file = "bitarray-3.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fe0998dfa82776c17c262b20c2ff735f310aed4c85dcc2614ed389815a6bb4ef"}, - {file = "bitarray-3.1.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19f7b08cabfeed0190fbfcf7aad2cd71bb6934f319bd9f195b138616ebc56edf"}, - {file = "bitarray-3.1.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ba0aede899cd78952977db93cf24f5845163efbe85e4220de95298976d3c54b"}, - {file = "bitarray-3.1.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6370b01839750fcb3aea8639344fb3898f73dac42abe344bc5e39669b93fe110"}, - {file = "bitarray-3.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:37901e3b417ae482858c29be7b963ef1e6cf0ac16f03f4852295aee1aa77ac18"}, - {file = "bitarray-3.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7eb06912a72f5728864808a68abc1e247bc24c9983a1f3e400f9954245715fac"}, - {file = "bitarray-3.1.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:b9fe9c40c13c0c796bee8d6416185c59f270eb9d22dfb7f6b1cadd0d8be3b4c8"}, - {file = "bitarray-3.1.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44b93406e302f1077c9b4c4b990a40a7afbebab66e4579dce2b06d7f55d8e7c7"}, - {file = "bitarray-3.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:984803aa15421f3bade7c05fd44ab6acdcebe16b646f3201f5ccf1a111ba7661"}, - {file = "bitarray-3.1.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d39a36b6b95889a4cbc1d3083053cebe3315c13ff9a8760c14a3fe9cd8a102b1"}, - {file = "bitarray-3.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:41282599c51e26bc491a3d1471a785a0158c529a2e99757a353a708a9df66655"}, - {file = "bitarray-3.1.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4653e4cab6001a97c6689a4bfe3517128a74656eaa5df22f6ce01fd9b264db30"}, - {file = "bitarray-3.1.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:8d6ef7906263c12a27361ec06e89180a01f8ac57ffa0e1cdf7953948505fea4e"}, - {file = "bitarray-3.1.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:868e0a4fc33333b9cfaa9a0c6b847ce64e7d2064162c2183608244a649487d0a"}, - {file = "bitarray-3.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c64123ae24065478a17d2c6ed08e7e9ea6131be054326a01c9b13f5e36ac1a95"}, - {file = "bitarray-3.1.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6dcceb74c288777e86dbf2497e0bfc444aecf1e710e8a030ea4dba13600ac066"}, - {file = "bitarray-3.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:244897669375d345fe67cd5c0701ec5ea81991a9193fa8cfa4bcb55277375eff"}, - {file = "bitarray-3.1.0.tar.gz", hash = "sha256:71757171a45eac58782861c49137ba3bed0da489155311857f69f4e9baf81fa4"}, + {file = "bitarray-3.8.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f5930731b736e3f9654029f3e9082bfb1721d81f04bff9e6eab8eb38b4dfed"}, + {file = "bitarray-3.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e835f33ab5aa297a9ce21b7813222c22ff1618b8f8c5e6f921e54b4ae8b8f43"}, + {file = "bitarray-3.8.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1061cb959efbe3b747c38d550d8d7f0794090a757dd552eae8cf614a5f8d76b6"}, + {file = "bitarray-3.8.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82a6574e98bdddfb7fdac4d41c1176e90e1fcaaed97fda39836a9e0d8b247ec3"}, + {file = "bitarray-3.8.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9a34663e05bf79ccb92e931e720fbd281e84007ed996d38754aadfbc33e71c24"}, + {file = "bitarray-3.8.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:819f93a1aa7e711ccbb083647a8995bbb0da8f741c8b691576ff1bf5b5018c51"}, + {file = "bitarray-3.8.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:07c20505dc8935b55d6de0bb1cc7e0e35de792d5f118d60b177dee53771a474f"}, + {file = "bitarray-3.8.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:874c6806c2c7b861da0f0e9eead173bb3b9b7a62fcfadc01be51c32d50d7f71c"}, + {file = "bitarray-3.8.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:4403e5b4da88ec195afe3eab5969b34358157d196e1c63e93328e64e632abbed"}, + {file = "bitarray-3.8.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8a23e06e87cfa2ba361040eae87479ac197502ba10533c0f2de03d3d93cce91b"}, + {file = "bitarray-3.8.2-cp310-cp310-win32.whl", hash = "sha256:e65b91b68aa072732d144fa11d86518324b8b27af7e2474bd7a50c88648dc5d4"}, + {file = "bitarray-3.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:156c6d964111e1c0029c5bb41148a73aa870ca10c03a03279b5597fa68ac6761"}, + {file = "bitarray-3.8.2-cp310-cp310-win_arm64.whl", hash = "sha256:1b7c6fd8755dda32bc83b171e0a0f625fea545bb6f8a70a7481244dc847b1c9e"}, + {file = "bitarray-3.8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7540de3e7609693b208020cb3cb28cb16395eb915dff742bdcdd9909d475bf3d"}, + {file = "bitarray-3.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c073cd936904e520990339745a2d561ceabc9daa1cefcaf9592196a3355eb1cd"}, + {file = "bitarray-3.8.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4c1c97c5712ad45c6c1427b70bb6524f40532e4a544ca2b7e0375ca61c09244"}, + {file = "bitarray-3.8.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7627bfa750a609f5df05c1da337984b8f3821927591aaf861ba70f38bc5f6da1"}, + {file = "bitarray-3.8.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ff06e0511682f117d0c24828f0ef1b4f2c3617d38984c7b3ce78d107bee016ab"}, + {file = "bitarray-3.8.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bcaeccab426b0a6e26c10bd8d8c21c15f81757320ad158a8c9e3e953ab81d223"}, + {file = "bitarray-3.8.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:385045390630f5f433c89caeed9bca9f5b40e3986ae2d7e829e93098c1a96b94"}, + {file = "bitarray-3.8.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:30541722bfa0f8213d8e621772bef538204fe9eeb4357f4261d404688c2281a5"}, + {file = "bitarray-3.8.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3daf8f1e040d48bf7ee664bd5c9df9d029c55780c671221d753f6f4fc769f10a"}, + {file = "bitarray-3.8.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c223cf53e4a458b05b9f78723d88d5a1221fa11fb00cd1a696ccd483dcae3f8c"}, + {file = "bitarray-3.8.2-cp311-cp311-win32.whl", hash = "sha256:d9367a5eb2a3dda6958a129ca939ce7dd1555a3b13967eb2e7c9dc8df2cdffa0"}, + {file = "bitarray-3.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:2d0af077831aff8f44d8befe6459544bea1cd8fbce6b5b2a30ae1cb086a50620"}, + {file = "bitarray-3.8.2-cp311-cp311-win_arm64.whl", hash = "sha256:a78778a0899c682537ac612b1a03ecd4ad30063c118825d0138d0f7518270e54"}, + {file = "bitarray-3.8.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d5dcca2b64bbfce46dc43d77a2973d0b949e2260d74e8bd4e9a766de3afd0e70"}, + {file = "bitarray-3.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c78dfbb8883133caeb11aa4ec375165ff1b456a28898cbe45536173369accb24"}, + {file = "bitarray-3.8.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c32189234e4206c3832f947ebdf1735926dea0dbe0e966effd62771884dedf63"}, + {file = "bitarray-3.8.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26490091d3ad8c039829b33ab1bc776941ce359ecdcf8beef3c1efc330fcf1a5"}, + {file = "bitarray-3.8.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8ad858bd35dbb554de248c277ba9052f31d8e153c133195ef40c198303725dc8"}, + {file = "bitarray-3.8.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58aeaf943929716b411a4ff24422c2b8bbf2c2d8ef3e23bbf08dc7d47c49e2ae"}, + {file = "bitarray-3.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b016d736e2b4aa8962962724b69893adce076622374cf4a275503049f5c7207"}, + {file = "bitarray-3.8.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6871b2b1680580e54fbf0196b3ab7b40a417b4d1fdb3ebda0debf3948e9b8604"}, + {file = "bitarray-3.8.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a4c6bb948d011bf18642e09a0a4d1dd067f0722db09d2d4b5d6cce292d71b448"}, + {file = "bitarray-3.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:78622f067a89360e8acf146be7878f62deafe687db40feb16dabfc808a20717c"}, + {file = "bitarray-3.8.2-cp312-cp312-win32.whl", hash = "sha256:75999de62a7c4686b901458d441bc3c6c03dade68d1dfbe808439e748d086ea3"}, + {file = "bitarray-3.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:3e44247fcf5dffa86031d5412b20278a953e4dcef4033012c93ebd9985d48fec"}, + {file = "bitarray-3.8.2-cp312-cp312-win_arm64.whl", hash = "sha256:f823fa67f074c0ede82014fd5c2020f301b88f351635f5ba7b802f53b5e0eade"}, + {file = "bitarray-3.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:71d7350c801eea43afb0a8679fd7475b0fd9868fd15352f0d3069f335b44af06"}, + {file = "bitarray-3.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa3be101ed71c4e4989899da744a926d1f55f5d5f7f93242c32f727f7c11350b"}, + {file = "bitarray-3.8.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f684eb138bae893a5d98c811d99ecd89fa4a1af4700b0e512b8e2b794c9cabd"}, + {file = "bitarray-3.8.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:94e7da622b723705caddd59ee681cee0355b444901cc6fb2bcdc24bafba85911"}, + {file = "bitarray-3.8.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3110786b00b28a756fd948c8d63e6ca3a74810b2d115582d85593d9d48035c49"}, + {file = "bitarray-3.8.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd40cf27e2b54b5e30d0ce1da4f59bc16dd7c8363a20786b6e9deeb0b8ebe8e0"}, + {file = "bitarray-3.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:24c6b97f27bd3868e28b201e1d777f5e168805862b7d9528099138bbb8c6a636"}, + {file = "bitarray-3.8.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:786dedb4b1ced22dfeaaa89902561616f7edfa91774702b1aac31df3a6073c88"}, + {file = "bitarray-3.8.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:01bf9ff247117533c11963a81f3529bc12283c600dd195cf3b28a97b095f5d1c"}, + {file = "bitarray-3.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cc7a76e77c158e793d7c1e0b6c2240374087ac690a8bcacc8f18c427e5d9e20c"}, + {file = "bitarray-3.8.2-cp313-cp313-win32.whl", hash = "sha256:db9add8dcc87154c0f011e0e1ce9b856e5948fbcf6faf44305aa140e525ec9a7"}, + {file = "bitarray-3.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:cf4926098970d2d1a14156c0fbddb47554124347db4acf3ba616064fb021cd1e"}, + {file = "bitarray-3.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:5c8281d0eb35e8685235e1d50f9b26156803dad398d0e7868ce9aae254c3777d"}, + {file = "bitarray-3.8.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cbe96e7384e36963a2cdf5bc4ac9d0a78ae0d87fc78c53159cd5ac08c661ff34"}, + {file = "bitarray-3.8.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8b4fcecbbd0969988cc115bee74119c767636e48606fad318361eb9fe40a13c6"}, + {file = "bitarray-3.8.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48798fc274e8a0329ca75185a0dc1e0a93ff627ea8f30c339bdf0a2ef26b1723"}, + {file = "bitarray-3.8.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4205ae045129e58e7b7a9abe929ea0b9a3c63fad39d760e6e3b90062b6e5aa5"}, + {file = "bitarray-3.8.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:49c16cdedbd3c4d6bf64aca7b370ea02456e9be030201e80c282d8df6af36d19"}, + {file = "bitarray-3.8.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5cad241cd0ceb79a0e4f76e86b36660c22d36c32efb364badcf7609ed5a9e5c"}, + {file = "bitarray-3.8.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3b44650aba323cb1c2285c310ffa6b1adfd5293acecd7f84aaa91afa27c802c"}, + {file = "bitarray-3.8.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5fc8fa50c6a89b1e75edcea4ae17787a0a9b424cdbaa03485e73a837262eca27"}, + {file = "bitarray-3.8.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5f246319a26221e36eaf3f6aed9cd98172f81e91740bbf5cdf31b4490ecfb87a"}, + {file = "bitarray-3.8.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e76c01ae0f191c5572c12b1fda333243bfe4d58ad1d601048f9e4928d94db0c5"}, + {file = "bitarray-3.8.2-cp314-cp314-win32.whl", hash = "sha256:a1df20419ccc23a0326ee0cb391d1c524ee3c338856e66528d73f4dcec0389d0"}, + {file = "bitarray-3.8.2-cp314-cp314-win_amd64.whl", hash = "sha256:4bfbeba9156834455ab107936ebd461728f1ed35ded8f15aafde2c3dac9badf5"}, + {file = "bitarray-3.8.2-cp314-cp314-win_arm64.whl", hash = "sha256:4149aeb7c8cad12f9ea13783550ab5508e6d553eeefead5e3da659ce6724c5a5"}, + {file = "bitarray-3.8.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f6f6cf5ec3be7e1bd32bfe1f4b24f7d1de28d72394d7f58789b9f9042d19f5f6"}, + {file = "bitarray-3.8.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2b9790847024cf1de275c8b2495331fe0982d099e407be1c1413ed40ddf2b5d"}, + {file = "bitarray-3.8.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c172161b8847f91e9f9ea9ae2e31fcfa784ec5d0cd413900c82574999e21ad05"}, + {file = "bitarray-3.8.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5fcc57961bc78885091a45b9ced5a5924b3b1fdd439a0e1d4b7e3aedf0c31ae2"}, + {file = "bitarray-3.8.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b5fdb5399f0f2c42abcca87f8167d7ad746cf6ca7decadb4f5ad432280cc3a2f"}, + {file = "bitarray-3.8.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8bb6195a2edafceee0e9ee12c13aad2162e9578d91a24e7c501c3bd4ab90511a"}, + {file = "bitarray-3.8.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ce3b3dd599d4eca214f9c7fb7ac2343ccab41d91f3da7aa3b75ddbbea49ec2d5"}, + {file = "bitarray-3.8.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:11cd9766ce95199bef5010ff63f73c880d9c0b6ba9c4c233aeeebc11ab1dfbb3"}, + {file = "bitarray-3.8.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:7a4bbbad17d3db92615497302b74cf77504f821eb9585b7948d92093017d5e70"}, + {file = "bitarray-3.8.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e715498f3dc9af954b9d0977470aa352cb3fe1c39e80c32f5ac4c0348e461f6d"}, + {file = "bitarray-3.8.2-cp314-cp314t-win32.whl", hash = "sha256:c85569fb99cf9d4aa964d2dbba3c095c7580b4368f63f51252e85b939fcd0a2c"}, + {file = "bitarray-3.8.2-cp314-cp314t-win_amd64.whl", hash = "sha256:7de416b313fc8e8aa1e323b83d2ba86b7c84161f7ebbaf986bdab80f9d06a2fb"}, + {file = "bitarray-3.8.2-cp314-cp314t-win_arm64.whl", hash = "sha256:7199451493d34a5c62cb7c9077fcfd238499af4e0d13a32d33760afe73054135"}, + {file = "bitarray-3.8.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b270e16ef21913f4014ff996c12373e192d5063807874c51ed3b0b15e307f3eb"}, + {file = "bitarray-3.8.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a214365b3d417f741b65249254b55a79a65738ac0785029033f89772b38aa0df"}, + {file = "bitarray-3.8.2-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd92b23877bf64f99fabe3a7451126867e32325064e7ba3c8d0e8ba0e1589212"}, + {file = "bitarray-3.8.2-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:61e6ae0b5337a89538e567c7a9e512e304eef4df8b0699abf878fd15db7a7df4"}, + {file = "bitarray-3.8.2-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bfa7d3c6a60d7f69620a6a362fc1a44096591770fce58b0461c0d043c17f1971"}, + {file = "bitarray-3.8.2-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:823d53d82f9fa1425f52e7abb3d46370e206efddeb256829c3aa0ea409b64dc2"}, + {file = "bitarray-3.8.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:c54e30c6f253e974b07aa1ea4654f9d8af204d1ff68cd4e40a85a6fe43b3e2e0"}, + {file = "bitarray-3.8.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:0ecd1b2a27b790c4feb0ce3cb099d7a2c63de6ba12c87b5cfeec6e2940bd7450"}, + {file = "bitarray-3.8.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:a55a70b90707b6cfb159b972a0231c1fdafbb078f31e86687feb0b1821c4326c"}, + {file = "bitarray-3.8.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6b9c0e8620ed46c13d8f09b0867df5c2ae22f652449c9126384871b42de9ac8e"}, + {file = "bitarray-3.8.2-cp38-cp38-win32.whl", hash = "sha256:0e479c2f41ccec9d66d60b371cb6d66cda456ccf1c02db538cba9de8a67d992d"}, + {file = "bitarray-3.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:d3a0e7b4b03923acecba4326e4f9937d8c34b2c20b0fbe06da0ac6ca3b1ed1f4"}, + {file = "bitarray-3.8.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:85c70e58aa7dda3b3ec2b52fcbb534c70c39289613033642424c433288e7714d"}, + {file = "bitarray-3.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b48e288844902d677345e883adfe37d91777a270d7c13a67e05b6cee4de49dc8"}, + {file = "bitarray-3.8.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e8ccdebb2691d3052793f74fa8efae8e75f33893dd3afe5fa49b1d3caf2e0a"}, + {file = "bitarray-3.8.2-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e85895bd31d63a2befcb4c6278fa56865df510f85bb418d5f7e83bd7478864d2"}, + {file = "bitarray-3.8.2-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cac91d0db05c56c948423b4bf32714f1c73736717e2e476e98212f11fed52548"}, + {file = "bitarray-3.8.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef867a8b356127487ea2ba1e44e77f3fd715a19f873028f0541acb960428a247"}, + {file = "bitarray-3.8.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3dde04cee5af75f61693c518a77af98d901b8ff0f732cbb51c939296fbe8602e"}, + {file = "bitarray-3.8.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:240b21dc95e77b0d32dff48f9f8f607016732a392276a2c5a2ae49c264cf17b8"}, + {file = "bitarray-3.8.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1575ea328e55e595d9445a075815d94e2b700a6d92b8821b7bdf5db24d5b5e7d"}, + {file = "bitarray-3.8.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8585448eed6be4f2b5b723fe0a8ec1c319f4e627b252f49ed1f9479d349c3093"}, + {file = "bitarray-3.8.2-cp39-cp39-win32.whl", hash = "sha256:458c94bdc5ce3e077108461e35799fa62d4ed4e2c5621b2be765421181c8866d"}, + {file = "bitarray-3.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:b48f08a6baba869db9cc5f579e8db9157b10d2cd77cd4429e93fa6ce55cce97b"}, + {file = "bitarray-3.8.2-cp39-cp39-win_arm64.whl", hash = "sha256:8815984306b2d218edba548babf52fd711a01629f1be881da8c34e66e08369a5"}, + {file = "bitarray-3.8.2.tar.gz", hash = "sha256:2675a0c17c0b2d12d0fbcf3b27eb833f96936a588da47ac445c0743c5aa69e6b"}, ] [[package]] name = "boto3" -version = "1.34.30" +version = "1.43.44" description = "The AWS SDK for Python" optional = false -python-versions = ">= 3.8" +python-versions = ">=3.10" files = [ - {file = "boto3-1.34.30-py3-none-any.whl", hash = "sha256:cd6173380768faaecf6236dbdcec15d8d032cbb162ce354fdb111056a74fc298"}, - {file = "boto3-1.34.30.tar.gz", hash = "sha256:9e1476ce2b26437881a0381bf2daa54de619ac74ab4bd74278668acda6004a64"}, + {file = "boto3-1.43.44-py3-none-any.whl", hash = "sha256:621113f7850caec7c8405a07baa26075f700f78844a7f2fdf4d1f879bd3cc3c1"}, + {file = "boto3-1.43.44.tar.gz", hash = "sha256:035d73afe3e29bf271a5b27b30476ff8eefa5ae36f6702eada8e412d5c1420aa"}, ] [package.dependencies] -botocore = ">=1.34.30,<1.35.0" +botocore = ">=1.43.44,<1.44.0" jmespath = ">=0.7.1,<2.0.0" -s3transfer = ">=0.10.0,<0.11.0" +s3transfer = ">=0.19.0,<0.20.0" [package.extras] crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.34.30" +version = "1.43.44" description = "Low-level, data-driven core of boto 3." optional = false -python-versions = ">= 3.8" +python-versions = ">=3.10" files = [ - {file = "botocore-1.34.30-py3-none-any.whl", hash = "sha256:caf82d91c2ff61235284a07ffdfba006873e0752e00896052f901a37720cefa4"}, - {file = "botocore-1.34.30.tar.gz", hash = "sha256:e071a9766e7fc2221ca42ec01dfc54368a7518610787342ea622f6edc57f7891"}, + {file = "botocore-1.43.44-py3-none-any.whl", hash = "sha256:6afb846fd93815133a53baf1578f1d6016ddbeda247623360379758ca2e3f338"}, + {file = "botocore-1.43.44.tar.gz", hash = "sha256:cc2a95a53efdcf0ce34a51bca28058e72fcc3b10dd625eb1ad900c5ca3eb8bef"}, ] [package.dependencies] jmespath = ">=0.7.1,<2.0.0" python-dateutil = ">=2.1,<3.0.0" -urllib3 = {version = ">=1.25.4,<2.1", markers = "python_version >= \"3.10\""} +urllib3 = ">=1.25.4,<2.2.0 || >2.2.0,<3" [package.extras] -crt = ["awscrt (==0.19.19)"] +crt = ["awscrt (==0.32.2)"] [[package]] name = "cachelib" @@ -450,314 +460,325 @@ files = [ {file = "cachelib-0.13.0.tar.gz", hash = "sha256:209d8996e3c57595bee274ff97116d1d73c4980b2fd9a34c7846cd07fd2e1a48"}, ] -[[package]] -name = "cachetools" -version = "5.3.2" -description = "Extensible memoizing collections and decorators" -optional = false -python-versions = ">=3.7" -files = [ - {file = "cachetools-5.3.2-py3-none-any.whl", hash = "sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1"}, - {file = "cachetools-5.3.2.tar.gz", hash = "sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2"}, -] - [[package]] name = "certifi" -version = "2023.11.17" +version = "2026.6.17" description = "Python package for providing Mozilla's CA Bundle." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, - {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, + {file = "certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db"}, + {file = "certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432"}, ] [[package]] name = "cffi" -version = "1.16.0" +version = "2.1.0" description = "Foreign Function Interface for Python calling C code." optional = false -python-versions = ">=3.8" -files = [ - {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, - {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, - {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, - {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, - {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, - {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, - {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, - {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, - {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, - {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, - {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, - {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, - {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, - {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, - {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +python-versions = ">=3.10" +files = [ + {file = "cffi-2.1.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0"}, + {file = "cffi-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd"}, + {file = "cffi-2.1.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46"}, + {file = "cffi-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2"}, + {file = "cffi-2.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd"}, + {file = "cffi-2.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3"}, + {file = "cffi-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0"}, + {file = "cffi-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43"}, + {file = "cffi-2.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c"}, + {file = "cffi-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd"}, + {file = "cffi-2.1.0-cp310-cp310-win32.whl", hash = "sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f"}, + {file = "cffi-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da"}, + {file = "cffi-2.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc"}, + {file = "cffi-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7"}, + {file = "cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93"}, + {file = "cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2"}, + {file = "cffi-2.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c"}, + {file = "cffi-2.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f"}, + {file = "cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565"}, + {file = "cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c"}, + {file = "cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02"}, + {file = "cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e"}, + {file = "cffi-2.1.0-cp311-cp311-win32.whl", hash = "sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479"}, + {file = "cffi-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458"}, + {file = "cffi-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d"}, + {file = "cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f"}, + {file = "cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde"}, + {file = "cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d"}, + {file = "cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7"}, + {file = "cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b"}, + {file = "cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7"}, + {file = "cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66"}, + {file = "cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe"}, + {file = "cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b"}, + {file = "cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a"}, + {file = "cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384"}, + {file = "cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6"}, + {file = "cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda"}, + {file = "cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b"}, + {file = "cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a"}, + {file = "cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea"}, + {file = "cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db"}, + {file = "cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f"}, + {file = "cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d"}, + {file = "cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0"}, + {file = "cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224"}, + {file = "cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c"}, + {file = "cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a"}, + {file = "cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2"}, + {file = "cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512"}, + {file = "cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f"}, + {file = "cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a"}, + {file = "cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3"}, + {file = "cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d"}, + {file = "cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac"}, + {file = "cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6"}, + {file = "cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913"}, + {file = "cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d"}, + {file = "cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5"}, + {file = "cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce"}, + {file = "cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326"}, + {file = "cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd"}, + {file = "cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb"}, + {file = "cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804"}, + {file = "cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714"}, + {file = "cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376"}, + {file = "cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98"}, + {file = "cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13"}, + {file = "cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d"}, + {file = "cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056"}, + {file = "cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4"}, + {file = "cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94"}, + {file = "cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76"}, + {file = "cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5"}, + {file = "cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8"}, + {file = "cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c"}, + {file = "cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001"}, + {file = "cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3"}, + {file = "cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc"}, + {file = "cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699"}, + {file = "cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022"}, + {file = "cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0"}, + {file = "cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1"}, + {file = "cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28"}, + {file = "cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629"}, + {file = "cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6"}, + {file = "cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853"}, + {file = "cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda"}, + {file = "cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc"}, + {file = "cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca"}, + {file = "cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d"}, + {file = "cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8"}, + {file = "cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd"}, + {file = "cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f"}, + {file = "cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc"}, + {file = "cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9"}, + {file = "cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b"}, + {file = "cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5"}, + {file = "cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210"}, + {file = "cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9"}, ] [package.dependencies] -pycparser = "*" +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} [[package]] name = "cfgv" -version = "3.4.0" +version = "3.5.0" description = "Validate configuration and produce human readable error messages." optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" files = [ - {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, - {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, + {file = "cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0"}, + {file = "cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132"}, ] [[package]] name = "charset-normalizer" -version = "3.3.2" +version = "3.4.9" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false -python-versions = ">=3.7.0" -files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +python-versions = ">=3.7" +files = [ + {file = "charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee"}, + {file = "charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1"}, + {file = "charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0"}, + {file = "charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115"}, + {file = "charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b"}, + {file = "charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177"}, + {file = "charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-win32.whl", hash = "sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-win_amd64.whl", hash = "sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b"}, + {file = "charset_normalizer-3.4.9-cp39-cp39-win_arm64.whl", hash = "sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe"}, + {file = "charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5"}, + {file = "charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b"}, ] [[package]] name = "ckzg" -version = "2.0.1" +version = "2.1.7" description = "Python bindings for C-KZG-4844" optional = false python-versions = "*" files = [ - {file = "ckzg-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b7f9ba6d215f8981c5545f952aac84875bd564a63da02fb22a3d1321662ecdc0"}, - {file = "ckzg-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8fdec3ff96399acba9baeef9e1b0b5258c08f73245780e6c69f7b73def5e8d0a"}, - {file = "ckzg-2.0.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1644369af9900a9f109d417d6760693edf134118f3100d0c68f56667de775b80"}, - {file = "ckzg-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0a2146f122d489ac7e67ae0c0743f8d0db1718e6aeed8f05717340594fe07dd"}, - {file = "ckzg-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:979841be50f2782b447762db38e9bc927ae251f6ca86c54a26561a52068ee779"}, - {file = "ckzg-2.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4516d86647ee4e8ea9470f4adf68fbebb6dc1bdedff7d9592c2504fe53145908"}, - {file = "ckzg-2.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:91866fc58a29b4829201efd9ffadfac3ffeca6359254a54a360ff6a189c34bf5"}, - {file = "ckzg-2.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed35508dac059b2c0a7994383bc7a92eaf35d0b9ce790016819e2619e0f4b8a9"}, - {file = "ckzg-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:449c4fe38017351eca362106420eeb2d28d50b7e54aa8668b3af29a8ab780132"}, - {file = "ckzg-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:260608a22e2f2cadcd31f4495832d45d6460438c38faba9761b92df885a99d88"}, - {file = "ckzg-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e1015f99c50215098751b07d7e459ba9a2790d3692ca81552eed29996128e90d"}, - {file = "ckzg-2.0.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dd350d97554c161dc5b8c7b32c2dc8e659632c374f60e2669fb3c9b5b294827"}, - {file = "ckzg-2.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eec7724fa8dc4ae95757efe4a87e7b2d4b880cb348c72ce7355fc0c4f64bc298"}, - {file = "ckzg-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3fa0f4398fa67fb71f0a2b34a652cc89e6e0e6af1340b0dc771db1a5f3e089c"}, - {file = "ckzg-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f865a0297aabeeb638187a46f7df445763360417b9df4dea60560d512c2cda09"}, - {file = "ckzg-2.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b6ec738350771dbf5974fb70cc8bbb20a4df784af770f7e655922adc08a2171"}, - {file = "ckzg-2.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9b4b669fc77edeb16adc182efc32b3737b36f741a2e33a170d40619e8b171a94"}, - {file = "ckzg-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:decb97f4a17c7338b2130dcc4b045df4cc0e7785ece872c764b554c7c73a99ff"}, - {file = "ckzg-2.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:285cf3121b8a8c5609c5b706314f68d2ba2784ab02c5bb7487c6ae1714ecb27f"}, - {file = "ckzg-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f927bc41c2551b0ef0056a649a7ebed29d9665680a10795f4cee5002c69ddb7"}, - {file = "ckzg-2.0.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fd9fb690c88919f30c9f3ab7cc46a7ecd734d5ff4c9ccea383c119b9b7cc4da"}, - {file = "ckzg-2.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fabc3bd41b306d1c7025d561c3281a007c2aca8ceaf998582dc3894904d9c73e"}, - {file = "ckzg-2.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eb50c53efdb9c34f762bd0c8006cf79bc92a9daf47aa6b541e496988484124f"}, - {file = "ckzg-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7960cc62f959403293fb53a3c2404778369ae7cefc6d7f202e5e00567cf98c4b"}, - {file = "ckzg-2.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d721bcd492294c70eca39da0b0a433c29b6a571dbac2f7084bab06334904af06"}, - {file = "ckzg-2.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dde2391d025b5033ef0eeacf62b11ecfe446aea25682b5f547a907766ad0a8cb"}, - {file = "ckzg-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fab8859d9420f6f7df4e094ee3639bc49d18c8dab0df81bee825e2363dd67a09"}, - {file = "ckzg-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9747d92883199d4f8f3a3d7018134745fddcf692dfe67115434e4b32609ea785"}, - {file = "ckzg-2.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b2cf58fb9e165da97f0ffe9f4a6efb73992645fac8e0fa223a6cc7ec486a434a"}, - {file = "ckzg-2.0.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d25d006899d76bb8c9d3e8b27981dd6b66a78f9826e33c1bf981af6577a69a19"}, - {file = "ckzg-2.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a04bf0b32f04f5ea5e4b8518e292d3321bc05596fde95f9c3b4f504e5e4bc780"}, - {file = "ckzg-2.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d0cf3dccd72376bff10e1833641cc9d642f34f60ca63972626d9dfcfdc8e77f"}, - {file = "ckzg-2.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:770809c7e93087470cc524724419b0f85590edb033c7c73ba94aef70b36ca18b"}, - {file = "ckzg-2.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e31b59b8124148d5e21f7e41b35532d7af98260c44a77c3917958adece84296d"}, - {file = "ckzg-2.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:174f0c356df644d6e349ce03b7284d83dbec859e11ca5d1b1b3bace8b8fbc65d"}, - {file = "ckzg-2.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:30e375cd45142e56b5dbfdec05ce4deb2368d7f7dedfc7408ba37d5639af05ff"}, - {file = "ckzg-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:abdee71958b214730a8341b16bdd413d0fab1b1a2504fbdb7b0ef2aeee9f9d22"}, - {file = "ckzg-2.0.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b4442667058db791325fe231f22e4fc7aaa3495d535d75af5595bc5f4f86036"}, - {file = "ckzg-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c3c9aa9d4477ad52f3561b717e776c1a8a442d9d8b06600c7d8a2857d1ecf05"}, - {file = "ckzg-2.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68e0a9cde35f11e80b4e560d22990f2f29dd200a95d3141acde137cb6c883f9a"}, - {file = "ckzg-2.0.1-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:4508a089e53330866d3360000d76483400eeab5f8057b8e1f3e344ce2cc0097b"}, - {file = "ckzg-2.0.1-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:828cecee16ec576dcf4386beac4eedfd058fd32ee90827f2282e7156a53600be"}, - {file = "ckzg-2.0.1-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:bd437ec1dfb4f5609979328b5f465a74307f45d46d24234868c67d44da96903b"}, - {file = "ckzg-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:70406b10acf68469ac62110047044a6c1a998f5d5fcd6e27cb3ec2d5760d0490"}, - {file = "ckzg-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2f53fba88febac17e82a96eb83dc38ecf4b28abcdd15c0246534c358bd3b26c4"}, - {file = "ckzg-2.0.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be8e0d5015e7755af4ddaab9ae1a4084f72c84b2cbb53628f4366aeed46cc380"}, - {file = "ckzg-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:261414121091042d29f28fc319d7c9a7f950f91f8bf54c010b581ee6a0499473"}, - {file = "ckzg-2.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:524e1e66edd2be2c38b660824aa7b5d4525b41b30ac029d80738a8eee491aeb5"}, - {file = "ckzg-2.0.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:4a12a1d8ef8f475d9f0af9a538e1674057e007806cb1204bb269ea00d9f8c1e5"}, - {file = "ckzg-2.0.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:4cc4bb5f62417a58065deeaf124e178cb1787ef3228e6032600d1e0a2775765b"}, - {file = "ckzg-2.0.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:e7b015f5615bcb82fa0d935481a209fc1dcd9308fb52fb1a7e5400108df67a94"}, - {file = "ckzg-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0518933ff3b9550f9dd60d833cdb74e8e97cc1cc58f0560b706916606dfd47d0"}, - {file = "ckzg-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ac0bca0795990076cde1930ecec307379b5303e34367c6e6e8a16bdba5a7ba5"}, - {file = "ckzg-2.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8086d23a41020ede312843bda7ea4ee0c9831265379027904106f99f2f8ed469"}, - {file = "ckzg-2.0.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31d1b141d41fa51aeac9440c936b812e885aef5719adfbd3a27550d8dc433997"}, - {file = "ckzg-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60a58e4d8cb91bad669ca111b7ccdd05c32de6787fdb571bb599625b043ad75b"}, - {file = "ckzg-2.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:633e143385622d7a43fcb5c4f400ec5ec15df0b1c74ab7d6449a41a7abed24ad"}, - {file = "ckzg-2.0.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:4876313614ea01f9a0039b5ca2c754340ba40aa8405f8756912d90ae55718011"}, - {file = "ckzg-2.0.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:19c86c8102200484074afac06b3946b457ba9915636de187f63854522be2e3bd"}, - {file = "ckzg-2.0.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:564abf27878f129781e1df4d33b1c4e264e5b25f89c1bdf95b7d6256e4bceb6c"}, - {file = "ckzg-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:bc2da29bb970d3f5de04fb60797dbb4490c010ffc683cbc6016349dd6fa60d14"}, - {file = "ckzg-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9c1869671140ae7e698520b678b594ebd26fb59ef476711403541597d7d32c01"}, - {file = "ckzg-2.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1dd2aec2c61e8cc2ec815900f6768c6fe74b8fd29810e79b57c4150c6db32fb6"}, - {file = "ckzg-2.0.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9632ef17285dbdd3fcd9780f599c266da736d9b2897decc4ea02ba8690bdf72"}, - {file = "ckzg-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5747d7926873e3af0f6af5fca666feb0097d06cab525950e2664a6fbcb90165d"}, - {file = "ckzg-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75484ffb78aaebaeb3a30f1194a9143b904312b0f365fc4101e58e1bf5f89f66"}, - {file = "ckzg-2.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b2f72bc861b8bee9bac3314c58586d1ab2d23530f932a8f0a8562c8a4a6a45f9"}, - {file = "ckzg-2.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6f85e5802fea5b77f52fc3a14c8dec18a3f2b7c7070c811a4608940834f563cc"}, - {file = "ckzg-2.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:583a0b6b531a16974676439b23e7defb3dfe9732f18d13d2316152019c538af1"}, - {file = "ckzg-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:fafb9ac36b3398f8091d40773d9a450e5f74883dad8ca4ee22d472e7a231ef4d"}, - {file = "ckzg-2.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a12e96f20dce35e5222f898a5c8355054ef7c5ee038eeb97dbb694640b57577b"}, - {file = "ckzg-2.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:4e0ebc55253addaa24dd2cd871bbe3b8f57855f32b5f74e70bf2cb76b6f7da54"}, - {file = "ckzg-2.0.1-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f917a7bf363a3735db30559e1ed63cf1ccf414234433ba687fa72c007abd756"}, - {file = "ckzg-2.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30f08c984286853271d4adae219e9ba87275a15047dbaa262ab8dd6c01be97b0"}, - {file = "ckzg-2.0.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fa1ea4888417e1f109fd5e57965788fb7f53b674329b937a65604a3c1ca1d03"}, - {file = "ckzg-2.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0b249914aeaf05cabc71c5c3797e3d6c126cb2c64192b7eb6755ef6aa5ab2f11"}, - {file = "ckzg-2.0.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a038e26baf650e1c733dcaa066ec948e75556b0c485e8c790c9a758875c71a93"}, - {file = "ckzg-2.0.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d6deb2c822122bdd32b555fa3b9216c86a355f24a2cc6a46b9b5743b412b60c"}, - {file = "ckzg-2.0.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50f6f2fbceba9ece3fbc1d2613a246f4e6ec4d787f542859e70c358928c0e4a1"}, - {file = "ckzg-2.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33ca40ef30129e2347bff3c95ad093403a0d5703476705ab92c92fbffe89bd5a"}, - {file = "ckzg-2.0.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:700b989c2f7089edc8fac6dfbd1b4677e85b966216ebedee8eb5e7894765c188"}, - {file = "ckzg-2.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f11933c007c3df02446a81957ac6e2488058b969e2eff5357c98ab569a0c7999"}, - {file = "ckzg-2.0.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:3dbc9580eccecbd485f22e48f6044c48cbe6d838a7b7514cce179c085c65a960"}, - {file = "ckzg-2.0.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad6eb83f343fea6dd9a13fd1bce87b9cd26abeeb72f0674a62d26e40fe0b8aca"}, - {file = "ckzg-2.0.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:269f82b992facbd20461310cf5784551c77d11017b7d4b85d741d70359be6794"}, - {file = "ckzg-2.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:895d67cfd43130652e1ae39b90465b392d9a72c7c7e6f250eaf14689bfda6351"}, - {file = "ckzg-2.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:369cf1aeaf336c31f2050a7f54ae21cf46f4b2db23ebb013fff621144ab361bb"}, - {file = "ckzg-2.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:24fda2637598a467e7b11ff664805ee7fdf4f6c7b0c043d6d0a6ccb69b5681ee"}, - {file = "ckzg-2.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ea27baabe5b22b92901c428768eacf93b992ac7681f93768ab24818ad26ccfed"}, - {file = "ckzg-2.0.1-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a33f71e382020f2bc4ead2bd6881a9bd3811d929f272da239ac01ad615a00802"}, - {file = "ckzg-2.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:926507c569727bb4c851a1eea702c5e902267de96e06ce2d685019f973f72968"}, - {file = "ckzg-2.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f5f29518b0a4555d8f2a28559209bd1d4080547aa629ff9ee51799346573b3f"}, - {file = "ckzg-2.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4595db84ce63c227e4448de0f7b39d3043e3477d78394ff651708c37fee6c486"}, - {file = "ckzg-2.0.1.tar.gz", hash = "sha256:62c5adc381637affa7e1df465c57750b356a761b8a3164c3106589b02532b9c9"}, + {file = "ckzg-2.1.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:21fbb7f5689413994d224046c0c06cb8385fb8de33c5171b2c057151710cffed"}, + {file = "ckzg-2.1.7-cp310-cp310-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:83f56b03c54fd9a610aeefd9fd241bb2af960cb703f208c7806b37ccc9fb7fb8"}, + {file = "ckzg-2.1.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8bfa41d97ee31a2053d0b2f2a53793f67745bfa694f48b6d091ae499a04c272f"}, + {file = "ckzg-2.1.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:244acf422fb727dbc376a082f71d66f6f2787b570ec27d17d20c3c3b85aef6fb"}, + {file = "ckzg-2.1.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8705f73a7efe0f01b8ce67677320be99c7d7c7077311d255bbf2d4e55fdc6a9b"}, + {file = "ckzg-2.1.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c6b29572b2a4f678991a1edc2426f1802e9190eb763510cf1e9bafe797f004ba"}, + {file = "ckzg-2.1.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6ce04e32c1c459afae80edd32304956340a1dc5464a9f732f115f1119e3ec51d"}, + {file = "ckzg-2.1.7-cp310-cp310-win_amd64.whl", hash = "sha256:f537529bebfc58de21a6326100ad33e7d7ee98b0d49e44ee7f53d17ef899dfd5"}, + {file = "ckzg-2.1.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c9172f571ac7ec6d90207ad1903d921c38e48482bc028f723d6908720af1add6"}, + {file = "ckzg-2.1.7-cp311-cp311-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:c5494f39edeffedfa085fe85614a1c05ddd895ceb9d6c1800dc5355f9132a8f9"}, + {file = "ckzg-2.1.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb67250207b93d2df7f694bb74bd6b4a15fb2bb67d6a78977ae8ff431678c7e7"}, + {file = "ckzg-2.1.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7828cb549e2e8368e966c9dab87f3a51456647f1a3e79bdac9194e17bbc4d54"}, + {file = "ckzg-2.1.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23eacac20c6d3be2c87e592c11d02e4a1912e799d77e2559502455e85113e7b4"}, + {file = "ckzg-2.1.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4dd2afdc41f063e57eb569034b81088ba724240d3247ca78ea6591a1e04df50d"}, + {file = "ckzg-2.1.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b3af91c230982d59afe6f42c9c2a4c74412424a566bd09a42ffdfb451872335a"}, + {file = "ckzg-2.1.7-cp311-cp311-win_amd64.whl", hash = "sha256:f959a3bbc6d7aa7a653946e67dadaa78c0c79828aaa93b125a26f171a602b8fa"}, + {file = "ckzg-2.1.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:126050ffb23b504c34c4c2073c54bd8b42f4a3034798a631c9e85911e26caf47"}, + {file = "ckzg-2.1.7-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:936b4bffc1a6fa2bf261eb5e673f4fcc59feaf70c6c07aac1b02e3e1f942fdb6"}, + {file = "ckzg-2.1.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:902c03b689d13684cd8b61c8e1b7a65528fdd5e1ab9d76338ddb2e902b5fd1ea"}, + {file = "ckzg-2.1.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e635e5e1f6ff8ffc05d2961ccfc4b3e8c95e50c87d9765b2dfe09e32474c402"}, + {file = "ckzg-2.1.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cbedb5e4732d37c87fe45a2b25891d00f434d4e0f4dd612daa034fe2011e5939"}, + {file = "ckzg-2.1.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:665d0094466b576e390b4a5e1caf199f1165841e99bf7b3cc65117f12ba4ea74"}, + {file = "ckzg-2.1.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f5d4d1fb20eda15b901fc393a4bfd39b1be661008218f9f0db47d4e143d25d62"}, + {file = "ckzg-2.1.7-cp312-cp312-win_amd64.whl", hash = "sha256:b580f65e61f3d89a99bfeeac0e256cf68c63d29df1c1e5e788785085083a303b"}, + {file = "ckzg-2.1.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e23e10b227209bfae11f6f1f88ff2a8b0a2232248f985321e5e844c9dd7a4c5f"}, + {file = "ckzg-2.1.7-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:382c015860e7159b1ec5a85642127d4b55f6b36eef5f73d664fc409d26a3b367"}, + {file = "ckzg-2.1.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6666801e925d2f1d7c045fe943c1265c39b90444f88288735cc1245c4fa8018a"}, + {file = "ckzg-2.1.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e823de2fd4103abc4b51512d27aa3e14107e84718e11a596eefcddc6f313b25"}, + {file = "ckzg-2.1.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a65c7be0bb72a159c5a4b98cc3c759b868274697de11d8248f5dde32f2400776"}, + {file = "ckzg-2.1.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:62523b275f74f2729fc788d02b26e447dabfd7706ffe8882ee96d776db54b920"}, + {file = "ckzg-2.1.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5d998cd6d0f8e37e969c96315ac8c1e87fcf581cf27ab970bd33e62dc1c43357"}, + {file = "ckzg-2.1.7-cp313-cp313-win_amd64.whl", hash = "sha256:d48b75fca9e928b2ea288fc079b0522fb91af5742b5eb4f2fdea4fc33a1b7b4e"}, + {file = "ckzg-2.1.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c19b98f29f4459587e1ec4cce3e2e10963a6974293cf3143d13ce43c30542806"}, + {file = "ckzg-2.1.7-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:d31583a24cf8166d81c36f1e424de1f343c1d604dbc8c68d938a908236ae11a3"}, + {file = "ckzg-2.1.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:baf6ac696e6a40b33ddb57aa0729d5e39230bd13fa4f1e40fe9236e8920d83fe"}, + {file = "ckzg-2.1.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8bbdf89f9327e442415a810beca692729c35664e154a6830296124a5c6f05470"}, + {file = "ckzg-2.1.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:716c2dde0a91c0095797b843f78a6425e20a3d8945ecb4f90550b5c681b6be05"}, + {file = "ckzg-2.1.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2a9f1a05ed44512b80581e47918b1f4546974e8e924ee0e8de84ab32de197326"}, + {file = "ckzg-2.1.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:42005c188e37c2f65d44f3a2585e89de18e0e229bc667a600d8716808ea2c33b"}, + {file = "ckzg-2.1.7-cp314-cp314-win_amd64.whl", hash = "sha256:14fbc642b1e81893df76a1636fddc169173da5dcdb55fc08a030658cd186150e"}, + {file = "ckzg-2.1.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:da1a07e25ecaeb341ad4caf583fdec12c6af1ef3642289bb7dfcad2ca1b73dd3"}, + {file = "ckzg-2.1.7-cp314-cp314t-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:c657892f93eb70e3295b4f385e25380644c40f8bfebfcd55659f5017257c5b8c"}, + {file = "ckzg-2.1.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03af4cf053be82c22a893c8ef971d17687182dd2e75bcc2fab320bc27a62b7cb"}, + {file = "ckzg-2.1.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ecd9c44427a0035a8a9cb3dc18b4b3c72347f7be7c9f6866b8eddd6598bf0a9"}, + {file = "ckzg-2.1.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:16e313e6029e88a564724217dd8eddd6226fbf0a0c07bf65a210bf3512c7b8ad"}, + {file = "ckzg-2.1.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8461ec7d69ccb450d4a4d031494a86dc6c15ad54b671967d4a8bdcd8158155b2"}, + {file = "ckzg-2.1.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:53f420a3fa55a92265e23394caa2aac5b0e1e63ee6489d414cafeb0accde9a9e"}, + {file = "ckzg-2.1.7-cp314-cp314t-win_amd64.whl", hash = "sha256:2cdcc023d842900564d6070e397cab0d04fd393e6af07d60bdd1c97dc3ff09fd"}, + {file = "ckzg-2.1.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ab6ec526c6c727dd0f97f169f40c96124904db84718bb33965844e9952072eee"}, + {file = "ckzg-2.1.7-cp38-cp38-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:774abe2a20efd4c6050e6d80fbe382158aa3732349f4a8a74c18f41db53bfecf"}, + {file = "ckzg-2.1.7-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e5ee64fe3c67d894ea76e8df2be549ea82921c9f5a762ab03cc9be7b0f74be"}, + {file = "ckzg-2.1.7-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d79a024ffde956ee958d912542c96981308fe1948443d6a52bba5fa25a8c6368"}, + {file = "ckzg-2.1.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:aac001a1832f6c93c7ee656379b070230fa1f0111229b4e3e794b901caa0e6b6"}, + {file = "ckzg-2.1.7-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:0838dc176b405b1bf4ea3c098bb3e4e6affd135bbdb3ae13f78f499d23a0fc8c"}, + {file = "ckzg-2.1.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:9513b1779765dc1e7c47c45c3f63f02119685a91f689c7ff57173388a172bcbb"}, + {file = "ckzg-2.1.7-cp38-cp38-win_amd64.whl", hash = "sha256:55db86ada15ed542168e33dc0693bd1566258c4ca376bddef135e420c7f75b40"}, + {file = "ckzg-2.1.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6a8fe05d77f4f8373cb67929d9f2538bb19fa137de3e9170092ae20daab64ffe"}, + {file = "ckzg-2.1.7-cp39-cp39-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:8af45d2f296ed9aa21a128a2d605020e63a0ea4a642e32ffedfccf743aa51531"}, + {file = "ckzg-2.1.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8c2aaad1b4d5c1b7da0f1bab9840ee09f5dfe1c903547a276a79cac86f56390"}, + {file = "ckzg-2.1.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0584b6011fc8c9e4b09bc090b36e9a9c1f4917bd216e0a064d0135c809e6c0ee"}, + {file = "ckzg-2.1.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:76f332442680d30ab7d7659ae566a7e17adfbdda6ef8aa5bffff62f4dc584d03"}, + {file = "ckzg-2.1.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9e6cd0c5c73da94d6ee88a5396e1c1b65f87f03f5299f624d3f62ce361a0b9d3"}, + {file = "ckzg-2.1.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6398bc0632682e7ff3b0835bbd79032e161c32a312adb2baa8a9bebe78eebc46"}, + {file = "ckzg-2.1.7-cp39-cp39-win_amd64.whl", hash = "sha256:043e76201346987e6370b0c21bd08f93bbc8e26607d110c998c8faa6005be50f"}, + {file = "ckzg-2.1.7.tar.gz", hash = "sha256:a0c61c5fd573af0267bcb435ef0f499911289ceb05e863480779ea284a3bb928"}, ] [[package]] name = "click" -version = "8.1.7" +version = "8.4.2" description = "Composable command line interface toolkit" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, + {file = "click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76"}, + {file = "click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6"}, ] [package.dependencies] @@ -776,66 +797,79 @@ files = [ [[package]] name = "contourpy" -version = "1.2.0" +version = "1.3.2" description = "Python library for calculating contours of 2D quadrilateral grids" optional = false -python-versions = ">=3.9" -files = [ - {file = "contourpy-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0274c1cb63625972c0c007ab14dd9ba9e199c36ae1a231ce45d725cbcbfd10a8"}, - {file = "contourpy-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ab459a1cbbf18e8698399c595a01f6dcc5c138220ca3ea9e7e6126232d102bb4"}, - {file = "contourpy-1.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fdd887f17c2f4572ce548461e4f96396681212d858cae7bd52ba3310bc6f00f"}, - {file = "contourpy-1.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d16edfc3fc09968e09ddffada434b3bf989bf4911535e04eada58469873e28e"}, - {file = "contourpy-1.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c203f617abc0dde5792beb586f827021069fb6d403d7f4d5c2b543d87edceb9"}, - {file = "contourpy-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b69303ceb2e4d4f146bf82fda78891ef7bcd80c41bf16bfca3d0d7eb545448aa"}, - {file = "contourpy-1.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:884c3f9d42d7218304bc74a8a7693d172685c84bd7ab2bab1ee567b769696df9"}, - {file = "contourpy-1.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4a1b1208102be6e851f20066bf0e7a96b7d48a07c9b0cfe6d0d4545c2f6cadab"}, - {file = "contourpy-1.2.0-cp310-cp310-win32.whl", hash = "sha256:34b9071c040d6fe45d9826cbbe3727d20d83f1b6110d219b83eb0e2a01d79488"}, - {file = "contourpy-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:bd2f1ae63998da104f16a8b788f685e55d65760cd1929518fd94cd682bf03e41"}, - {file = "contourpy-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dd10c26b4eadae44783c45ad6655220426f971c61d9b239e6f7b16d5cdaaa727"}, - {file = "contourpy-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5c6b28956b7b232ae801406e529ad7b350d3f09a4fde958dfdf3c0520cdde0dd"}, - {file = "contourpy-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebeac59e9e1eb4b84940d076d9f9a6cec0064e241818bcb6e32124cc5c3e377a"}, - {file = "contourpy-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:139d8d2e1c1dd52d78682f505e980f592ba53c9f73bd6be102233e358b401063"}, - {file = "contourpy-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e9dc350fb4c58adc64df3e0703ab076f60aac06e67d48b3848c23647ae4310e"}, - {file = "contourpy-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18fc2b4ed8e4a8fe849d18dce4bd3c7ea637758c6343a1f2bae1e9bd4c9f4686"}, - {file = "contourpy-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:16a7380e943a6d52472096cb7ad5264ecee36ed60888e2a3d3814991a0107286"}, - {file = "contourpy-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8d8faf05be5ec8e02a4d86f616fc2a0322ff4a4ce26c0f09d9f7fb5330a35c95"}, - {file = "contourpy-1.2.0-cp311-cp311-win32.whl", hash = "sha256:67b7f17679fa62ec82b7e3e611c43a016b887bd64fb933b3ae8638583006c6d6"}, - {file = "contourpy-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:99ad97258985328b4f207a5e777c1b44a83bfe7cf1f87b99f9c11d4ee477c4de"}, - {file = "contourpy-1.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:575bcaf957a25d1194903a10bc9f316c136c19f24e0985a2b9b5608bdf5dbfe0"}, - {file = "contourpy-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9e6c93b5b2dbcedad20a2f18ec22cae47da0d705d454308063421a3b290d9ea4"}, - {file = "contourpy-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:464b423bc2a009088f19bdf1f232299e8b6917963e2b7e1d277da5041f33a779"}, - {file = "contourpy-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:68ce4788b7d93e47f84edd3f1f95acdcd142ae60bc0e5493bfd120683d2d4316"}, - {file = "contourpy-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d7d1f8871998cdff5d2ff6a087e5e1780139abe2838e85b0b46b7ae6cc25399"}, - {file = "contourpy-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e739530c662a8d6d42c37c2ed52a6f0932c2d4a3e8c1f90692ad0ce1274abe0"}, - {file = "contourpy-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:247b9d16535acaa766d03037d8e8fb20866d054d3c7fbf6fd1f993f11fc60ca0"}, - {file = "contourpy-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:461e3ae84cd90b30f8d533f07d87c00379644205b1d33a5ea03381edc4b69431"}, - {file = "contourpy-1.2.0-cp312-cp312-win32.whl", hash = "sha256:1c2559d6cffc94890b0529ea7eeecc20d6fadc1539273aa27faf503eb4656d8f"}, - {file = "contourpy-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:491b1917afdd8638a05b611a56d46587d5a632cabead889a5440f7c638bc6ed9"}, - {file = "contourpy-1.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5fd1810973a375ca0e097dee059c407913ba35723b111df75671a1976efa04bc"}, - {file = "contourpy-1.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:999c71939aad2780f003979b25ac5b8f2df651dac7b38fb8ce6c46ba5abe6ae9"}, - {file = "contourpy-1.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7caf9b241464c404613512d5594a6e2ff0cc9cb5615c9475cc1d9b514218ae8"}, - {file = "contourpy-1.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:266270c6f6608340f6c9836a0fb9b367be61dde0c9a9a18d5ece97774105ff3e"}, - {file = "contourpy-1.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbd50d0a0539ae2e96e537553aff6d02c10ed165ef40c65b0e27e744a0f10af8"}, - {file = "contourpy-1.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11f8d2554e52f459918f7b8e6aa20ec2a3bce35ce95c1f0ef4ba36fbda306df5"}, - {file = "contourpy-1.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ce96dd400486e80ac7d195b2d800b03e3e6a787e2a522bfb83755938465a819e"}, - {file = "contourpy-1.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6d3364b999c62f539cd403f8123ae426da946e142312a514162adb2addd8d808"}, - {file = "contourpy-1.2.0-cp39-cp39-win32.whl", hash = "sha256:1c88dfb9e0c77612febebb6ac69d44a8d81e3dc60f993215425b62c1161353f4"}, - {file = "contourpy-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:78e6ad33cf2e2e80c5dfaaa0beec3d61face0fb650557100ee36db808bfa6843"}, - {file = "contourpy-1.2.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:be16975d94c320432657ad2402f6760990cb640c161ae6da1363051805fa8108"}, - {file = "contourpy-1.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b95a225d4948b26a28c08307a60ac00fb8671b14f2047fc5476613252a129776"}, - {file = "contourpy-1.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0d7e03c0f9a4f90dc18d4e77e9ef4ec7b7bbb437f7f675be8e530d65ae6ef956"}, - {file = "contourpy-1.2.0.tar.gz", hash = "sha256:171f311cb758de7da13fc53af221ae47a5877be5a0843a9fe150818c51ed276a"}, +python-versions = ">=3.10" +files = [ + {file = "contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934"}, + {file = "contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989"}, + {file = "contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d"}, + {file = "contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9"}, + {file = "contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512"}, + {file = "contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631"}, + {file = "contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f"}, + {file = "contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2"}, + {file = "contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0"}, + {file = "contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a"}, + {file = "contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445"}, + {file = "contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773"}, + {file = "contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1"}, + {file = "contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43"}, + {file = "contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab"}, + {file = "contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7"}, + {file = "contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83"}, + {file = "contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd"}, + {file = "contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f"}, + {file = "contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878"}, + {file = "contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2"}, + {file = "contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15"}, + {file = "contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92"}, + {file = "contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87"}, + {file = "contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415"}, + {file = "contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe"}, + {file = "contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441"}, + {file = "contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e"}, + {file = "contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912"}, + {file = "contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73"}, + {file = "contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb"}, + {file = "contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08"}, + {file = "contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c"}, + {file = "contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f"}, + {file = "contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85"}, + {file = "contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841"}, + {file = "contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422"}, + {file = "contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef"}, + {file = "contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f"}, + {file = "contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9"}, + {file = "contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f"}, + {file = "contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739"}, + {file = "contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823"}, + {file = "contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5"}, + {file = "contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532"}, + {file = "contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b"}, + {file = "contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52"}, + {file = "contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd"}, + {file = "contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1"}, + {file = "contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69"}, + {file = "contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c"}, + {file = "contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16"}, + {file = "contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad"}, + {file = "contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0"}, + {file = "contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5"}, + {file = "contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5"}, + {file = "contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54"}, ] [package.dependencies] -numpy = ">=1.20,<2.0" +numpy = ">=1.23" [package.extras] bokeh = ["bokeh", "selenium"] docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] -mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.6.1)", "types-Pillow"] +mypy = ["bokeh", "contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.15.0)", "types-Pillow"] test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] -test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"] +test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist", "wurlitzer"] [[package]] name = "cryptography" @@ -888,13 +922,13 @@ test-randomorder = ["pytest-randomly"] [[package]] name = "cvat-sdk" -version = "2.37.0" -description = "CVAT REST API" +version = "2.69.0" +description = "Software Development Kit for CVAT" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "cvat_sdk-2.37.0-py3-none-any.whl", hash = "sha256:faa94cfd6678089814179a8da828761dfa3daf08eb752490ee85551a1045dac5"}, - {file = "cvat_sdk-2.37.0.tar.gz", hash = "sha256:e990908a473c499eb6d7b84f7f2e640ea729ef027d4c4cc32a5a925752532689"}, + {file = "cvat_sdk-2.69.0-py3-none-any.whl", hash = "sha256:f239d3cb609f45e4fc1d7cce1e49fe920d515be19058648fb79eba923ba0023e"}, + {file = "cvat_sdk-2.69.0.tar.gz", hash = "sha256:799ff1a763e1444a515edafcd162b7ab2fa89638211872bad139c63796a9dc7a"}, ] [package.dependencies] @@ -903,9 +937,7 @@ packaging = ">=21.3" Pillow = ">=10.3.0" platformdirs = ">=2.1.0" python_dateutil = ">=2.5.3" -setuptools = ">=21.0.0" tqdm = ">=4.64.0" -tuspy = "0.2.5" typing_extensions = ">=4.2.0" urllib3 = ">=1.25.3" @@ -930,118 +962,192 @@ tests = ["pytest", "pytest-cov", "pytest-xdist"] [[package]] name = "cytoolz" -version = "1.0.1" +version = "1.1.0" description = "Cython implementation of Toolz: High performance functional utilities" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "cytoolz-1.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cec9af61f71fc3853eb5dca3d42eb07d1f48a4599fa502cbe92adde85f74b042"}, - {file = "cytoolz-1.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:140bbd649dbda01e91add7642149a5987a7c3ccc251f2263de894b89f50b6608"}, - {file = "cytoolz-1.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e90124bdc42ff58b88cdea1d24a6bc5f776414a314cc4d94f25c88badb3a16d1"}, - {file = "cytoolz-1.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e74801b751e28f7c5cc3ad264c123954a051f546f2fdfe089f5aa7a12ccfa6da"}, - {file = "cytoolz-1.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:582dad4545ddfb5127494ef23f3fa4855f1673a35d50c66f7638e9fb49805089"}, - {file = "cytoolz-1.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd7bd0618e16efe03bd12f19c2a26a27e6e6b75d7105adb7be1cd2a53fa755d8"}, - {file = "cytoolz-1.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d74cca6acf1c4af58b2e4a89cc565ed61c5e201de2e434748c93e5a0f5c541a5"}, - {file = "cytoolz-1.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:823a3763828d8d457f542b2a45d75d6b4ced5e470b5c7cf2ed66a02f508ed442"}, - {file = "cytoolz-1.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:51633a14e6844c61db1d68c1ffd077cf949f5c99c60ed5f1e265b9e2966f1b52"}, - {file = "cytoolz-1.0.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f3ec9b01c45348f1d0d712507d54c2bfd69c62fbd7c9ef555c9d8298693c2432"}, - {file = "cytoolz-1.0.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1855022b712a9c7a5bce354517ab4727a38095f81e2d23d3eabaf1daeb6a3b3c"}, - {file = "cytoolz-1.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9930f7288c4866a1dc1cc87174f0c6ff4cad1671eb1f6306808aa6c445857d78"}, - {file = "cytoolz-1.0.1-cp310-cp310-win32.whl", hash = "sha256:a9baad795d72fadc3445ccd0f122abfdbdf94269157e6d6d4835636dad318804"}, - {file = "cytoolz-1.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:ad95b386a84e18e1f6136f6d343d2509d4c3aae9f5a536f3dc96808fcc56a8cf"}, - {file = "cytoolz-1.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2d958d4f04d9d7018e5c1850790d9d8e68b31c9a2deebca74b903706fdddd2b6"}, - {file = "cytoolz-1.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0f445b8b731fc0ecb1865b8e68a070084eb95d735d04f5b6c851db2daf3048ab"}, - {file = "cytoolz-1.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f546a96460a7e28eb2ec439f4664fa646c9b3e51c6ebad9a59d3922bbe65e30"}, - {file = "cytoolz-1.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0317681dd065532d21836f860b0563b199ee716f55d0c1f10de3ce7100c78a3b"}, - {file = "cytoolz-1.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c0ef52febd5a7821a3fd8d10f21d460d1a3d2992f724ba9c91fbd7a96745d41"}, - {file = "cytoolz-1.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5ebaf419acf2de73b643cf96108702b8aef8e825cf4f63209ceb078d5fbbbfd"}, - {file = "cytoolz-1.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f7f04eeb4088947585c92d6185a618b25ad4a0f8f66ea30c8db83cf94a425e3"}, - {file = "cytoolz-1.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f61928803bb501c17914b82d457c6f50fe838b173fb40d39c38d5961185bd6c7"}, - {file = "cytoolz-1.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d2960cb4fa01ccb985ad1280db41f90dc97a80b397af970a15d5a5de403c8c61"}, - {file = "cytoolz-1.0.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2b407cc3e9defa8df5eb46644f6f136586f70ba49eba96f43de67b9a0984fd3"}, - {file = "cytoolz-1.0.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8245f929144d4d3bd7b972c9593300195c6cea246b81b4c46053c48b3f044580"}, - {file = "cytoolz-1.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e37385db03af65763933befe89fa70faf25301effc3b0485fec1c15d4ce4f052"}, - {file = "cytoolz-1.0.1-cp311-cp311-win32.whl", hash = "sha256:50f9c530f83e3e574fc95c264c3350adde8145f4f8fc8099f65f00cc595e5ead"}, - {file = "cytoolz-1.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:b7f6b617454b4326af7bd3c7c49b0fc80767f134eb9fd6449917a058d17a0e3c"}, - {file = "cytoolz-1.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fcb8f7d0d65db1269022e7e0428471edee8c937bc288ebdcb72f13eaa67c2fe4"}, - {file = "cytoolz-1.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:207d4e4b445e087e65556196ff472ff134370d9a275d591724142e255f384662"}, - {file = "cytoolz-1.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21cdf6bac6fd843f3b20280a66fd8df20dea4c58eb7214a2cd8957ec176f0bb3"}, - {file = "cytoolz-1.0.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a55ec098036c0dea9f3bdc021f8acd9d105a945227d0811589f0573f21c9ce1"}, - {file = "cytoolz-1.0.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a13ab79ff4ce202e03ab646a2134696988b554b6dc4b71451e948403db1331d8"}, - {file = "cytoolz-1.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e2d944799026e1ff08a83241f1027a2d9276c41f7a74224cd98b7df6e03957d"}, - {file = "cytoolz-1.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88ba85834cd523b91fdf10325e1e6d71c798de36ea9bdc187ca7bd146420de6f"}, - {file = "cytoolz-1.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a750b1af7e8bf6727f588940b690d69e25dc47cce5ce467925a76561317eaf7"}, - {file = "cytoolz-1.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:44a71870f7eae31d263d08b87da7c2bf1176f78892ed8bdade2c2850478cb126"}, - {file = "cytoolz-1.0.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8231b9abbd8e368e036f4cc2e16902c9482d4cf9e02a6147ed0e9a3cd4a9ab0"}, - {file = "cytoolz-1.0.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:aa87599ccc755de5a096a4d6c34984de6cd9dc928a0c5eaa7607457317aeaf9b"}, - {file = "cytoolz-1.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:67cd16537df51baabde3baa770ab7b8d16839c4d21219d5b96ac59fb012ebd2d"}, - {file = "cytoolz-1.0.1-cp312-cp312-win32.whl", hash = "sha256:fb988c333f05ee30ad4693fe4da55d95ec0bb05775d2b60191236493ea2e01f9"}, - {file = "cytoolz-1.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:8f89c48d8e5aec55ffd566a8ec858706d70ed0c6a50228eca30986bfa5b4da8b"}, - {file = "cytoolz-1.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6944bb93b287032a4c5ca6879b69bcd07df46f3079cf8393958cf0b0454f50c0"}, - {file = "cytoolz-1.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e027260fd2fc5cb041277158ac294fc13dca640714527219f702fb459a59823a"}, - {file = "cytoolz-1.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88662c0e07250d26f5af9bc95911e6137e124a5c1ec2ce4a5d74de96718ab242"}, - {file = "cytoolz-1.0.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:309dffa78b0961b4c0cf55674b828fbbc793cf2d816277a5c8293c0c16155296"}, - {file = "cytoolz-1.0.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:edb34246e6eb40343c5860fc51b24937698e4fa1ee415917a73ad772a9a1746b"}, - {file = "cytoolz-1.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a54da7a8e4348a18d45d4d5bc84af6c716d7f131113a4f1cc45569d37edff1b"}, - {file = "cytoolz-1.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:241c679c3b1913c0f7259cf1d9639bed5084c86d0051641d537a0980548aa266"}, - {file = "cytoolz-1.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5bfc860251a8f280ac79696fc3343cfc3a7c30b94199e0240b6c9e5b6b01a2a5"}, - {file = "cytoolz-1.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c8edd1547014050c1bdad3ff85d25c82bd1c2a3c96830c6181521eb78b9a42b3"}, - {file = "cytoolz-1.0.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b349bf6162e8de215403d7f35f8a9b4b1853dc2a48e6e1a609a5b1a16868b296"}, - {file = "cytoolz-1.0.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1b18b35256219b6c3dd0fa037741b85d0bea39c552eab0775816e85a52834140"}, - {file = "cytoolz-1.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:738b2350f340ff8af883eb301054eb724997f795d20d90daec7911c389d61581"}, - {file = "cytoolz-1.0.1-cp313-cp313-win32.whl", hash = "sha256:9cbd9c103df54fcca42be55ef40e7baea624ac30ee0b8bf1149f21146d1078d9"}, - {file = "cytoolz-1.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:90e577e08d3a4308186d9e1ec06876d4756b1e8164b92971c69739ea17e15297"}, - {file = "cytoolz-1.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f3a509e4ac8e711703c368476b9bbce921fcef6ebb87fa3501525f7000e44185"}, - {file = "cytoolz-1.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a7eecab6373e933dfbf4fdc0601d8fd7614f8de76793912a103b5fccf98170cd"}, - {file = "cytoolz-1.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e55ed62087f6e3e30917b5f55350c3b6be6470b849c6566018419cd159d2cebc"}, - {file = "cytoolz-1.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43de33d99a4ccc07234cecd81f385456b55b0ea9c39c9eebf42f024c313728a5"}, - {file = "cytoolz-1.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:139bed875828e1727018aa0982aa140e055cbafccb7fd89faf45cbb4f2a21514"}, - {file = "cytoolz-1.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22c12671194b518aa8ce2f4422bd5064f25ab57f410ba0b78705d0a219f4a97a"}, - {file = "cytoolz-1.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79888f2f7dc25709cd5d37b032a8833741e6a3692c8823be181d542b5999128e"}, - {file = "cytoolz-1.0.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:51628b4eb41fa25bd428f8f7b5b74fbb05f3ae65fbd265019a0dd1ded4fdf12a"}, - {file = "cytoolz-1.0.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:1db9eb7179285403d2fb56ba1ff6ec35a44921b5e2fa5ca19d69f3f9f0285ea5"}, - {file = "cytoolz-1.0.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:08ab7efae08e55812340bfd1b3f09f63848fe291675e2105eab1aa5327d3a16e"}, - {file = "cytoolz-1.0.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e5fdc5264f884e7c0a1711a81dff112708a64b9c8561654ee578bfdccec6be09"}, - {file = "cytoolz-1.0.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:90d6a2e6ab891043ee655ec99d5e77455a9bee9e1131bdfcfb745edde81200dd"}, - {file = "cytoolz-1.0.1-cp38-cp38-win32.whl", hash = "sha256:08946e083faa5147751b34fbf78ab931f149ef758af5c1092932b459e18dcf5c"}, - {file = "cytoolz-1.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:a91b4e10a9c03796c0dc93e47ebe25bb41ecc6fafc3cf5197c603cf767a3d44d"}, - {file = "cytoolz-1.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:980c323e626ba298b77ae62871b2de7c50b9d7219e2ddf706f52dd34b8be7349"}, - {file = "cytoolz-1.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:45f6fa1b512bc2a0f2de5123db932df06c7f69d12874fe06d67772b2828e2c8b"}, - {file = "cytoolz-1.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f93f42d9100c415155ad1f71b0de362541afd4ac95e3153467c4c79972521b6b"}, - {file = "cytoolz-1.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a76d20dec9c090cdf4746255bbf06a762e8cc29b5c9c1d138c380bbdb3122ade"}, - {file = "cytoolz-1.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:239039585487c69aa50c5b78f6a422016297e9dea39755761202fb9f0530fe87"}, - {file = "cytoolz-1.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c28307640ca2ab57b9fbf0a834b9bf563958cd9e038378c3a559f45f13c3c541"}, - {file = "cytoolz-1.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:454880477bb901cee3a60f6324ec48c95d45acc7fecbaa9d49a5af737ded0595"}, - {file = "cytoolz-1.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:902115d1b1f360fd81e44def30ac309b8641661150fcbdde18ead446982ada6a"}, - {file = "cytoolz-1.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e68e6b38473a3a79cee431baa22be31cac39f7df1bf23eaa737eaff42e213883"}, - {file = "cytoolz-1.0.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:32fba3f63fcb76095b0a22f4bdcc22bc62a2bd2d28d58bf02fd21754c155a3ec"}, - {file = "cytoolz-1.0.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:0724ba4cf41eb40b6cf75250820ab069e44bdf4183ff78857aaf4f0061551075"}, - {file = "cytoolz-1.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c42420e0686f887040d5230420ed44f0e960ccbfa29a0d65a3acd9ca52459209"}, - {file = "cytoolz-1.0.1-cp39-cp39-win32.whl", hash = "sha256:4ba8b16358ea56b1fe8e637ec421e36580866f2e787910bac1cf0a6997424a34"}, - {file = "cytoolz-1.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:92d27f84bf44586853d9562bfa3610ecec000149d030f793b4cb614fd9da1813"}, - {file = "cytoolz-1.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:83d19d55738ad9c60763b94f3f6d3c6e4de979aeb8d76841c1401081e0e58d96"}, - {file = "cytoolz-1.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f112a71fad6ea824578e6393765ce5c054603afe1471a5c753ff6c67fd872d10"}, - {file = "cytoolz-1.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a515df8f8aa6e1eaaf397761a6e4aff2eef73b5f920aedf271416d5471ae5ee"}, - {file = "cytoolz-1.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92c398e7b7023460bea2edffe5fcd0a76029580f06c3f6938ac3d198b47156f3"}, - {file = "cytoolz-1.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3237e56211e03b13df47435b2369f5df281e02b04ad80a948ebd199b7bc10a47"}, - {file = "cytoolz-1.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ba0d1da50aab1909b165f615ba1125c8b01fcc30d606c42a61c42ea0269b5e2c"}, - {file = "cytoolz-1.0.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25b6e8dec29aa5a390092d193abd673e027d2c0b50774ae816a31454286c45c7"}, - {file = "cytoolz-1.0.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:36cd6989ebb2f18fe9af8f13e3c61064b9f741a40d83dc5afeb0322338ad25f2"}, - {file = "cytoolz-1.0.1-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a47394f8ab7fca3201f40de61fdeea20a2baffb101485ae14901ea89c3f6c95d"}, - {file = "cytoolz-1.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d00ac423542af944302e034e618fb055a0c4e87ba704cd6a79eacfa6ac83a3c9"}, - {file = "cytoolz-1.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a5ca923d1fa632f7a4fb33c0766c6fba7f87141a055c305c3e47e256fb99c413"}, - {file = "cytoolz-1.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:058bf996bcae9aad3acaeeb937d42e0c77c081081e67e24e9578a6a353cb7fb2"}, - {file = "cytoolz-1.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69e2a1f41a3dad94a17aef4a5cc003323359b9f0a9d63d4cc867cb5690a2551d"}, - {file = "cytoolz-1.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67daeeeadb012ec2b59d63cb29c4f2a2023b0c4957c3342d354b8bb44b209e9a"}, - {file = "cytoolz-1.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:54d3d36bbf0d4344d1afa22c58725d1668e30ff9de3a8f56b03db1a6da0acb11"}, - {file = "cytoolz-1.0.1.tar.gz", hash = "sha256:89cc3161b89e1bb3ed7636f74ed2e55984fd35516904fc878cae216e42b2c7d6"}, + {file = "cytoolz-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:72d7043a88ea5e61ba9d17ea0d1c1eff10f645d7edfcc4e56a31ef78be287644"}, + {file = "cytoolz-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d759e9ed421bacfeb456d47af8d734c057b9912b5f2441f95b27ca35e5efab07"}, + {file = "cytoolz-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fdb5be8fbcc0396141189022724155a4c1c93712ac4aef8c03829af0c2a816d7"}, + {file = "cytoolz-1.1.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c8c0a513dc89bc05cc72893609118815bced5ef201f1a317b4cc3423b3a0e750"}, + {file = "cytoolz-1.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce94db4f8ebe842c30c0ece42ff5de977c47859088c2c363dede5a68f6906484"}, + {file = "cytoolz-1.1.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b622d4f54e370c853ded94a668f94fe72c6d70e06ac102f17a2746661c27ab52"}, + {file = "cytoolz-1.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:375a65baa5a5b4ff6a0c5ff17e170cf23312e4c710755771ca966144c24216b5"}, + {file = "cytoolz-1.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c0d51bcdb3203a062a78f66bbe33db5e3123048e24a5f0e1402422d79df8ee2d"}, + {file = "cytoolz-1.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1010869529bb05dc9802b6d776a34ca1b6d48b9deec70ad5e2918ae175be5c2f"}, + {file = "cytoolz-1.1.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11a8f2e83295bdb33f35454d6bafcb7845b03b5881dcaed66ecbd726c7f16772"}, + {file = "cytoolz-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0499c5e0a8e688ed367a2e51cc13792ae8f08226c15f7d168589fc44b9b9cada"}, + {file = "cytoolz-1.1.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:87d44e6033d4c5e95a7d39ba59b8e105ba1c29b1ccd1d215f26477cc1d64be39"}, + {file = "cytoolz-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a68cef396a7de237f7b97422a6a450dfb111722296ba217ba5b34551832f1f6e"}, + {file = "cytoolz-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:06ad4c95b258141f138a93ebfdc1d76ac087afc1a82f1401100a1f44b44ba656"}, + {file = "cytoolz-1.1.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ada59a4b3c59d4ac7162e0ed08667ffa78abf48e975c8a9f9d5b9bc50720f4fd"}, + {file = "cytoolz-1.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a8957bcaea1ba01327a9b219d2adb84144377684f51444253890dab500ca171f"}, + {file = "cytoolz-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6d8cdc299d67eb0f3b9ecdafeeb55eb3b7b7470e2d950ac34b05ed4c7a5572b8"}, + {file = "cytoolz-1.1.0-cp310-cp310-win32.whl", hash = "sha256:d8e08464c5cdea4f6df31e84b11ed6bfd79cedb99fbcbfdc15eb9361a6053c5a"}, + {file = "cytoolz-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:7e49922a7ed54262d41960bf3b835a7700327bf79cff1e9bfc73d79021132ff8"}, + {file = "cytoolz-1.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:943a662d2e72ffc4438d43ab5a1de8d852237775a423236594a3b3e381b8032c"}, + {file = "cytoolz-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dba8e5a8c6e3c789d27b0eb5e7ce5ed7d032a7a9aae17ca4ba5147b871f6e327"}, + {file = "cytoolz-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:44b31c05addb0889167a720123b3b497b28dd86f8a0aeaf3ae4ffa11e2c85d55"}, + {file = "cytoolz-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:653cb18c4fc5d8a8cfce2bce650aabcbe82957cd0536827367d10810566d5294"}, + {file = "cytoolz-1.1.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:853a5b4806915020c890e1ce70cc056bbc1dd8bc44f2d74d555cccfd7aefba7d"}, + {file = "cytoolz-1.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7b44e9de86bea013fe84fd8c399d6016bbb96c37c5290769e5c99460b9c53e5"}, + {file = "cytoolz-1.1.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:098d628a801dc142e9740126be5624eb7aef1d732bc7a5719f60a2095547b485"}, + {file = "cytoolz-1.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:779ee4096ed7a82cffab89372ffc339631c285079dbf33dbe7aff1f6174985df"}, + {file = "cytoolz-1.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f2ce18dd99533d077e9712f9faa852f389f560351b1efd2f2bdb193a95eddde2"}, + {file = "cytoolz-1.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac266a34437812cf841cecbfe19f355ab9c3dd1ef231afc60415d40ff12a76e4"}, + {file = "cytoolz-1.1.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1920b9b9c13d60d0bb6cd14594b3bce0870022eccb430618c37156da5f2b7a55"}, + {file = "cytoolz-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47caa376dafd2bdc29f8a250acf59c810ec9105cd6f7680b9a9d070aae8490ec"}, + {file = "cytoolz-1.1.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5ab2c97d8aaa522b038cca9187b1153347af22309e7c998b14750c6fdec7b1cb"}, + {file = "cytoolz-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4bce006121b120e8b359244ee140bb0b1093908efc8b739db8dbaa3f8fb42139"}, + {file = "cytoolz-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7fc0f1e4e9bb384d26e73c6657bbc26abdae4ff66a95933c00f3d578be89181b"}, + {file = "cytoolz-1.1.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:dd3f894ff972da1994d06ac6157d74e40dda19eb31fe5e9b7863ca4278c3a167"}, + {file = "cytoolz-1.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0846f49cf8a4496bd42659040e68bd0484ce6af819709cae234938e039203ba0"}, + {file = "cytoolz-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:16a3af394ade1973226d64bb2f9eb3336adbdea03ed5b134c1bbec5a3b20028e"}, + {file = "cytoolz-1.1.0-cp311-cp311-win32.whl", hash = "sha256:b786c9c8aeab76cc2f76011e986f7321a23a56d985b77d14f155d5e5514ea781"}, + {file = "cytoolz-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:ebf06d1c5344fb22fee71bf664234733e55db72d74988f2ecb7294b05e4db30c"}, + {file = "cytoolz-1.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:b63f5f025fac893393b186e132e3e242de8ee7265d0cd3f5bdd4dda93f6616c9"}, + {file = "cytoolz-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:99f8e134c9be11649342853ec8c90837af4089fc8ff1e8f9a024a57d1fa08514"}, + {file = "cytoolz-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0a6f44cf9319c30feb9a50aa513d777ef51efec16f31c404409e7deb8063df64"}, + {file = "cytoolz-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:945580dc158c557172fca899a35a99a16fbcebf6db0c77cb6621084bc82189f9"}, + {file = "cytoolz-1.1.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:257905ec050d04f2f856854620d1e25556fd735064cebd81b460f54939b9f9d5"}, + {file = "cytoolz-1.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82779049f352fb3ab5e8c993ab45edbb6e02efb1f17f0b50f4972c706cc51d76"}, + {file = "cytoolz-1.1.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7d3e405e435320e08c5a1633afaf285a392e2d9cef35c925d91e2a31dfd7a688"}, + {file = "cytoolz-1.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:923df8f5591e0d20543060c29909c149ab1963a7267037b39eee03a83dbc50a8"}, + {file = "cytoolz-1.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:25db9e4862f22ea0ae2e56c8bec9fc9fd756b655ae13e8c7b5625d7ed1c582d4"}, + {file = "cytoolz-1.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7a98deb11ccd8e5d9f9441ef2ff3352aab52226a2b7d04756caaa53cd612363"}, + {file = "cytoolz-1.1.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dce4ee9fc99104bc77efdea80f32ca5a650cd653bcc8a1d984a931153d3d9b58"}, + {file = "cytoolz-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80d6da158f7d20c15819701bbda1c041f0944ede2f564f5c739b1bc80a9ffb8b"}, + {file = "cytoolz-1.1.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3b5c5a192abda123ad45ef716ec9082b4cf7d95e9ada8291c5c2cc5558be858b"}, + {file = "cytoolz-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5b399ce7d967b1cb6280250818b786be652aa8ddffd3c0bb5c48c6220d945ab5"}, + {file = "cytoolz-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e7e29a1a03f00b4322196cfe8e2c38da9a6c8d573566052c586df83aacc5663c"}, + {file = "cytoolz-1.1.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5291b117d71652a817ec164e7011f18e6a51f8a352cc9a70ed5b976c51102fda"}, + {file = "cytoolz-1.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8caef62f846a9011676c51bda9189ae394cdd6bb17f2946ecaedc23243268320"}, + {file = "cytoolz-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:de425c5a8e3be7bb3a195e19191d28d9eb3c2038046064a92edc4505033ec9cb"}, + {file = "cytoolz-1.1.0-cp312-cp312-win32.whl", hash = "sha256:296440a870e8d1f2e1d1edf98f60f1532b9d3ab8dfbd4b25ec08cd76311e79e5"}, + {file = "cytoolz-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:07156987f224c6dac59aa18fb8bf91e1412f5463961862716a3381bf429c8699"}, + {file = "cytoolz-1.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:23e616b38f5b3160c7bb45b0f84a8f3deb4bd26b29fb2dfc716f241c738e27b8"}, + {file = "cytoolz-1.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:76c9b58555300be6dde87a41faf1f97966d79b9a678b7a526fcff75d28ef4945"}, + {file = "cytoolz-1.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d1d638b10d3144795655e9395566ce35807df09219fd7cacd9e6acbdef67946a"}, + {file = "cytoolz-1.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:26801c1a165e84786a99e03c9c9973356caaca002d66727b761fb1042878ef06"}, + {file = "cytoolz-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2a9a464542912d3272f6dccc5142df057c71c6a5cbd30439389a732df401afb7"}, + {file = "cytoolz-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed6104fa942aa5784bf54f339563de637557e3443b105760bc4de8f16a7fc79b"}, + {file = "cytoolz-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56161f0ab60dc4159ec343509abaf809dc88e85c7e420e354442c62e3e7cbb77"}, + {file = "cytoolz-1.1.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:832bd36cc9123535f1945acf6921f8a2a15acc19cfe4065b1c9b985a28671886"}, + {file = "cytoolz-1.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1842636b6e034f229bf084c2bcdcfd36c8437e752eefd2c74ce9e2f10415cb6e"}, + {file = "cytoolz-1.1.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:823df012ab90d2f2a0f92fea453528539bf71ac1879e518524cd0c86aa6df7b9"}, + {file = "cytoolz-1.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f1fcf9e7e7b3487883ff3f815abc35b89dcc45c4cf81c72b7ee457aa72d197b"}, + {file = "cytoolz-1.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4cdb3fa1772116827f263f25b0cdd44c663b6701346a56411960534a06c082de"}, + {file = "cytoolz-1.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1b5c95041741b81430454db65183e133976f45ac3c03454cfa8147952568529"}, + {file = "cytoolz-1.1.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b2079fd9f1a65f4c61e6278c8a6d4f85edf30c606df8d5b32f1add88cbbe2286"}, + {file = "cytoolz-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a92a320d72bef1c7e2d4c6d875125cf57fc38be45feb3fac1bfa64ea401f54a4"}, + {file = "cytoolz-1.1.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06d1c79aa51e6a92a90b0e456ebce2288f03dd6a76c7f582bfaa3eda7692e8a5"}, + {file = "cytoolz-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e1d7be25f6971e986a52b6d3a0da28e1941850985417c35528f6823aef2cfec5"}, + {file = "cytoolz-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:964b248edc31efc50a65e9eaa0c845718503823439d2fa5f8d2c7e974c2b5409"}, + {file = "cytoolz-1.1.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c9ff2b3c57c79b65cb5be14a18c6fd4a06d5036fb3f33e973a9f70e9ac13ca28"}, + {file = "cytoolz-1.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:22290b73086af600042d99f5ce52a43d4ad9872c382610413176e19fc1d4fd2d"}, + {file = "cytoolz-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a2ade74fccd080ea793382968913ee38d7a35c921df435bbf0a6aeecf0d17574"}, + {file = "cytoolz-1.1.0-cp313-cp313-win32.whl", hash = "sha256:db5dbcfda1c00e937426cbf9bdc63c24ebbc358c3263bfcbc1ab4a88dc52aa8e"}, + {file = "cytoolz-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9e2d3fe3b45c3eb7233746f7aca37789be3dceec3e07dcc406d3e045ea0f7bdc"}, + {file = "cytoolz-1.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:32c559f95ff44a9ebcbd934acaa1e6dc8f3e6ffce4762a79a88528064873d6d5"}, + {file = "cytoolz-1.1.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9e2cd93b28f667c5870a070ab2b8bb4397470a85c4b204f2454b0ad001cd1ca3"}, + {file = "cytoolz-1.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f494124e141a9361f31d79875fe7ea459a3be2b9dadd90480427c0c52a0943d4"}, + {file = "cytoolz-1.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53a3262bf221f19437ed544bf8c0e1980c81ac8e2a53d87a9bc075dba943d36f"}, + {file = "cytoolz-1.1.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:47663e57d3f3f124921f38055e86a1022d0844c444ede2e8f090d3bbf80deb65"}, + {file = "cytoolz-1.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5a8755c4104ee4e3d5ba434c543b5f85fdee6a1f1df33d93f518294da793a60"}, + {file = "cytoolz-1.1.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4d96ff3d381423af1b105295f97de86d1db51732c9566eb37378bab6670c5010"}, + {file = "cytoolz-1.1.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0ec96b3d537cdf47d4e76ded199f7440715f4c71029b45445cff92c1248808c2"}, + {file = "cytoolz-1.1.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:208e2f2ef90a32b0acbff3303d90d89b13570a228d491d2e622a7883a3c68148"}, + {file = "cytoolz-1.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d416a81bb0bd517558668e49d30a7475b5445f9bbafaab7dcf066f1e9adba36"}, + {file = "cytoolz-1.1.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f32e94c91ffe49af04835ee713ebd8e005c85ebe83e7e1fdcc00f27164c2d636"}, + {file = "cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15d0c6405efc040499c46df44056a5c382f551a7624a41cf3e4c84a96b988a15"}, + {file = "cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:bf069c5381d757debae891401b88b3a346ba3a28ca45ba9251103b282463fad8"}, + {file = "cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d5cf15892e63411ec1bd67deff0e84317d974e6ab2cdfefdd4a7cea2989df66"}, + {file = "cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3e3872c21170f8341656f8692f8939e8800dcee6549ad2474d4c817bdefd62cd"}, + {file = "cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b9ddeff8e8fd65eb1fcefa61018100b2b627e759ea6ad275d2e2a93ffac147bf"}, + {file = "cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:02feeeda93e1fa3b33414eb57c2b0aefd1db8f558dd33fdfcce664a0f86056e4"}, + {file = "cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d08154ad45349162b6c37f12d5d1b2e6eef338e657b85e1621e4e6a4a69d64cb"}, + {file = "cytoolz-1.1.0-cp313-cp313t-win32.whl", hash = "sha256:10ae4718a056948d73ca3e1bb9ab1f95f897ec1e362f829b9d37cc29ab566c60"}, + {file = "cytoolz-1.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:1bb77bc6197e5cb19784b6a42bb0f8427e81737a630d9d7dda62ed31733f9e6c"}, + {file = "cytoolz-1.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:563dda652c6ff52d215704fbe6b491879b78d7bbbb3a9524ec8e763483cb459f"}, + {file = "cytoolz-1.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d542cee7c7882d2a914a33dec4d3600416fb336734df979473249d4c53d207a1"}, + {file = "cytoolz-1.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31922849b701b0f24bb62e56eb2488dcd3aa6ae3057694bd6b3b7c4c2bc27c2f"}, + {file = "cytoolz-1.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e68308d32afd31943314735c1335e4ab5696110e96b405f6bdb8f2a8dc771a16"}, + {file = "cytoolz-1.1.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fc4bb48b3b866e1867f7c6411a4229e5b44be3989060663713e10efc24c9bd5f"}, + {file = "cytoolz-1.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:456f77207d1445025d7ef262b8370a05492dcb1490cb428b0f3bf1bd744a89b0"}, + {file = "cytoolz-1.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:174ebc71ebb20a9baeffce6ee07ee2cd913754325c93f99d767380d8317930f7"}, + {file = "cytoolz-1.1.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8b3604fef602bcd53415055a4f68468339192fd17be39e687ae24f476d23d56e"}, + {file = "cytoolz-1.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3604b959a01f64c366e7d10ec7634d5f5cfe10301e27a8f090f6eb3b2a628a18"}, + {file = "cytoolz-1.1.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6db2127a3c1bc2f59f08010d2ae53a760771a9de2f67423ad8d400e9ba4276e8"}, + {file = "cytoolz-1.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56584745ac647993a016a21bc76399113b7595e312f8d0a1b140c9fcf9b58a27"}, + {file = "cytoolz-1.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db2c4c3a7f7bd7e03bb1a236a125c8feb86c75802f4ecda6ecfaf946610b2930"}, + {file = "cytoolz-1.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48cb8a692111a285d2b9acd16d185428176bfbffa8a7c274308525fccd01dd42"}, + {file = "cytoolz-1.1.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d2f344ba5eb17dcf38ee37fdde726f69053f54927db8f8a1bed6ac61e5b1890d"}, + {file = "cytoolz-1.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abf76b1c1abd031f098f293b6d90ee08bdaa45f8b5678430e331d991b82684b1"}, + {file = "cytoolz-1.1.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ddf9a38a5b686091265ff45b53d142e44a538cd6c2e70610d3bc6be094219032"}, + {file = "cytoolz-1.1.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:946786755274f07bb2be0400f28adb31d7d85a7c7001873c0a8e24a503428fb3"}, + {file = "cytoolz-1.1.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d5b8f78b9fed79cf185ad4ddec099abeef45951bdcb416c5835ba05f0a1242c7"}, + {file = "cytoolz-1.1.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fccde6efefdbc02e676ccb352a2ccc8a8e929f59a1c6d3d60bb78e923a49ca44"}, + {file = "cytoolz-1.1.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:717b7775313da5f51b0fbf50d865aa9c39cb241bd4cb605df3cf2246d6567397"}, + {file = "cytoolz-1.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5158744a09d0e0e4a4f82225e3a3c4ebf38f9ae74467aaa905467270e52f2794"}, + {file = "cytoolz-1.1.0-cp314-cp314-win32.whl", hash = "sha256:1ed534bdbbf063b2bb28fca7d0f6723a3e5a72b086e7c7fe6d74ae8c3e4d00e2"}, + {file = "cytoolz-1.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:472c1c9a085f5ad973ec0ad7f0b9ba0969faea6f96c9e397f6293d386f3a25ec"}, + {file = "cytoolz-1.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:a7ad7ca3386fa86bd301be3fa36e7f0acb024f412f665937955acfc8eb42deff"}, + {file = "cytoolz-1.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:64b63ed4b71b1ba813300ad0f06b8aff19a12cf51116e0e4f1ed837cea4debcf"}, + {file = "cytoolz-1.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a60ba6f2ed9eb0003a737e1ee1e9fa2258e749da6477946008d4324efa25149f"}, + {file = "cytoolz-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1aa58e2434d732241f7f051e6f17657e969a89971025e24578b5cbc6f1346485"}, + {file = "cytoolz-1.1.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6965af3fc7214645970e312deb9bd35a213a1eaabcfef4f39115e60bf2f76867"}, + {file = "cytoolz-1.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddd2863f321d67527d3b67a93000a378ad6f967056f68c06467fe011278a6d0e"}, + {file = "cytoolz-1.1.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4e6b428e9eb5126053c2ae0efa62512ff4b38ed3951f4d0888ca7005d63e56f5"}, + {file = "cytoolz-1.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d758e5ef311d2671e0ae8c214c52e44617cf1e58bef8f022b547b9802a5a7f30"}, + {file = "cytoolz-1.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a95416eca473e6c1179b48d86adcf528b59c63ce78f4cb9934f2e413afa9b56b"}, + {file = "cytoolz-1.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36c8ede93525cf11e2cc787b7156e5cecd7340193ef800b816a16f1404a8dc6d"}, + {file = "cytoolz-1.1.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c949755b6d8a649c5fbc888bc30915926f1b09fe42fea9f289e297c2f6ddd3"}, + {file = "cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e1b6d37545816905a76d9ed59fa4e332f929e879f062a39ea0f6f620405cdc27"}, + {file = "cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:05332112d4087904842b36954cd1d3fc0e463a2f4a7ef9477bd241427c593c3b"}, + {file = "cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:31538ca2fad2d688cbd962ccc3f1da847329e2258a52940f10a2ac0719e526be"}, + {file = "cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:747562aa70abf219ea16f07d50ac0157db856d447f7f498f592e097cbc77df0b"}, + {file = "cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:3dc15c48b20c0f467e15e341e102896c8422dccf8efc6322def5c1b02f074629"}, + {file = "cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3c03137ee6103ba92d5d6ad6a510e86fded69cd67050bd8a1843f15283be17ac"}, + {file = "cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be8e298d88f88bd172b59912240558be3b7a04959375646e7fd4996401452941"}, + {file = "cytoolz-1.1.0-cp314-cp314t-win32.whl", hash = "sha256:3d407140f5604a89578285d4aac7b18b8eafa055cf776e781aabb89c48738fad"}, + {file = "cytoolz-1.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:56e5afb69eb6e1b3ffc34716ee5f92ffbdb5cb003b3a5ca4d4b0fe700e217162"}, + {file = "cytoolz-1.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:27b19b4a286b3ff52040efa42dbe403730aebe5fdfd2def704eb285e2125c63e"}, + {file = "cytoolz-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:08a63935c66488511b7b29b06233be0be5f4123622fc8fd488f28dc1b7e4c164"}, + {file = "cytoolz-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:93bd0afcc4cc05794507084afaefb161c3639f283ee629bd0e8654b5c0327ba8"}, + {file = "cytoolz-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f3d4da470cfd5cf44f6d682c6eb01363066e0af53ebe111225e44a618f9453d"}, + {file = "cytoolz-1.1.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ba6c12d0e6a67399f4102b4980f4f1bebdbf226ed0a68e84617709d4009b4e71"}, + {file = "cytoolz-1.1.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b557071405b4aeeaa7cbec1a95d15d6c8f37622fe3f4b595311e0e226ce772c"}, + {file = "cytoolz-1.1.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cdb406001474726a47fbe903f3aba0de86f5c0b9c9861f55c09c366368225ae0"}, + {file = "cytoolz-1.1.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b6072876ba56446d9ac29d349983677d6f44c6d1c6c1c6be44e66e377c57c767"}, + {file = "cytoolz-1.1.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c3784c965c9a6822d315d099c3a85b0884ac648952815891c667b469116f1d0"}, + {file = "cytoolz-1.1.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cc537ad78981df1a827773069fd3b7774f4478db43f518b1616efaf87d7d8f9"}, + {file = "cytoolz-1.1.0-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:574ee9dfdc632db8bf9237f27f2a687d1a0b90d29d5e96cab2b21fd2b419c17d"}, + {file = "cytoolz-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6594efbaea72dc58b368b53e745ad902c8d8cc41286f00b3743ceac464d5ef3f"}, + {file = "cytoolz-1.1.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:7c849f9ddaf3c7faba938440f9c849235a2908b303063d49da3092a93acd695b"}, + {file = "cytoolz-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1fef0296fb3577d0a08ad9b70344ee418f728f1ec21a768ffe774437d67ac859"}, + {file = "cytoolz-1.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:1dce1e66fdf72cc474367bd7a7f2b90ec67bb8197dc3fe8ecd08f4ce3ab950a1"}, + {file = "cytoolz-1.1.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:202fe9975efaec0085cab14a6a6050418bc041f5316f2cf098c0cd2aced4c50e"}, + {file = "cytoolz-1.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:528349434601b9d55e65c6a495494de0001c9a06b431547fea4c60b5edc7d5b3"}, + {file = "cytoolz-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3e248cdbf2a54bafdadf4486ddd32e8352f816d3caa2014e44de99f8c525d4a8"}, + {file = "cytoolz-1.1.0-cp39-cp39-win32.whl", hash = "sha256:e63f2b70f4654648a5c6a176ae80897c0de6401f385540dce8e365019e800cfe"}, + {file = "cytoolz-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:f731c53ed29959f105ae622b62e39603c207ed8e8cb2a40cd4accb63d9f92901"}, + {file = "cytoolz-1.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:5a2120bf9e6e8f25e1b32748424a5571e319ef03a995a8fde663fd2feec1a696"}, + {file = "cytoolz-1.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f32e93a55681d782fc6af939f6df36509d65122423cbc930be39b141064adff8"}, + {file = "cytoolz-1.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5d9bc596751cbda8073e65be02ca11706f00029768fbbbc81e11a8c290bb41aa"}, + {file = "cytoolz-1.1.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b16660d01c3931951fab49db422c627897c38c1a1f0393a97582004019a4887"}, + {file = "cytoolz-1.1.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b7de5718e2113d4efccea3f06055758cdbc17388ecc3341ba4d1d812837d7c1a"}, + {file = "cytoolz-1.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a12a2a1a6bc44099491c05a12039efa08cc33a3d0f8c7b0566185e085e139283"}, + {file = "cytoolz-1.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:047defa7f5f9a32f82373dbc3957289562e8a3fa58ae02ec8e4dca4f43a33a21"}, + {file = "cytoolz-1.1.0.tar.gz", hash = "sha256:13a7bf254c3c0d28b12e2290b82aed0f0977a4c2a2bf84854fcdc7796a29f3b0"}, ] [package.dependencies] toolz = ">=0.8.0" [package.extras] -cython = ["cython"] +cython = ["cython (>=0.29)"] +test = ["pytest"] [[package]] name = "datumaro" @@ -1098,44 +1204,44 @@ files = [ [[package]] name = "distlib" -version = "0.4.0" +version = "0.4.3" description = "Distribution utilities" optional = false python-versions = "*" files = [ - {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, - {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, + {file = "distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b"}, + {file = "distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed"}, ] [[package]] name = "dnspython" -version = "2.7.0" +version = "2.8.0" description = "DNS toolkit" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86"}, - {file = "dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"}, + {file = "dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af"}, + {file = "dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f"}, ] [package.extras] -dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.16.0)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "quart-trio (>=0.11.0)", "sphinx (>=7.2.0)", "sphinx-rtd-theme (>=2.0.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] -dnssec = ["cryptography (>=43)"] -doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] -doq = ["aioquic (>=1.0.0)"] -idna = ["idna (>=3.7)"] -trio = ["trio (>=0.23)"] +dev = ["black (>=25.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.17.0)", "mypy (>=1.17)", "pylint (>=3)", "pytest (>=8.4)", "pytest-cov (>=6.2.0)", "quart-trio (>=0.12.0)", "sphinx (>=8.2.0)", "sphinx-rtd-theme (>=3.0.0)", "twine (>=6.1.0)", "wheel (>=0.45.0)"] +dnssec = ["cryptography (>=45)"] +doh = ["h2 (>=4.2.0)", "httpcore (>=1.0.0)", "httpx (>=0.28.0)"] +doq = ["aioquic (>=1.2.0)"] +idna = ["idna (>=3.10)"] +trio = ["trio (>=0.30)"] wmi = ["wmi (>=1.5.1)"] [[package]] name = "email-validator" -version = "2.2.0" +version = "2.3.0" description = "A robust email address syntax and deliverability validation library." optional = false python-versions = ">=3.8" files = [ - {file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"}, - {file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"}, + {file = "email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4"}, + {file = "email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426"}, ] [package.dependencies] @@ -1166,13 +1272,13 @@ tools = ["hypothesis (>=6.22.0,<6.108.7)"] [[package]] name = "eth-account" -version = "0.13.5" +version = "0.13.7" description = "eth-account: Sign Ethereum transactions and messages with local private keys" optional = false python-versions = "<4,>=3.8" files = [ - {file = "eth_account-0.13.5-py3-none-any.whl", hash = "sha256:e43fd30c9a7fabb882b50e8c4c41d4486d2f3478ad97c66bb18cfcc872fdbec8"}, - {file = "eth_account-0.13.5.tar.gz", hash = "sha256:010c9ce5f3d2688106cf9bfeb711bb8eaf0154ea6f85325f54fecea85c2b3759"}, + {file = "eth_account-0.13.7-py3-none-any.whl", hash = "sha256:39727de8c94d004ff61d10da7587509c04d2dc7eac71e04830135300bdfc6d24"}, + {file = "eth_account-0.13.7.tar.gz", hash = "sha256:5853ecbcbb22e65411176f121f5f24b8afeeaf13492359d254b16d8b18c77a46"}, ] [package.dependencies] @@ -1194,20 +1300,20 @@ test = ["coverage", "hypothesis (>=6.22.0,<6.108.7)", "pytest (>=7.0.0)", "pytes [[package]] name = "eth-hash" -version = "0.7.1" +version = "0.8.0" description = "eth-hash: The Ethereum hashing function, keccak256, sometimes (erroneously) called sha3" optional = false -python-versions = "<4,>=3.8" +python-versions = "<4,>=3.10" files = [ - {file = "eth_hash-0.7.1-py3-none-any.whl", hash = "sha256:0fb1add2adf99ef28883fd6228eb447ef519ea72933535ad1a0b28c6f65f868a"}, - {file = "eth_hash-0.7.1.tar.gz", hash = "sha256:d2411a403a0b0a62e8247b4117932d900ffb4c8c64b15f92620547ca5ce46be5"}, + {file = "eth_hash-0.8.0-py3-none-any.whl", hash = "sha256:523718a51b369ab89866b929a5c93c52978cd866ea309192ad980dd8271f9fac"}, + {file = "eth_hash-0.8.0.tar.gz", hash = "sha256:b009752b620da2e9c7668014849d1f5fadbe4f138603f1871cc5d4ca706896b1"}, ] [package.dependencies] pycryptodome = {version = ">=3.6.6,<4", optional = true, markers = "extra == \"pycryptodome\""} [package.extras] -dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "ipython", "mypy (==1.18.2)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel (>=0.38.1)"] docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] pycryptodome = ["pycryptodome (>=3.6.6,<4)"] pysha3 = ["pysha3 (>=1.0.0,<2.0.0)", "safe-pysha3 (>=1.0.0)"] @@ -1236,13 +1342,13 @@ test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] [[package]] name = "eth-keys" -version = "0.6.1" +version = "0.7.0" description = "eth-keys: Common API for Ethereum key operations" optional = false python-versions = "<4,>=3.8" files = [ - {file = "eth_keys-0.6.1-py3-none-any.whl", hash = "sha256:7deae4cd56e862e099ec58b78176232b931c4ea5ecded2f50c7b1ccbc10c24cf"}, - {file = "eth_keys-0.6.1.tar.gz", hash = "sha256:a43e263cbcabfd62fa769168efc6c27b1f5603040e4de22bb84d12567e4fd962"}, + {file = "eth_keys-0.7.0-py3-none-any.whl", hash = "sha256:b0cdda8ffe8e5ba69c7c5ca33f153828edcace844f67aabd4542d7de38b159cf"}, + {file = "eth_keys-0.7.0.tar.gz", hash = "sha256:79d24fd876201df67741de3e3fefb3f4dbcbb6ace66e47e6fe662851a4547814"}, ] [package.dependencies] @@ -1250,8 +1356,8 @@ eth-typing = ">=3" eth-utils = ">=2" [package.extras] -coincurve = ["coincurve (>=12.0.0)"] -dev = ["asn1tools (>=0.146.2)", "build (>=0.9.0)", "bump_my_version (>=0.19.0)", "coincurve (>=12.0.0)", "eth-hash[pysha3]", "factory-boy (>=3.0.1)", "hypothesis (>=5.10.3)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pyasn1 (>=0.4.5)", "pytest (>=7.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +coincurve = ["coincurve (>=17.0.0)"] +dev = ["asn1tools (>=0.146.2)", "build (>=0.9.0)", "bump_my_version (>=0.19.0)", "coincurve (>=17.0.0)", "eth-hash[pysha3]", "factory-boy (>=3.0.1)", "hypothesis (>=5.10.3)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pyasn1 (>=0.4.5)", "pytest (>=7.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] docs = ["towncrier (>=24,<25)"] test = ["asn1tools (>=0.146.2)", "eth-hash[pysha3]", "factory-boy (>=3.0.1)", "hypothesis (>=5.10.3)", "pyasn1 (>=0.4.5)", "pytest (>=7.0.0)"] @@ -1279,97 +1385,102 @@ test = ["eth-hash[pycryptodome]", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] [[package]] name = "eth-typing" -version = "5.2.0" +version = "6.0.0" description = "eth-typing: Common type annotations for ethereum python packages" optional = false -python-versions = "<4,>=3.8" +python-versions = "<4,>=3.10" files = [ - {file = "eth_typing-5.2.0-py3-none-any.whl", hash = "sha256:e1f424e97990fc3c6a1c05a7b0968caed4e20e9c99a4d5f4db3df418e25ddc80"}, - {file = "eth_typing-5.2.0.tar.gz", hash = "sha256:28685f7e2270ea0d209b75bdef76d8ecef27703e1a16399f6929820d05071c28"}, + {file = "eth_typing-6.0.0-py3-none-any.whl", hash = "sha256:ee74fb641eb36dd885e1c42c2a3055314efa532b3e71480816df70a94d35cfb9"}, + {file = "eth_typing-6.0.0.tar.gz", hash = "sha256:315dd460dc0b71c15a6cd51e3c0b70d237eec8771beb844144f3a1fb4adb2392"}, ] [package.dependencies] typing_extensions = ">=4.5.0" [package.extras] -dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "ipython", "mypy (==1.18.2)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel (>=0.38.1)"] docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] [[package]] name = "eth-utils" -version = "5.2.0" +version = "6.0.0" description = "eth-utils: Common utility functions for python code that interacts with Ethereum" optional = false -python-versions = "<4,>=3.8" +python-versions = "<4,>=3.10" files = [ - {file = "eth_utils-5.2.0-py3-none-any.whl", hash = "sha256:4d43eeb6720e89a042ad5b28d4b2111630ae764f444b85cbafb708d7f076da10"}, - {file = "eth_utils-5.2.0.tar.gz", hash = "sha256:17e474eb654df6e18f20797b22c6caabb77415a996b3ba0f3cc8df3437463134"}, + {file = "eth_utils-6.0.0-py3-none-any.whl", hash = "sha256:63cf48ee32c45541cb5748751909a8345c470432fb6f0fed4bd7c53fd6400469"}, + {file = "eth_utils-6.0.0.tar.gz", hash = "sha256:eb54b2f82dd300d3142c49a89da195e823f5e5284d43203593f87c67bad92a96"}, ] [package.dependencies] cytoolz = {version = ">=0.10.1", markers = "implementation_name == \"cpython\""} eth-hash = ">=0.3.1" eth-typing = ">=5.0.0" +pydantic = ">=2.0.0,<3" toolz = {version = ">0.8.2", markers = "implementation_name == \"pypy\""} [package.extras] -dev = ["build (>=0.9.0)", "bump-my-version (>=0.19.0)", "eth-hash[pycryptodome]", "hypothesis (>=4.43.0)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] -docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=24,<25)"] -test = ["hypothesis (>=4.43.0)", "mypy (==1.10.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "eth-hash[pycryptodome]", "hypothesis (>=4.43.0)", "ipython", "mypy (==1.18.2)", "mypy (==1.18.2)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel (>=0.38.1)"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +test = ["hypothesis (>=4.43.0)", "mypy (==1.18.2)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] [[package]] name = "exceptiongroup" -version = "1.2.0" +version = "1.3.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, - {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, + {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, + {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, ] +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + [package.extras] test = ["pytest (>=6)"] [[package]] name = "fastapi" -version = "0.115.4" +version = "0.115.14" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" files = [ - {file = "fastapi-0.115.4-py3-none-any.whl", hash = "sha256:0b504a063ffb3cf96a5e27dc1bc32c80ca743a2528574f9cdc77daa2d31b4742"}, - {file = "fastapi-0.115.4.tar.gz", hash = "sha256:db653475586b091cb8b2fec2ac54a680ac6a158e07406e1abae31679e8826349"}, + {file = "fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca"}, + {file = "fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739"}, ] [package.dependencies] email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"standard\""} fastapi-cli = {version = ">=0.0.5", extras = ["standard"], optional = true, markers = "extra == \"standard\""} httpx = {version = ">=0.23.0", optional = true, markers = "extra == \"standard\""} -jinja2 = {version = ">=2.11.2", optional = true, markers = "extra == \"standard\""} +jinja2 = {version = ">=3.1.5", optional = true, markers = "extra == \"standard\""} pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" -python-multipart = {version = ">=0.0.7", optional = true, markers = "extra == \"standard\""} -starlette = ">=0.40.0,<0.42.0" +python-multipart = {version = ">=0.0.18", optional = true, markers = "extra == \"standard\""} +starlette = ">=0.40.0,<0.47.0" typing-extensions = ">=4.8.0" uvicorn = {version = ">=0.12.0", extras = ["standard"], optional = true, markers = "extra == \"standard\""} [package.extras] -all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] [[package]] name = "fastapi-cli" -version = "0.0.5" +version = "0.0.7" description = "Run and manage FastAPI apps from the command line with FastAPI CLI. 🚀" optional = false python-versions = ">=3.8" files = [ - {file = "fastapi_cli-0.0.5-py3-none-any.whl", hash = "sha256:e94d847524648c748a5350673546bbf9bcaeb086b33c24f2e82e021436866a46"}, - {file = "fastapi_cli-0.0.5.tar.gz", hash = "sha256:d30e1239c6f46fcb95e606f02cdda59a1e2fa778a54b64686b3ff27f6211ff9f"}, + {file = "fastapi_cli-0.0.7-py3-none-any.whl", hash = "sha256:d549368ff584b2804336c61f192d86ddea080c11255f375959627911944804f4"}, + {file = "fastapi_cli-0.0.7.tar.gz", hash = "sha256:02b3b65956f526412515907a0793c9094abd4bfb5457b389f645b0ea6ba3605e"}, ] [package.dependencies] +rich-toolkit = ">=0.11.1" typer = ">=0.12.3" uvicorn = {version = ">=0.15.0", extras = ["standard"]} @@ -1413,13 +1524,13 @@ redis = ">=4.2.0rc1" [[package]] name = "fastapi-pagination" -version = "0.12.27" +version = "0.12.34" description = "FastAPI pagination" optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "fastapi_pagination-0.12.27-py3-none-any.whl", hash = "sha256:b239d449a878cbd5d2d4939871f8f1c328bfe8ec67779475c628ce4416b1812d"}, - {file = "fastapi_pagination-0.12.27.tar.gz", hash = "sha256:3f9ad8dba85a4e784de55145e934e06b9c5bcdc8e49ad00efa0515d962cd8696"}, + {file = "fastapi_pagination-0.12.34-py3-none-any.whl", hash = "sha256:089d1078aae1784395b4dbd923d0c8246641ddcc291c5ec6d92a30edb92ecbdd"}, + {file = "fastapi_pagination-0.12.34.tar.gz", hash = "sha256:05ee8c0bc572072160f7f30900bfd87869e1880c87bc5797922fec2e49e65f11"}, ] [package.dependencies] @@ -1427,484 +1538,503 @@ pydantic = ">=1.9.1" typing-extensions = ">=4.8.0,<5.0.0" [package.extras] -all = ["SQLAlchemy (>=1.3.20)", "asyncpg (>=0.24.0)", "beanie (>=1.25.0)", "bunnet (>=1.1.0,<2.0.0)", "databases (>=0.6.0)", "django (<5.0.0)", "mongoengine (>=0.23.1,<0.30.0)", "motor (>=2.5.1,<4.0.0)", "orm (>=0.3.1)", "ormar (>=0.11.2)", "piccolo (>=0.89,<0.122)", "pony (>=0.7.16,<0.8.0)", "scylla-driver (>=3.25.6,<4.0.0)", "sqlakeyset (>=2.0.1680321678,<3.0.0)", "sqlmodel (>=0.0.8,<0.0.15)", "tortoise-orm (>=0.16.18,!=0.21.0,<0.22.0)"] +all = ["SQLAlchemy (>=1.3.20)", "asyncpg (>=0.24.0)", "beanie (>=1.25.0)", "bunnet (>=1.1.0,<2.0.0)", "databases (>=0.6.0)", "django (<5.0.0)", "mongoengine (>=0.23.1,<0.30.0)", "motor (>=3.6.0,<4.0.0)", "odmantic (>=1.0.2,<2.0.0)", "orm (>=0.3.1)", "piccolo (>=0.89,<1.22)", "pony (>=0.7.16,<0.8.0)", "scylla-driver (>=3.25.6,<4.0.0)", "sqlakeyset (>=2.0.1680321678,<3.0.0)", "sqlmodel (>=0.0.22)", "tortoise-orm (>=0.22.0)"] asyncpg = ["SQLAlchemy (>=1.3.20)", "asyncpg (>=0.24.0)"] beanie = ["beanie (>=1.25.0)"] bunnet = ["bunnet (>=1.1.0,<2.0.0)"] databases = ["databases (>=0.6.0)"] django = ["databases (>=0.6.0)", "django (<5.0.0)"] mongoengine = ["mongoengine (>=0.23.1,<0.30.0)"] -motor = ["motor (>=2.5.1,<4.0.0)"] +motor = ["motor (>=3.6.0,<4.0.0)"] +odmantic = ["odmantic (>=1.0.2,<2.0.0)"] orm = ["databases (>=0.6.0)", "orm (>=0.3.1)"] -ormar = ["ormar (>=0.11.2)"] -piccolo = ["piccolo (>=0.89,<0.122)"] +piccolo = ["piccolo (>=0.89,<1.22)"] scylla-driver = ["scylla-driver (>=3.25.6,<4.0.0)"] sqlalchemy = ["SQLAlchemy (>=1.3.20)", "sqlakeyset (>=2.0.1680321678,<3.0.0)"] -sqlmodel = ["sqlakeyset (>=2.0.1680321678,<3.0.0)", "sqlmodel (>=0.0.8,<0.0.15)"] -tortoise = ["tortoise-orm (>=0.16.18,!=0.21.0,<0.22.0)"] +sqlmodel = ["sqlakeyset (>=2.0.1680321678,<3.0.0)", "sqlmodel (>=0.0.22)"] +tortoise = ["tortoise-orm (>=0.22.0)"] [[package]] name = "filelock" -version = "3.13.1" +version = "3.29.7" description = "A platform independent file lock." optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" files = [ - {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, - {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, + {file = "filelock-3.29.7-py3-none-any.whl", hash = "sha256:987db6f789a3a2a59f55081801b2b3697cb97e2a736b5f1a9e99b559285fbc51"}, + {file = "filelock-3.29.7.tar.gz", hash = "sha256:5b481979797ae69e72f0b389d89a80bdd585c260c5b3f1fb9c0a5ba9bb3f195d"}, ] -[package.extras] -docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] -typing = ["typing-extensions (>=4.8)"] - [[package]] name = "fonttools" -version = "4.47.2" +version = "4.63.0" description = "Tools to manipulate font files" optional = false -python-versions = ">=3.8" -files = [ - {file = "fonttools-4.47.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3b629108351d25512d4ea1a8393a2dba325b7b7d7308116b605ea3f8e1be88df"}, - {file = "fonttools-4.47.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c19044256c44fe299d9a73456aabee4b4d06c6b930287be93b533b4737d70aa1"}, - {file = "fonttools-4.47.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8be28c036b9f186e8c7eaf8a11b42373e7e4949f9e9f370202b9da4c4c3f56c"}, - {file = "fonttools-4.47.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f83a4daef6d2a202acb9bf572958f91cfde5b10c8ee7fb1d09a4c81e5d851fd8"}, - {file = "fonttools-4.47.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a5a5318ba5365d992666ac4fe35365f93004109d18858a3e18ae46f67907670"}, - {file = "fonttools-4.47.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8f57ecd742545362a0f7186774b2d1c53423ed9ece67689c93a1055b236f638c"}, - {file = "fonttools-4.47.2-cp310-cp310-win32.whl", hash = "sha256:a1c154bb85dc9a4cf145250c88d112d88eb414bad81d4cb524d06258dea1bdc0"}, - {file = "fonttools-4.47.2-cp310-cp310-win_amd64.whl", hash = "sha256:3e2b95dce2ead58fb12524d0ca7d63a63459dd489e7e5838c3cd53557f8933e1"}, - {file = "fonttools-4.47.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:29495d6d109cdbabe73cfb6f419ce67080c3ef9ea1e08d5750240fd4b0c4763b"}, - {file = "fonttools-4.47.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0a1d313a415eaaba2b35d6cd33536560deeebd2ed758b9bfb89ab5d97dc5deac"}, - {file = "fonttools-4.47.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90f898cdd67f52f18049250a6474185ef6544c91f27a7bee70d87d77a8daf89c"}, - {file = "fonttools-4.47.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3480eeb52770ff75140fe7d9a2ec33fb67b07efea0ab5129c7e0c6a639c40c70"}, - {file = "fonttools-4.47.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0255dbc128fee75fb9be364806b940ed450dd6838672a150d501ee86523ac61e"}, - {file = "fonttools-4.47.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f791446ff297fd5f1e2247c188de53c1bfb9dd7f0549eba55b73a3c2087a2703"}, - {file = "fonttools-4.47.2-cp311-cp311-win32.whl", hash = "sha256:740947906590a878a4bde7dd748e85fefa4d470a268b964748403b3ab2aeed6c"}, - {file = "fonttools-4.47.2-cp311-cp311-win_amd64.whl", hash = "sha256:63fbed184979f09a65aa9c88b395ca539c94287ba3a364517698462e13e457c9"}, - {file = "fonttools-4.47.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4ec558c543609e71b2275c4894e93493f65d2f41c15fe1d089080c1d0bb4d635"}, - {file = "fonttools-4.47.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e040f905d542362e07e72e03612a6270c33d38281fd573160e1003e43718d68d"}, - {file = "fonttools-4.47.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6dd58cc03016b281bd2c74c84cdaa6bd3ce54c5a7f47478b7657b930ac3ed8eb"}, - {file = "fonttools-4.47.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32ab2e9702dff0dd4510c7bb958f265a8d3dd5c0e2547e7b5f7a3df4979abb07"}, - {file = "fonttools-4.47.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a808f3c1d1df1f5bf39be869b6e0c263570cdafb5bdb2df66087733f566ea71"}, - {file = "fonttools-4.47.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ac71e2e201df041a2891067dc36256755b1229ae167edbdc419b16da78732c2f"}, - {file = "fonttools-4.47.2-cp312-cp312-win32.whl", hash = "sha256:69731e8bea0578b3c28fdb43dbf95b9386e2d49a399e9a4ad736b8e479b08085"}, - {file = "fonttools-4.47.2-cp312-cp312-win_amd64.whl", hash = "sha256:b3e1304e5f19ca861d86a72218ecce68f391646d85c851742d265787f55457a4"}, - {file = "fonttools-4.47.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:254d9a6f7be00212bf0c3159e0a420eb19c63793b2c05e049eb337f3023c5ecc"}, - {file = "fonttools-4.47.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eabae77a07c41ae0b35184894202305c3ad211a93b2eb53837c2a1143c8bc952"}, - {file = "fonttools-4.47.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a86a5ab2873ed2575d0fcdf1828143cfc6b977ac448e3dc616bb1e3d20efbafa"}, - {file = "fonttools-4.47.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13819db8445a0cec8c3ff5f243af6418ab19175072a9a92f6cc8ca7d1452754b"}, - {file = "fonttools-4.47.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4e743935139aa485fe3253fc33fe467eab6ea42583fa681223ea3f1a93dd01e6"}, - {file = "fonttools-4.47.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d49ce3ea7b7173faebc5664872243b40cf88814ca3eb135c4a3cdff66af71946"}, - {file = "fonttools-4.47.2-cp38-cp38-win32.whl", hash = "sha256:94208ea750e3f96e267f394d5588579bb64cc628e321dbb1d4243ffbc291b18b"}, - {file = "fonttools-4.47.2-cp38-cp38-win_amd64.whl", hash = "sha256:0f750037e02beb8b3569fbff701a572e62a685d2a0e840d75816592280e5feae"}, - {file = "fonttools-4.47.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3d71606c9321f6701642bd4746f99b6089e53d7e9817fc6b964e90d9c5f0ecc6"}, - {file = "fonttools-4.47.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:86e0427864c6c91cf77f16d1fb9bf1bbf7453e824589e8fb8461b6ee1144f506"}, - {file = "fonttools-4.47.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a00bd0e68e88987dcc047ea31c26d40a3c61185153b03457956a87e39d43c37"}, - {file = "fonttools-4.47.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5d77479fb885ef38a16a253a2f4096bc3d14e63a56d6246bfdb56365a12b20c"}, - {file = "fonttools-4.47.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5465df494f20a7d01712b072ae3ee9ad2887004701b95cb2cc6dcb9c2c97a899"}, - {file = "fonttools-4.47.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4c811d3c73b6abac275babb8aa439206288f56fdb2c6f8835e3d7b70de8937a7"}, - {file = "fonttools-4.47.2-cp39-cp39-win32.whl", hash = "sha256:5b60e3afa9635e3dfd3ace2757039593e3bd3cf128be0ddb7a1ff4ac45fa5a50"}, - {file = "fonttools-4.47.2-cp39-cp39-win_amd64.whl", hash = "sha256:7ee48bd9d6b7e8f66866c9090807e3a4a56cf43ffad48962725a190e0dd774c8"}, - {file = "fonttools-4.47.2-py3-none-any.whl", hash = "sha256:7eb7ad665258fba68fd22228a09f347469d95a97fb88198e133595947a20a184"}, - {file = "fonttools-4.47.2.tar.gz", hash = "sha256:7df26dd3650e98ca45f1e29883c96a0b9f5bb6af8d632a6a108bc744fa0bd9b3"}, +python-versions = ">=3.10" +files = [ + {file = "fonttools-4.63.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e3297a6a4059b4acc3a1e9a8b04741f240a80044eef08ebd32e8b5bcdddce75b"}, + {file = "fonttools-4.63.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b1cd75a03ad8cb5bc40c90bfde68c0c47de423aa19e5c0f362b43520645eea94"}, + {file = "fonttools-4.63.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0425b277a59cff3d80ca42162a8de360f318438a2ac83570842a678d826d579"}, + {file = "fonttools-4.63.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d7e5c9973aa04c95650c96e5f5ad865fbf42d62079163ecfab1e01cbc2504c22"}, + {file = "fonttools-4.63.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cb014d58140a38135f16064c74c652ed57aa0b75cbf8bb59cac821f7edb5334e"}, + {file = "fonttools-4.63.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:032038247a96c1690f9f31e377c389383c902531b085aa4e4dabd6f57f870e69"}, + {file = "fonttools-4.63.0-cp310-cp310-win32.whl", hash = "sha256:a8b33a82979e0a6a34ff435cc81317be1f95ec1ebb7a3a2d1c8a6a54f02ae44e"}, + {file = "fonttools-4.63.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c18358a155d75034911c5ee397a5b44cd19dd325dbb8b35fb60bf421d6a72ac"}, + {file = "fonttools-4.63.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2b8ae05d9eacf6081414d759c0a352769ac28ce31280d6bb8e77b03f9e3c449f"}, + {file = "fonttools-4.63.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79cdc9f567aec74a72918fd060283911406750cbc9fd28c1316023deb6ce31a9"}, + {file = "fonttools-4.63.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c14b4fd138c4bafcca294765c547914e1aa431ae1ca94ab99d8db08c958bd3b"}, + {file = "fonttools-4.63.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76ac49f929aecaf82d83250b8347e099d7aecba0f4726c1d9b6df3b8bb5fe18"}, + {file = "fonttools-4.63.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dcf076a4474fe0d7367e5bbf5b052c7284fa1feca729c04176ce513521afd8a0"}, + {file = "fonttools-4.63.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7dd683fef0663e9f0f45cf541d788d24caa3ec9db50796b588e1757d8b3bc007"}, + {file = "fonttools-4.63.0-cp311-cp311-win32.whl", hash = "sha256:afefc1ed0a59785a7fb06ea7e1678e849c193e1e387db783579bc7b3056fcfcb"}, + {file = "fonttools-4.63.0-cp311-cp311-win_amd64.whl", hash = "sha256:063e08bd17bd5a90127a14123de0d6a952dbc847695fd98b63c043d58057f90c"}, + {file = "fonttools-4.63.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02"}, + {file = "fonttools-4.63.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0"}, + {file = "fonttools-4.63.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af"}, + {file = "fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8"}, + {file = "fonttools-4.63.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b"}, + {file = "fonttools-4.63.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78"}, + {file = "fonttools-4.63.0-cp312-cp312-win32.whl", hash = "sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263"}, + {file = "fonttools-4.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272"}, + {file = "fonttools-4.63.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd"}, + {file = "fonttools-4.63.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59"}, + {file = "fonttools-4.63.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d"}, + {file = "fonttools-4.63.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68"}, + {file = "fonttools-4.63.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be"}, + {file = "fonttools-4.63.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27"}, + {file = "fonttools-4.63.0-cp313-cp313-win32.whl", hash = "sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380"}, + {file = "fonttools-4.63.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b"}, + {file = "fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745"}, + {file = "fonttools-4.63.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03"}, + {file = "fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49"}, + {file = "fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b"}, + {file = "fonttools-4.63.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6"}, + {file = "fonttools-4.63.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4"}, + {file = "fonttools-4.63.0-cp314-cp314-win32.whl", hash = "sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616"}, + {file = "fonttools-4.63.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5"}, + {file = "fonttools-4.63.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001"}, + {file = "fonttools-4.63.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e"}, + {file = "fonttools-4.63.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096"}, + {file = "fonttools-4.63.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f"}, + {file = "fonttools-4.63.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40"}, + {file = "fonttools-4.63.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196"}, + {file = "fonttools-4.63.0-cp314-cp314t-win32.whl", hash = "sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8"}, + {file = "fonttools-4.63.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419"}, + {file = "fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d"}, + {file = "fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0"}, ] [package.extras] -all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0,<5)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"] +all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.45.0)", "unicodedata2 (>=17.0.0)", "xattr", "zopfli (>=0.1.4)"] graphite = ["lz4 (>=1.7.4.2)"] interpolatable = ["munkres", "pycairo", "scipy"] -lxml = ["lxml (>=4.0,<5)"] +lxml = ["lxml (>=4.0)"] pathops = ["skia-pathops (>=0.5.0)"] plot = ["matplotlib"] -repacker = ["uharfbuzz (>=0.23.0)"] +repacker = ["uharfbuzz (>=0.45.0)"] symfont = ["sympy"] type1 = ["xattr"] -ufo = ["fs (>=2.2.0,<3)"] -unicode = ["unicodedata2 (>=15.1.0)"] +unicode = ["unicodedata2 (>=17.0.0)"] woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] [[package]] name = "frozenlist" -version = "1.5.0" +version = "1.8.0" description = "A list-like structure which implements collections.abc.MutableSequence" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"}, - {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"}, - {file = "frozenlist-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec"}, - {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5"}, - {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76"}, - {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17"}, - {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba"}, - {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d"}, - {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2"}, - {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f"}, - {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c"}, - {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab"}, - {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5"}, - {file = "frozenlist-1.5.0-cp310-cp310-win32.whl", hash = "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb"}, - {file = "frozenlist-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4"}, - {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30"}, - {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5"}, - {file = "frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778"}, - {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a"}, - {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869"}, - {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d"}, - {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45"}, - {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d"}, - {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3"}, - {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a"}, - {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9"}, - {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2"}, - {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf"}, - {file = "frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942"}, - {file = "frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d"}, - {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21"}, - {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d"}, - {file = "frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e"}, - {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a"}, - {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a"}, - {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee"}, - {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6"}, - {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e"}, - {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9"}, - {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039"}, - {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784"}, - {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631"}, - {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f"}, - {file = "frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8"}, - {file = "frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f"}, - {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953"}, - {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0"}, - {file = "frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2"}, - {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f"}, - {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608"}, - {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b"}, - {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840"}, - {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439"}, - {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de"}, - {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641"}, - {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e"}, - {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9"}, - {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03"}, - {file = "frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c"}, - {file = "frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28"}, - {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca"}, - {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10"}, - {file = "frozenlist-1.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604"}, - {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3"}, - {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307"}, - {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10"}, - {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9"}, - {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99"}, - {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c"}, - {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171"}, - {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e"}, - {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf"}, - {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e"}, - {file = "frozenlist-1.5.0-cp38-cp38-win32.whl", hash = "sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723"}, - {file = "frozenlist-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923"}, - {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972"}, - {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336"}, - {file = "frozenlist-1.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f"}, - {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f"}, - {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6"}, - {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411"}, - {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08"}, - {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2"}, - {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d"}, - {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b"}, - {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b"}, - {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0"}, - {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c"}, - {file = "frozenlist-1.5.0-cp39-cp39-win32.whl", hash = "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3"}, - {file = "frozenlist-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0"}, - {file = "frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3"}, - {file = "frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817"}, -] - -[[package]] -name = "future" -version = "0.18.3" -description = "Clean single-source support for Python 3 and 2" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "future-0.18.3.tar.gz", hash = "sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7"}, + {file = "frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a"}, + {file = "frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6"}, + {file = "frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967"}, + {file = "frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25"}, + {file = "frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b"}, + {file = "frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa"}, + {file = "frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf"}, + {file = "frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746"}, + {file = "frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed"}, + {file = "frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496"}, + {file = "frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231"}, + {file = "frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7"}, + {file = "frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806"}, + {file = "frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0"}, + {file = "frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda"}, + {file = "frozenlist-1.8.0-cp39-cp39-win32.whl", hash = "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087"}, + {file = "frozenlist-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a"}, + {file = "frozenlist-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103"}, + {file = "frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d"}, + {file = "frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad"}, ] [[package]] name = "google-api-core" -version = "2.17.1" +version = "2.31.0" description = "Google API client core library" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" files = [ - {file = "google-api-core-2.17.1.tar.gz", hash = "sha256:9df18a1f87ee0df0bc4eea2770ebc4228392d8cc4066655b320e2cfccb15db95"}, - {file = "google_api_core-2.17.1-py3-none-any.whl", hash = "sha256:610c5b90092c360736baccf17bd3efbcb30dd380e7a6dc28a71059edb8bd0d8e"}, + {file = "google_api_core-2.31.0-py3-none-any.whl", hash = "sha256:ef79fb3784c71cbac89cbd03301ba0c8fb8ad2aa95d7f9204dd9628f7adf59ab"}, + {file = "google_api_core-2.31.0.tar.gz", hash = "sha256:2be84ee0f584c48e6bde1b36766e23348b361fb7e55e56135fc76ce1c397f9c2"}, ] [package.dependencies] -google-auth = ">=2.14.1,<3.0.dev0" -googleapis-common-protos = ">=1.56.2,<2.0.dev0" -protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0.dev0" -requests = ">=2.18.0,<3.0.0.dev0" +google-auth = ">=2.14.1,<3.0.0" +googleapis-common-protos = ">=1.63.2,<2.0.0" +proto-plus = ">=1.24.0,<2.0.0" +protobuf = ">=5.29.6,<8.0.0" +requests = ">=2.33.0,<3.0.0" [package.extras] -grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0)"] -grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] -grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] +async-rest = ["aiohttp (>=3.13.4)", "google-auth[aiohttp] (>=2.14.1,<3.0.0)"] +grpc = ["grpcio (>=1.41.0,<2.0.0)", "grpcio (>=1.49.1,<2.0.0)", "grpcio (>=1.75.1,<2.0.0)", "grpcio-status (>=1.41.0,<2.0.0)", "grpcio-status (>=1.49.1,<2.0.0)", "grpcio-status (>=1.75.1,<2.0.0)"] [[package]] name = "google-auth" -version = "2.28.0" +version = "2.55.2" description = "Google Authentication Library" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" files = [ - {file = "google-auth-2.28.0.tar.gz", hash = "sha256:3cfc1b6e4e64797584fb53fc9bd0b7afa9b7c0dba2004fa7dcc9349e58cc3195"}, - {file = "google_auth-2.28.0-py2.py3-none-any.whl", hash = "sha256:7634d29dcd1e101f5226a23cbc4a0c6cda6394253bf80e281d9c5c6797869c53"}, + {file = "google_auth-2.55.2-py3-none-any.whl", hash = "sha256:d715f265f2cafc6a5f1bf0dc19870d20e3119f6f6682785a250bce3d03d38a3b"}, + {file = "google_auth-2.55.2.tar.gz", hash = "sha256:97ae7790ff740f2bc9db60eb864a7804f4ac19f5f02c38b3d942f2fea6e9b9ae"}, ] [package.dependencies] -cachetools = ">=2.0.0,<6.0" +cryptography = ">=38.0.3" pyasn1-modules = ">=0.2.1" -rsa = ">=3.1.4,<5" [package.extras] -aiohttp = ["aiohttp (>=3.6.2,<4.0.0.dev0)", "requests (>=2.20.0,<3.0.0.dev0)"] -enterprise-cert = ["cryptography (==36.0.2)", "pyopenssl (==22.0.0)"] -pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] +aiohttp = ["aiohttp (>=3.8.0,<4.0.0)", "requests (>=2.20.0,<3.0.0)"] +cryptography = ["cryptography (>=38.0.3)"] +enterprise-cert = ["cryptography (>=38.0.3)"] +grpc = ["grpcio (>=1.59.0,<2.0.0)", "grpcio (>=1.75.1,<2.0.0)"] +pyjwt = ["pyjwt (>=2.0)"] +pyopenssl = ["cryptography (>=38.0.3)"] reauth = ["pyu2f (>=0.1.5)"] -requests = ["requests (>=2.20.0,<3.0.0.dev0)"] +requests = ["requests (>=2.20.0,<3.0.0)"] +rsa = ["rsa (>=3.1.4,<5)"] +testing = ["aiohttp (>=3.8.0,<4.0.0)", "aioresponses", "flask", "freezegun", "grpcio (>=1.59.0,<2.0.0)", "grpcio (>=1.75.1,<2.0.0)", "packaging", "pyjwt (>=2.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-localserver", "pyu2f (>=0.1.5)", "requests (>=2.20.0,<3.0.0)", "responses", "urllib3"] +urllib3 = ["packaging", "urllib3"] [[package]] name = "google-cloud-core" -version = "2.4.1" +version = "2.6.0" description = "Google Cloud API client core library" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" files = [ - {file = "google-cloud-core-2.4.1.tar.gz", hash = "sha256:9b7749272a812bde58fff28868d0c5e2f585b82f37e09a1f6ed2d4d10f134073"}, - {file = "google_cloud_core-2.4.1-py2.py3-none-any.whl", hash = "sha256:a9e6a4422b9ac5c29f79a0ede9485473338e2ce78d91f2370c01e730eab22e61"}, + {file = "google_cloud_core-2.6.0-py3-none-any.whl", hash = "sha256:6d63ac8e5eca6d9e4319d0a1e2265fadcd7f1049904378caecfa01cf52dd869e"}, + {file = "google_cloud_core-2.6.0.tar.gz", hash = "sha256:e76149739f90fac1fc6757c09f47eaccb3145b54adbd7759b0f7c4b235f46c83"}, ] [package.dependencies] -google-api-core = ">=1.31.6,<2.0.dev0 || >2.3.0,<3.0.0dev" -google-auth = ">=1.25.0,<3.0dev" +google-api-core = ">=2.11.0,<3.0.0" +google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0" [package.extras] -grpc = ["grpcio (>=1.38.0,<2.0dev)", "grpcio-status (>=1.38.0,<2.0.dev0)"] +grpc = ["grpcio (>=1.47.0,<2.0.0)", "grpcio (>=1.75.1,<2.0.0)", "grpcio-status (>=1.47.0,<2.0.0)"] [[package]] name = "google-cloud-storage" -version = "2.14.0" +version = "2.19.0" description = "Google Cloud Storage API client library" optional = false python-versions = ">=3.7" files = [ - {file = "google-cloud-storage-2.14.0.tar.gz", hash = "sha256:2d23fcf59b55e7b45336729c148bb1c464468c69d5efbaee30f7201dd90eb97e"}, - {file = "google_cloud_storage-2.14.0-py2.py3-none-any.whl", hash = "sha256:8641243bbf2a2042c16a6399551fbb13f062cbc9a2de38d6c0bb5426962e9dbd"}, + {file = "google_cloud_storage-2.19.0-py2.py3-none-any.whl", hash = "sha256:aeb971b5c29cf8ab98445082cbfe7b161a1f48ed275822f59ed3f1524ea54fba"}, + {file = "google_cloud_storage-2.19.0.tar.gz", hash = "sha256:cd05e9e7191ba6cb68934d8eb76054d9be4562aa89dbc4236feee4d7d51342b2"}, ] [package.dependencies] -google-api-core = ">=1.31.5,<2.0.dev0 || >2.3.0,<3.0.0dev" -google-auth = ">=2.23.3,<3.0dev" +google-api-core = ">=2.15.0,<3.0.0dev" +google-auth = ">=2.26.1,<3.0dev" google-cloud-core = ">=2.3.0,<3.0dev" google-crc32c = ">=1.0,<2.0dev" -google-resumable-media = ">=2.6.0" +google-resumable-media = ">=2.7.2" requests = ">=2.18.0,<3.0.0dev" [package.extras] -protobuf = ["protobuf (<5.0.0dev)"] +protobuf = ["protobuf (<6.0.0dev)"] +tracing = ["opentelemetry-api (>=1.1.0)"] [[package]] name = "google-crc32c" -version = "1.5.0" +version = "1.8.0" description = "A python wrapper of the C library 'Google CRC32C'" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "google-crc32c-1.5.0.tar.gz", hash = "sha256:89284716bc6a5a415d4eaa11b1726d2d60a0cd12aadf5439828353662ede9dd7"}, - {file = "google_crc32c-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:596d1f98fc70232fcb6590c439f43b350cb762fb5d61ce7b0e9db4539654cc13"}, - {file = "google_crc32c-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:be82c3c8cfb15b30f36768797a640e800513793d6ae1724aaaafe5bf86f8f346"}, - {file = "google_crc32c-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:461665ff58895f508e2866824a47bdee72497b091c730071f2b7575d5762ab65"}, - {file = "google_crc32c-1.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2096eddb4e7c7bdae4bd69ad364e55e07b8316653234a56552d9c988bd2d61b"}, - {file = "google_crc32c-1.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:116a7c3c616dd14a3de8c64a965828b197e5f2d121fedd2f8c5585c547e87b02"}, - {file = "google_crc32c-1.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5829b792bf5822fd0a6f6eb34c5f81dd074f01d570ed7f36aa101d6fc7a0a6e4"}, - {file = "google_crc32c-1.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:64e52e2b3970bd891309c113b54cf0e4384762c934d5ae56e283f9a0afcd953e"}, - {file = "google_crc32c-1.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:02ebb8bf46c13e36998aeaad1de9b48f4caf545e91d14041270d9dca767b780c"}, - {file = "google_crc32c-1.5.0-cp310-cp310-win32.whl", hash = "sha256:2e920d506ec85eb4ba50cd4228c2bec05642894d4c73c59b3a2fe20346bd00ee"}, - {file = "google_crc32c-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:07eb3c611ce363c51a933bf6bd7f8e3878a51d124acfc89452a75120bc436289"}, - {file = "google_crc32c-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cae0274952c079886567f3f4f685bcaf5708f0a23a5f5216fdab71f81a6c0273"}, - {file = "google_crc32c-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1034d91442ead5a95b5aaef90dbfaca8633b0247d1e41621d1e9f9db88c36298"}, - {file = "google_crc32c-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c42c70cd1d362284289c6273adda4c6af8039a8ae12dc451dcd61cdabb8ab57"}, - {file = "google_crc32c-1.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8485b340a6a9e76c62a7dce3c98e5f102c9219f4cfbf896a00cf48caf078d438"}, - {file = "google_crc32c-1.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77e2fd3057c9d78e225fa0a2160f96b64a824de17840351b26825b0848022906"}, - {file = "google_crc32c-1.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f583edb943cf2e09c60441b910d6a20b4d9d626c75a36c8fcac01a6c96c01183"}, - {file = "google_crc32c-1.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:a1fd716e7a01f8e717490fbe2e431d2905ab8aa598b9b12f8d10abebb36b04dd"}, - {file = "google_crc32c-1.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:72218785ce41b9cfd2fc1d6a017dc1ff7acfc4c17d01053265c41a2c0cc39b8c"}, - {file = "google_crc32c-1.5.0-cp311-cp311-win32.whl", hash = "sha256:66741ef4ee08ea0b2cc3c86916ab66b6aef03768525627fd6a1b34968b4e3709"}, - {file = "google_crc32c-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba1eb1843304b1e5537e1fca632fa894d6f6deca8d6389636ee5b4797affb968"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:98cb4d057f285bd80d8778ebc4fde6b4d509ac3f331758fb1528b733215443ae"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd8536e902db7e365f49e7d9029283403974ccf29b13fc7028b97e2295b33556"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19e0a019d2c4dcc5e598cd4a4bc7b008546b0358bd322537c74ad47a5386884f"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02c65b9817512edc6a4ae7c7e987fea799d2e0ee40c53ec573a692bee24de876"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6ac08d24c1f16bd2bf5eca8eaf8304812f44af5cfe5062006ec676e7e1d50afc"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3359fc442a743e870f4588fcf5dcbc1bf929df1fad8fb9905cd94e5edb02e84c"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e986b206dae4476f41bcec1faa057851f3889503a70e1bdb2378d406223994a"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:de06adc872bcd8c2a4e0dc51250e9e65ef2ca91be023b9d13ebd67c2ba552e1e"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-win32.whl", hash = "sha256:d3515f198eaa2f0ed49f8819d5732d70698c3fa37384146079b3799b97667a94"}, - {file = "google_crc32c-1.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:67b741654b851abafb7bc625b6d1cdd520a379074e64b6a128e3b688c3c04740"}, - {file = "google_crc32c-1.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c02ec1c5856179f171e032a31d6f8bf84e5a75c45c33b2e20a3de353b266ebd8"}, - {file = "google_crc32c-1.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:edfedb64740750e1a3b16152620220f51d58ff1b4abceb339ca92e934775c27a"}, - {file = "google_crc32c-1.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84e6e8cd997930fc66d5bb4fde61e2b62ba19d62b7abd7a69920406f9ecca946"}, - {file = "google_crc32c-1.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:024894d9d3cfbc5943f8f230e23950cd4906b2fe004c72e29b209420a1e6b05a"}, - {file = "google_crc32c-1.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:998679bf62b7fb599d2878aa3ed06b9ce688b8974893e7223c60db155f26bd8d"}, - {file = "google_crc32c-1.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:83c681c526a3439b5cf94f7420471705bbf96262f49a6fe546a6db5f687a3d4a"}, - {file = "google_crc32c-1.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4c6fdd4fccbec90cc8a01fc00773fcd5fa28db683c116ee3cb35cd5da9ef6c37"}, - {file = "google_crc32c-1.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5ae44e10a8e3407dbe138984f21e536583f2bba1be9491239f942c2464ac0894"}, - {file = "google_crc32c-1.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:37933ec6e693e51a5b07505bd05de57eee12f3e8c32b07da7e73669398e6630a"}, - {file = "google_crc32c-1.5.0-cp38-cp38-win32.whl", hash = "sha256:fe70e325aa68fa4b5edf7d1a4b6f691eb04bbccac0ace68e34820d283b5f80d4"}, - {file = "google_crc32c-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:74dea7751d98034887dbd821b7aae3e1d36eda111d6ca36c206c44478035709c"}, - {file = "google_crc32c-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c6c777a480337ac14f38564ac88ae82d4cd238bf293f0a22295b66eb89ffced7"}, - {file = "google_crc32c-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:759ce4851a4bb15ecabae28f4d2e18983c244eddd767f560165563bf9aefbc8d"}, - {file = "google_crc32c-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f13cae8cc389a440def0c8c52057f37359014ccbc9dc1f0827936bcd367c6100"}, - {file = "google_crc32c-1.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e560628513ed34759456a416bf86b54b2476c59144a9138165c9a1575801d0d9"}, - {file = "google_crc32c-1.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1674e4307fa3024fc897ca774e9c7562c957af85df55efe2988ed9056dc4e57"}, - {file = "google_crc32c-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:278d2ed7c16cfc075c91378c4f47924c0625f5fc84b2d50d921b18b7975bd210"}, - {file = "google_crc32c-1.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d5280312b9af0976231f9e317c20e4a61cd2f9629b7bfea6a693d1878a264ebd"}, - {file = "google_crc32c-1.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8b87e1a59c38f275c0e3676fc2ab6d59eccecfd460be267ac360cc31f7bcde96"}, - {file = "google_crc32c-1.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7c074fece789b5034b9b1404a1f8208fc2d4c6ce9decdd16e8220c5a793e6f61"}, - {file = "google_crc32c-1.5.0-cp39-cp39-win32.whl", hash = "sha256:7f57f14606cd1dd0f0de396e1e53824c371e9544a822648cd76c034d209b559c"}, - {file = "google_crc32c-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:a2355cba1f4ad8b6988a4ca3feed5bff33f6af2d7f134852cf279c2aebfde541"}, - {file = "google_crc32c-1.5.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f314013e7dcd5cf45ab1945d92e713eec788166262ae8deb2cfacd53def27325"}, - {file = "google_crc32c-1.5.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b747a674c20a67343cb61d43fdd9207ce5da6a99f629c6e2541aa0e89215bcd"}, - {file = "google_crc32c-1.5.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8f24ed114432de109aa9fd317278518a5af2d31ac2ea6b952b2f7782b43da091"}, - {file = "google_crc32c-1.5.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8667b48e7a7ef66afba2c81e1094ef526388d35b873966d8a9a447974ed9178"}, - {file = "google_crc32c-1.5.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1c7abdac90433b09bad6c43a43af253e688c9cfc1c86d332aed13f9a7c7f65e2"}, - {file = "google_crc32c-1.5.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6f998db4e71b645350b9ac28a2167e6632c239963ca9da411523bb439c5c514d"}, - {file = "google_crc32c-1.5.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c99616c853bb585301df6de07ca2cadad344fd1ada6d62bb30aec05219c45d2"}, - {file = "google_crc32c-1.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ad40e31093a4af319dadf503b2467ccdc8f67c72e4bcba97f8c10cb078207b5"}, - {file = "google_crc32c-1.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd67cf24a553339d5062eff51013780a00d6f97a39ca062781d06b3a73b15462"}, - {file = "google_crc32c-1.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:398af5e3ba9cf768787eef45c803ff9614cc3e22a5b2f7d7ae116df8b11e3314"}, - {file = "google_crc32c-1.5.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b1f8133c9a275df5613a451e73f36c2aea4fe13c5c8997e22cf355ebd7bd0728"}, - {file = "google_crc32c-1.5.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ba053c5f50430a3fcfd36f75aff9caeba0440b2d076afdb79a318d6ca245f88"}, - {file = "google_crc32c-1.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:272d3892a1e1a2dbc39cc5cde96834c236d5327e2122d3aaa19f6614531bb6eb"}, - {file = "google_crc32c-1.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:635f5d4dd18758a1fbd1049a8e8d2fee4ffed124462d837d1a02a0e009c3ab31"}, - {file = "google_crc32c-1.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c672d99a345849301784604bfeaeba4db0c7aae50b95be04dd651fd2a7310b93"}, + {file = "google_crc32c-1.8.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0470b8c3d73b5f4e3300165498e4cf25221c7eb37f1159e221d1825b6df8a7ff"}, + {file = "google_crc32c-1.8.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:119fcd90c57c89f30040b47c211acee231b25a45d225e3225294386f5d258288"}, + {file = "google_crc32c-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f35aaffc8ccd81ba3162443fabb920e65b1f20ab1952a31b13173a67811467d"}, + {file = "google_crc32c-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:864abafe7d6e2c4c66395c1eb0fe12dc891879769b52a3d56499612ca93b6092"}, + {file = "google_crc32c-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:db3fe8eaf0612fc8b20fa21a5f25bd785bc3cd5be69f8f3412b0ac2ffd49e733"}, + {file = "google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8"}, + {file = "google_crc32c-1.8.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7"}, + {file = "google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15"}, + {file = "google_crc32c-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a"}, + {file = "google_crc32c-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2"}, + {file = "google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113"}, + {file = "google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb"}, + {file = "google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411"}, + {file = "google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454"}, + {file = "google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962"}, + {file = "google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b"}, + {file = "google_crc32c-1.8.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27"}, + {file = "google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa"}, + {file = "google_crc32c-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8"}, + {file = "google_crc32c-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f"}, + {file = "google_crc32c-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697"}, + {file = "google_crc32c-1.8.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651"}, + {file = "google_crc32c-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2"}, + {file = "google_crc32c-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21"}, + {file = "google_crc32c-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2"}, + {file = "google_crc32c-1.8.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:ba6aba18daf4d36ad4412feede6221414692f44d17e5428bdd81ad3fc1eee5dc"}, + {file = "google_crc32c-1.8.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:87b0072c4ecc9505cfa16ee734b00cd7721d20a0f595be4d40d3d21b41f65ae2"}, + {file = "google_crc32c-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d488e98b18809f5e322978d4506373599c0c13e6c5ad13e53bb44758e18d215"}, + {file = "google_crc32c-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01f126a5cfddc378290de52095e2c7052be2ba7656a9f0caf4bcd1bfb1833f8a"}, + {file = "google_crc32c-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:61f58b28e0b21fcb249a8247ad0db2e64114e201e2e9b4200af020f3b6242c9f"}, + {file = "google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93"}, + {file = "google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c"}, + {file = "google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79"}, ] -[package.extras] -testing = ["pytest"] - [[package]] name = "google-resumable-media" -version = "2.7.0" +version = "2.10.0" description = "Utilities for Google Media Downloads and Resumable Uploads" optional = false -python-versions = ">= 3.7" +python-versions = ">=3.10" files = [ - {file = "google-resumable-media-2.7.0.tar.gz", hash = "sha256:5f18f5fa9836f4b083162064a1c2c98c17239bfda9ca50ad970ccf905f3e625b"}, - {file = "google_resumable_media-2.7.0-py2.py3-none-any.whl", hash = "sha256:79543cfe433b63fd81c0844b7803aba1bb8950b47bedf7d980c38fa123937e08"}, + {file = "google_resumable_media-2.10.0-py3-none-any.whl", hash = "sha256:88152884bee37b2bf36a0ab81ad8c7fd12212c9803dd981d77c1b35b02d34e7c"}, + {file = "google_resumable_media-2.10.0.tar.gz", hash = "sha256:e324bc9d0fdae4c52a08ae90456edc4e71ece858399e1217ac0eb3a51d6bc6ee"}, ] [package.dependencies] -google-crc32c = ">=1.0,<2.0dev" +google-crc32c = ">=1.0.0,<2.0.0" [package.extras] -aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)", "google-auth (>=1.22.0,<2.0dev)"] -requests = ["requests (>=2.18.0,<3.0.0dev)"] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0)", "google-auth (>=1.22.0,<2.0.0)"] +requests = ["requests (>=2.18.0,<3.0.0)"] [[package]] name = "googleapis-common-protos" -version = "1.62.0" +version = "1.75.0" description = "Common protobufs used in Google APIs" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "googleapis-common-protos-1.62.0.tar.gz", hash = "sha256:83f0ece9f94e5672cced82f592d2a5edf527a96ed1794f0bab36d5735c996277"}, - {file = "googleapis_common_protos-1.62.0-py2.py3-none-any.whl", hash = "sha256:4750113612205514f9f6aa4cb00d523a94f3e8c06c5ad2fee466387dc4875f07"}, + {file = "googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed"}, + {file = "googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd"}, ] [package.dependencies] -protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0.dev0" +protobuf = ">=4.25.8,<8.0.0" [package.extras] -grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] +grpc = ["grpcio (>=1.44.0,<2.0.0)"] [[package]] name = "greenlet" -version = "3.0.3" +version = "3.5.3" description = "Lightweight in-process concurrent programming" optional = false -python-versions = ">=3.7" -files = [ - {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, - {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, - {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, - {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, - {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, - {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, - {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, - {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, - {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, - {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, - {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, - {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, - {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, - {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, - {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, - {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, - {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, - {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, - {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, - {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, - {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, - {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, - {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, - {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, - {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, - {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, - {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, - {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, +python-versions = ">=3.10" +files = [ + {file = "greenlet-3.5.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c180d22d325fb613956b443c3c6f4406eb70e6defc70d3974da2a7b59e06f48c"}, + {file = "greenlet-3.5.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483d08c11181c83a6ce1a7a61df0f624a208ec40817a3bb2302714592eee4f04"}, + {file = "greenlet-3.5.3-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1dae6e0091eae084317e411f047f0b7cb241c6db570f7c45fd6b900a274914ce"}, + {file = "greenlet-3.5.3-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0f6ff50ff8dbd51fae9b37f4101648b04ea0df19b3f50ab2beb5061e7716a5c8"}, + {file = "greenlet-3.5.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcd2d72ccd70a1ec68ba6ef93e7fbb4420ef9997dabc7010d893bd4015e0bec"}, + {file = "greenlet-3.5.3-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:37bf9c538f5ae6e63d643f88dec37c0c83bdf0e2ebc62961dedcf458822f7b71"}, + {file = "greenlet-3.5.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:73f152c895e09907e0dbe24f6c2db37beb085cd63db91c3825a0fcd0064124a8"}, + {file = "greenlet-3.5.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8bdb43e1a1d1873721acab2be99c5befd4d2044ddfd52e4d610801019880a702"}, + {file = "greenlet-3.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:0909f9355a9f24845d3299f3112e266a06afb68302041989fd26bd68894933db"}, + {file = "greenlet-3.5.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:aca9b4ce85b152b5524ef7d88170efdff80dc0032aa8b75f9aaf7f3479ea95b4"}, + {file = "greenlet-3.5.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f71be4920368fe1fabeeaa53d1e3548337e2b223d9565f8ad5e392a75ba23fc"}, + {file = "greenlet-3.5.3-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d77e67f65f98449e3fb83f795b5d0a8437aead2f874ca89c96576caf4be3af6"}, + {file = "greenlet-3.5.3-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e18619ba655ac05d78d80fc83cac4ba892bd6927b99e3b8237aee861aaacc8bb"}, + {file = "greenlet-3.5.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8540f1e6205bd13ca0ce685581037219ca54a1b41a0a15d228c6c9b8ad5903d7"}, + {file = "greenlet-3.5.3-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:d27c0c653a60d9535f690226474a5cc1036a8b0d7b57504d1c4f89c44a07a80c"}, + {file = "greenlet-3.5.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ef56fe650f50575bf843acde967b9c567687f3c22340941a899b7bc56e956a8"}, + {file = "greenlet-3.5.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5121af01cf911e70056c00d4b46d5e9b5d1415550038573d744138bacb59e6b8"}, + {file = "greenlet-3.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:0f41e4a05a3c0cb31b17023eff28dd111e1d16bf7d7d00406cd7df23f31398a7"}, + {file = "greenlet-3.5.3-cp311-cp311-win_arm64.whl", hash = "sha256:ec6f1af59f6b5f3fc9678e2ea062d8377d22ac644f7844cb7a292910cf12ff44"}, + {file = "greenlet-3.5.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:719757059f5a53fd0dde23f78cffeafcdd97b21c850ddb7ca684a3c1a1f122e2"}, + {file = "greenlet-3.5.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efa9f765dd09f9d0cdac651ffdf631ee59ec5dc6ee7a73e0c012ba9c52fbdf5b"}, + {file = "greenlet-3.5.3-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7faba15ac005376e02a0384504e0243be3370ce010296a44a820feb342b505ab"}, + {file = "greenlet-3.5.3-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5795cd1101371140551c645f2d408b8d3c01a5a29cf8a9bce6e759c983682d23"}, + {file = "greenlet-3.5.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:87142215824be6ac05e2e8e2786eec307ccbc27c36723c3881959df654af6861"}, + {file = "greenlet-3.5.3-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:af4923b3096e26a36d7e9cf24ab88083a20f97d191e3b97f253731ce9b41b28c"}, + {file = "greenlet-3.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215275b1b49320987352e6c1b054acca0064f965a2c66992bed9a6f7d913f149"}, + {file = "greenlet-3.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6b1b0eed82364b0e32c4ea0f221452d33e6bb17ae094d9f72aed9851812747ea"}, + {file = "greenlet-3.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:cde8adafa2365676f74a979744629589999093bc86e2484214f58e61df08902c"}, + {file = "greenlet-3.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:c4e7b79d83805475f0102008843f6eb45fd3bb0b2e88c774adab5fbaab27117d"}, + {file = "greenlet-3.5.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c8d87c2134d871df96ecdea9cec7cbaab286dadab0f56476e57aaf9e8ac11550"}, + {file = "greenlet-3.5.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2d185dd1621757e70c3861cceffd5317ab4e7ed7eb09c82994828468527ade5"}, + {file = "greenlet-3.5.3-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c514a468149bf8fbbab874188a3535cd8a48a3e353eb53a3d424296f8dbacd3"}, + {file = "greenlet-3.5.3-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9ad04dd75458c6300b047c61b8639092433d205a25a14e310d6582a480efcca1"}, + {file = "greenlet-3.5.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:915f887cf2682b66419b879423a2e072634aa7b7dce6f3ada4957cfced3f1e9a"}, + {file = "greenlet-3.5.3-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:afaabdd554cd7ae9bbb3ca070b0d7fdfd207dbf1d16865f7233837709d354bda"}, + {file = "greenlet-3.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:766cfd421c13e450feb340cd472a3ed9957d438727b7b4593ad7c76c5d2b0deb"}, + {file = "greenlet-3.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2ecda9ec22edf38fa389369eaed8c3d37c05f3c54e69f69438dbb2cc1de1458b"}, + {file = "greenlet-3.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:c82304750f057167ff60d188df1d0cc1764ce9567eadf03e6a7443bcedd0b30b"}, + {file = "greenlet-3.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:dc133a1569ee667b2a6ef56ce551084aeefd87a5acbc4736d336d1e2edc6cfc4"}, + {file = "greenlet-3.5.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117"}, + {file = "greenlet-3.5.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8"}, + {file = "greenlet-3.5.3-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d"}, + {file = "greenlet-3.5.3-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8d19fe6c39ebff9259f07bcc685d3290f8fa4ea2278e51dd0008e4d6b0f2d814"}, + {file = "greenlet-3.5.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c"}, + {file = "greenlet-3.5.3-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:962c5df2db8cb446da51edf1ca5296c389d93b99c9d8aa2ee4c7d0d8f1218260"}, + {file = "greenlet-3.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a"}, + {file = "greenlet-3.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154"}, + {file = "greenlet-3.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e"}, + {file = "greenlet-3.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:5b4807c4082c9d1b6d9eed56fcd041863e37f2228106eef24c30ca096e238605"}, + {file = "greenlet-3.5.3-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be"}, + {file = "greenlet-3.5.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310"}, + {file = "greenlet-3.5.3-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8"}, + {file = "greenlet-3.5.3-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b897d97759425953f69a9c0fac67f8fe333ec0ce7377ef186fb2b0c3ad5e354d"}, + {file = "greenlet-3.5.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f"}, + {file = "greenlet-3.5.3-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:3236754d423955ea08e9bb5f6c04a7895f9e22c290b66aa7653fcb922d839eb0"}, + {file = "greenlet-3.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21"}, + {file = "greenlet-3.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da"}, + {file = "greenlet-3.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3"}, + {file = "greenlet-3.5.3-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc"}, + {file = "greenlet-3.5.3-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47"}, + {file = "greenlet-3.5.3-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81"}, + {file = "greenlet-3.5.3-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cefa9cef4b371f9844c6053db71f1138bc6807bab1578b0dae5149c1f1141357"}, + {file = "greenlet-3.5.3-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d"}, + {file = "greenlet-3.5.3-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:6219b6d04dbf6ba6084d77dc609e8473060dc55f759cbf626d512122781fa128"}, + {file = "greenlet-3.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34"}, + {file = "greenlet-3.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b"}, + {file = "greenlet-3.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930"}, + {file = "greenlet-3.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:499fef2acede88c1864a57bb586b4bf533c81e1b82df7ab93451cdb47dfec227"}, + {file = "greenlet-3.5.3-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c"}, + {file = "greenlet-3.5.3-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f"}, + {file = "greenlet-3.5.3-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2"}, + {file = "greenlet-3.5.3-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d192579ed281051396dddd7f7754dac6259e6b1fb26378c87b66622f8e3f91"}, + {file = "greenlet-3.5.3-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608"}, + {file = "greenlet-3.5.3-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:4399eb8d041f20b68d943918bc55502a93d6fdc0a37c14da7881c04139acee9d"}, + {file = "greenlet-3.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb"}, + {file = "greenlet-3.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16"}, + {file = "greenlet-3.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf"}, + {file = "greenlet-3.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:b7068bd09f761f3f5b4d214c2bed063186b2a86148c740b3873e3f56d79bac31"}, + {file = "greenlet-3.5.3.tar.gz", hash = "sha256:a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1"}, ] [package.extras] docs = ["Sphinx", "furo"] -test = ["objgraph", "psutil"] +test = ["objgraph", "psutil", "setuptools"] [[package]] name = "h11" @@ -1919,56 +2049,79 @@ files = [ [[package]] name = "h5py" -version = "3.10.0" +version = "3.16.0" description = "Read and write HDF5 files from Python" optional = false -python-versions = ">=3.8" -files = [ - {file = "h5py-3.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b963fb772964fc1d1563c57e4e2e874022ce11f75ddc6df1a626f42bd49ab99f"}, - {file = "h5py-3.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:012ab448590e3c4f5a8dd0f3533255bc57f80629bf7c5054cf4c87b30085063c"}, - {file = "h5py-3.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:781a24263c1270a62cd67be59f293e62b76acfcc207afa6384961762bb88ea03"}, - {file = "h5py-3.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f42e6c30698b520f0295d70157c4e202a9e402406f50dc08f5a7bc416b24e52d"}, - {file = "h5py-3.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:93dd840bd675787fc0b016f7a05fc6efe37312a08849d9dd4053fd0377b1357f"}, - {file = "h5py-3.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2381e98af081b6df7f6db300cd88f88e740649d77736e4b53db522d8874bf2dc"}, - {file = "h5py-3.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:667fe23ab33d5a8a6b77970b229e14ae3bb84e4ea3382cc08567a02e1499eedd"}, - {file = "h5py-3.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90286b79abd085e4e65e07c1bd7ee65a0f15818ea107f44b175d2dfe1a4674b7"}, - {file = "h5py-3.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c013d2e79c00f28ffd0cc24e68665ea03ae9069e167087b2adb5727d2736a52"}, - {file = "h5py-3.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:92273ce69ae4983dadb898fd4d3bea5eb90820df953b401282ee69ad648df684"}, - {file = "h5py-3.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c97d03f87f215e7759a354460fb4b0d0f27001450b18b23e556e7856a0b21c3"}, - {file = "h5py-3.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86df4c2de68257b8539a18646ceccdcf2c1ce6b1768ada16c8dcfb489eafae20"}, - {file = "h5py-3.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba9ab36be991119a3ff32d0c7cbe5faf9b8d2375b5278b2aea64effbeba66039"}, - {file = "h5py-3.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c8e4fda19eb769e9a678592e67eaec3a2f069f7570c82d2da909c077aa94339"}, - {file = "h5py-3.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:492305a074327e8d2513011fa9fffeb54ecb28a04ca4c4227d7e1e9616d35641"}, - {file = "h5py-3.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9450464b458cca2c86252b624279115dcaa7260a40d3cb1594bf2b410a2bd1a3"}, - {file = "h5py-3.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd6f6d1384a9f491732cee233b99cd4bfd6e838a8815cc86722f9d2ee64032af"}, - {file = "h5py-3.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3074ec45d3dc6e178c6f96834cf8108bf4a60ccb5ab044e16909580352010a97"}, - {file = "h5py-3.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:212bb997a91e6a895ce5e2f365ba764debeaef5d2dca5c6fb7098d66607adf99"}, - {file = "h5py-3.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5dfc65ac21fa2f630323c92453cadbe8d4f504726ec42f6a56cf80c2f90d6c52"}, - {file = "h5py-3.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d4682b94fd36ab217352be438abd44c8f357c5449b8995e63886b431d260f3d3"}, - {file = "h5py-3.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aece0e2e1ed2aab076c41802e50a0c3e5ef8816d60ece39107d68717d4559824"}, - {file = "h5py-3.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43a61b2c2ad65b1fabc28802d133eed34debcc2c8b420cb213d3d4ef4d3e2229"}, - {file = "h5py-3.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:ae2f0201c950059676455daf92700eeb57dcf5caaf71b9e1328e6e6593601770"}, - {file = "h5py-3.10.0.tar.gz", hash = "sha256:d93adc48ceeb33347eb24a634fb787efc7ae4644e6ea4ba733d099605045c049"}, +python-versions = ">=3.10" +files = [ + {file = "h5py-3.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e06f864bedb2c8e7c1358e6c73af48519e317457c444d6f3d332bb4e8fa6d7d9"}, + {file = "h5py-3.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec86d4fffd87a0f4cb3d5796ceb5a50123a2a6d99b43e616e5504e66a953eca3"}, + {file = "h5py-3.16.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:86385ea895508220b8a7e45efa428aeafaa586bd737c7af9ee04661d8d84a10d"}, + {file = "h5py-3.16.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:8975273c2c5921c25700193b408e28d6bdd0111c37468b2d4e25dcec4cd1d84d"}, + {file = "h5py-3.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1677ad48b703f44efc9ea0c3ab284527f81bc4f318386aaaebc5fede6bbae56f"}, + {file = "h5py-3.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c4dd4cf5f0a4e36083f73172f6cfc25a5710789269547f132a20975bfe2434c"}, + {file = "h5py-3.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:bdef06507725b455fccba9c16529121a5e1fbf56aa375f7d9713d9e8ff42454d"}, + {file = "h5py-3.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:719439d14b83f74eeb080e9650a6c7aa6d0d9ea0ca7f804347b05fac6fbf18af"}, + {file = "h5py-3.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c3f0a0e136f2e95dd0b67146abb6668af4f1a69c81ef8651a2d316e8e01de447"}, + {file = "h5py-3.16.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a6fbc5367d4046801f9b7db9191b31895f22f1c6df1f9987d667854cac493538"}, + {file = "h5py-3.16.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:fb1720028d99040792bb2fb31facb8da44a6f29df7697e0b84f0d79aff2e9bd3"}, + {file = "h5py-3.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:314b6054fe0b1051c2b0cb2df5cbdab15622fb05e80f202e3b6a5eee0d6fe365"}, + {file = "h5py-3.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ffbab2fedd6581f6aa31cf1639ca2cb86e02779de525667892ebf4cc9fd26434"}, + {file = "h5py-3.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:17d1f1630f92ad74494a9a7392ab25982ce2b469fc62da6074c0ce48366a2999"}, + {file = "h5py-3.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:85b9c49dd58dc44cf70af944784e2c2038b6f799665d0dcbbc812a26e0faa859"}, + {file = "h5py-3.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5313566f4643121a78503a473f0fb1e6dcc541d5115c44f05e037609c565c4d"}, + {file = "h5py-3.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:42b012933a83e1a558c673176676a10ce2fd3759976a0fedee1e672d1e04fc9d"}, + {file = "h5py-3.16.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:ff24039e2573297787c3063df64b60aab0591980ac898329a08b0320e0cf2527"}, + {file = "h5py-3.16.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:dfc21898ff025f1e8e67e194965a95a8d4754f452f83454538f98f8a3fcb207e"}, + {file = "h5py-3.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:698dd69291272642ffda44a0ecd6cd3bda5faf9621452d255f57ce91487b9794"}, + {file = "h5py-3.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2b2c02b0a160faed5fb33f1ba8a264a37ee240b22e049ecc827345d0d9043074"}, + {file = "h5py-3.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:96b422019a1c8975c2d5dadcf61d4ba6f01c31f92bbde6e4649607885fe502d6"}, + {file = "h5py-3.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:39c2838fb1e8d97bcf1755e60ad1f3dd76a7b2a475928dc321672752678b96db"}, + {file = "h5py-3.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:370a845f432c2c9619db8eed334d1e610c6015796122b0e57aa46312c22617d9"}, + {file = "h5py-3.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42108e93326c50c2810025aade9eac9d6827524cdccc7d4b75a546e5ab308edb"}, + {file = "h5py-3.16.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:099f2525c9dcf28de366970a5fb34879aab20491589fa89ce2863a84218bb524"}, + {file = "h5py-3.16.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9300ad32dea9dfc5171f94d5f6948e159ed93e4701280b0f508773b3f582f402"}, + {file = "h5py-3.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:171038f23bccddfc23f344cadabdfc9917ff554db6a0d417180d2747fe4c75a7"}, + {file = "h5py-3.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7e420b539fb6023a259a1b14d4c9f6df8cf50d7268f48e161169987a57b737ff"}, + {file = "h5py-3.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:18f2bbcd545e6991412253b98727374c356d67caa920e68dc79eab36bf5fedad"}, + {file = "h5py-3.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:656f00e4d903199a1d58df06b711cf3ca632b874b4207b7dbec86185b5c8c7d4"}, + {file = "h5py-3.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9c9d307c0ef862d1cd5714f72ecfafe0a5d7529c44845afa8de9f46e5ba8bd65"}, + {file = "h5py-3.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8c1eff849cdd53cbc73c214c30ebdb6f1bb8b64790b4b4fc36acdb5e43570210"}, + {file = "h5py-3.16.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:e2c04d129f180019e216ee5f9c40b78a418634091c8782e1f723a6ca3658b965"}, + {file = "h5py-3.16.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e4360f15875a532bc7b98196c7592ed4fc92672a57c0a621355961cafb17a6dd"}, + {file = "h5py-3.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3fae9197390c325e62e0a1aa977f2f62d994aa87aab182abbea85479b791197c"}, + {file = "h5py-3.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:43259303989ac8adacc9986695b31e35dba6fd1e297ff9c6a04b7da5542139cc"}, + {file = "h5py-3.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:fa48993a0b799737ba7fd21e2350fa0a60701e58180fae9f2de834bc39a147ab"}, + {file = "h5py-3.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:1897a771a7f40d05c262fc8f37376ec37873218544b70216872876c627640f63"}, + {file = "h5py-3.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:15922e485844f77c0b9d275396d435db3baa58292a9c2176a386e072e0cf2491"}, + {file = "h5py-3.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:df02dd29bd247f98674634dfe41f89fd7c16ba3d7de8695ec958f58404a4e618"}, + {file = "h5py-3.16.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:0f456f556e4e2cebeebd9d66adf8dc321770a42593494a0b6f0af54a7567b242"}, + {file = "h5py-3.16.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:3e6cb3387c756de6a9492d601553dffea3fe11b5f22b443aac708c69f3f55e16"}, + {file = "h5py-3.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8389e13a1fd745ad2856873e8187fd10268b2d9677877bb667b41aebd771d8b7"}, + {file = "h5py-3.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:346df559a0f7dcb31cf8e44805319e2ab24b8957c45e7708ce503b2ec79ba725"}, + {file = "h5py-3.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4c6ab014ab704b4feaa719ae783b86522ed0bf1f82184704ed3c9e4e3228796e"}, + {file = "h5py-3.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:faca8fb4e4319c09d83337adc80b2ca7d5c5a343c2d6f1b6388f32cfecca13c1"}, + {file = "h5py-3.16.0.tar.gz", hash = "sha256:a0dbaad796840ccaa67a4c144a0d0c8080073c34c76d5a6941d6818678ef2738"}, ] [package.dependencies] -numpy = ">=1.17.3" +numpy = ">=1.21.2" [[package]] name = "hexbytes" -version = "1.2.1" +version = "1.3.1" description = "hexbytes: Python `bytes` subclass that decodes hex, with a readable console output" optional = false python-versions = "<4,>=3.8" files = [ - {file = "hexbytes-1.2.1-py3-none-any.whl", hash = "sha256:e64890b203a31f4a23ef11470ecfcca565beaee9198df623047df322b757471a"}, - {file = "hexbytes-1.2.1.tar.gz", hash = "sha256:515f00dddf31053db4d0d7636dd16061c1d896c3109b8e751005db4ca46bcca7"}, + {file = "hexbytes-1.3.1-py3-none-any.whl", hash = "sha256:da01ff24a1a9a2b1881c4b85f0e9f9b0f51b526b379ffa23832ae7899d29c2c7"}, + {file = "hexbytes-1.3.1.tar.gz", hash = "sha256:a657eebebdfe27254336f98d8af6e2236f3f83aed164b87466b6cf6c5f5a4765"}, ] [package.extras] -dev = ["build (>=0.9.0)", "bump-my-version (>=0.19.0)", "eth-utils (>=2.0.0)", "hypothesis (>=3.44.24,<=6.31.6)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)", "tox (>=4.0.0)", "twine", "wheel"] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)"] -test = ["eth-utils (>=2.0.0)", "hypothesis (>=3.44.24,<=6.31.6)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "eth_utils (>=2.0.0)", "hypothesis (>=3.44.24)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +test = ["eth_utils (>=2.0.0)", "hypothesis (>=3.44.24)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] [[package]] name = "httpcore" @@ -1993,59 +2146,63 @@ socks = ["socksio (==1.*)"] [[package]] name = "httptools" -version = "0.6.4" +version = "0.8.0" description = "A collection of framework independent HTTP protocol utils." optional = false -python-versions = ">=3.8.0" -files = [ - {file = "httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0"}, - {file = "httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da"}, - {file = "httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1"}, - {file = "httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50"}, - {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959"}, - {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4"}, - {file = "httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c"}, - {file = "httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069"}, - {file = "httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a"}, - {file = "httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975"}, - {file = "httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636"}, - {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721"}, - {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988"}, - {file = "httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17"}, - {file = "httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2"}, - {file = "httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44"}, - {file = "httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1"}, - {file = "httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2"}, - {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81"}, - {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f"}, - {file = "httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970"}, - {file = "httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660"}, - {file = "httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083"}, - {file = "httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3"}, - {file = "httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071"}, - {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5"}, - {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0"}, - {file = "httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8"}, - {file = "httptools-0.6.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d3f0d369e7ffbe59c4b6116a44d6a8eb4783aae027f2c0b366cf0aa964185dba"}, - {file = "httptools-0.6.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:94978a49b8f4569ad607cd4946b759d90b285e39c0d4640c6b36ca7a3ddf2efc"}, - {file = "httptools-0.6.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40dc6a8e399e15ea525305a2ddba998b0af5caa2566bcd79dcbe8948181eeaff"}, - {file = "httptools-0.6.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab9ba8dcf59de5181f6be44a77458e45a578fc99c31510b8c65b7d5acc3cf490"}, - {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc411e1c0a7dcd2f902c7c48cf079947a7e65b5485dea9decb82b9105ca71a43"}, - {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d54efd20338ac52ba31e7da78e4a72570cf729fac82bc31ff9199bedf1dc7440"}, - {file = "httptools-0.6.4-cp38-cp38-win_amd64.whl", hash = "sha256:df959752a0c2748a65ab5387d08287abf6779ae9165916fe053e68ae1fbdc47f"}, - {file = "httptools-0.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85797e37e8eeaa5439d33e556662cc370e474445d5fab24dcadc65a8ffb04003"}, - {file = "httptools-0.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:db353d22843cf1028f43c3651581e4bb49374d85692a85f95f7b9a130e1b2cab"}, - {file = "httptools-0.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ffd262a73d7c28424252381a5b854c19d9de5f56f075445d33919a637e3547"}, - {file = "httptools-0.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703c346571fa50d2e9856a37d7cd9435a25e7fd15e236c397bf224afaa355fe9"}, - {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aafe0f1918ed07b67c1e838f950b1c1fabc683030477e60b335649b8020e1076"}, - {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0e563e54979e97b6d13f1bbc05a96109923e76b901f786a5eae36e99c01237bd"}, - {file = "httptools-0.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:b799de31416ecc589ad79dd85a0b2657a8fe39327944998dea368c1d4c9e55e6"}, - {file = "httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c"}, +python-versions = ">=3.9" +files = [ + {file = "httptools-0.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bf3b6f807c8541503cecfbb8a8dffb385640d0d96102f3d112aa8740f9b7c826"}, + {file = "httptools-0.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da684f2e1aa2ee9bdcb083f3f3a68c5956750b375bc5df864d3a5f0c42a40b77"}, + {file = "httptools-0.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6f21e2a3b0067bbe7f67e34cfd16276af556e5e52f4c7503be0cb5f90e905e4"}, + {file = "httptools-0.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea897f0c729581ebf72131a438a7932d9b14efef72d75ada966700cac3caaeb"}, + {file = "httptools-0.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c0d726cc107fceb7d45f978483b4b70dd8caa836f5914d3434bb18628eb73813"}, + {file = "httptools-0.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9878eb2785ba5eb70631ad269b37976f73d647955e26c91d490eb8a4edfda4ba"}, + {file = "httptools-0.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:b205e5f5523fa039679da0dfe5a10132b2a4abeae6a86fdd1ddc035f7f836557"}, + {file = "httptools-0.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168"}, + {file = "httptools-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d"}, + {file = "httptools-0.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376"}, + {file = "httptools-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d"}, + {file = "httptools-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085"}, + {file = "httptools-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124"}, + {file = "httptools-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07"}, + {file = "httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d"}, + {file = "httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5"}, + {file = "httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2"}, + {file = "httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09"}, + {file = "httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a"}, + {file = "httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745"}, + {file = "httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150"}, + {file = "httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8"}, + {file = "httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c"}, + {file = "httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7"}, + {file = "httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d"}, + {file = "httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681"}, + {file = "httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683"}, + {file = "httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1"}, + {file = "httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6"}, + {file = "httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b"}, + {file = "httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0"}, + {file = "httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e"}, + {file = "httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b"}, + {file = "httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0"}, + {file = "httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527"}, + {file = "httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568"}, + {file = "httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b"}, + {file = "httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca"}, + {file = "httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f"}, + {file = "httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d"}, + {file = "httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081"}, + {file = "httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77"}, + {file = "httptools-0.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:df31ef5494f406ab6cf827b7e64a22841c6e2d654100e6a116ea15b46d02d5e8"}, + {file = "httptools-0.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5eb911c515b96ee44bbd861e42cbefc488681d450545b1d02127f6136e3a86f5"}, + {file = "httptools-0.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c08ffe3e79756e0963cbc8fe410139f38a5884874b6f2e17761bef6563fdcd9b"}, + {file = "httptools-0.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe2a4c95aeba2209434e7b31172da572846cae8ca0bf1e7013e61b99fbbf5e72"}, + {file = "httptools-0.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7b71e7d7031928c650e1006e6c03e911bf967f7c69c011d37d541c3e7bf55005"}, + {file = "httptools-0.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9fc1644f415372cec4f8a5be3a64183737398f10dbb1263602a036427fe75247"}, + {file = "httptools-0.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:5d7fa4ba7292c1139c0526f0b5aad507c6263c948206ea1b1cbca015c8af1b62"}, + {file = "httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999"}, ] -[package.extras] -test = ["Cython (>=0.29.24)"] - [[package]] name = "httpx" version = "0.24.1" @@ -2093,13 +2250,13 @@ agreement = ["numpy", "pyerf"] [[package]] name = "identify" -version = "2.5.33" +version = "2.6.19" description = "File identification library for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" files = [ - {file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"}, - {file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"}, + {file = "identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a"}, + {file = "identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842"}, ] [package.extras] @@ -2107,35 +2264,57 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.6" +version = "3.18" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.9" files = [ - {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, - {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, + {file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"}, + {file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"}, ] +[package.extras] +all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "importlib-resources" +version = "7.1.0" +description = "Read resources from Python packages" +optional = false +python-versions = ">=3.10" +files = [ + {file = "importlib_resources-7.1.0-py3-none-any.whl", hash = "sha256:1bd7b48b4088eddb2cd16382150bb515af0bd2c70128194392725f82ad2c96a1"}, + {file = "importlib_resources-7.1.0.tar.gz", hash = "sha256:0722d4c6212489c530f2a145a34c0a7a3b4721bc96a15fada5930e2a0b760708"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.14)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"] +type = ["pytest-mypy (>=1.0.1)"] + [[package]] name = "iniconfig" -version = "2.0.0" +version = "2.3.0" description = "brain-dead simple config-ini parsing" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, ] [[package]] name = "jinja2" -version = "3.1.4" +version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ - {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, - {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, ] [package.dependencies] @@ -2146,230 +2325,299 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "jmespath" -version = "1.0.1" +version = "1.1.0" description = "JSON Matching Expressions" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, - {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, + {file = "jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64"}, + {file = "jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d"}, ] [[package]] name = "kiwisolver" -version = "1.4.5" +version = "1.5.0" description = "A fast implementation of the Cassowary constraint solver" optional = false -python-versions = ">=3.7" -files = [ - {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af"}, - {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3"}, - {file = "kiwisolver-1.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ef7afcd2d281494c0a9101d5c571970708ad911d028137cd558f02b851c08b4"}, - {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9eaa8b117dc8337728e834b9c6e2611f10c79e38f65157c4c38e9400286f5cb1"}, - {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec20916e7b4cbfb1f12380e46486ec4bcbaa91a9c448b97023fde0d5bbf9e4ff"}, - {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b42c68602539407884cf70d6a480a469b93b81b7701378ba5e2328660c847a"}, - {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa12042de0171fad672b6c59df69106d20d5596e4f87b5e8f76df757a7c399aa"}, - {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a40773c71d7ccdd3798f6489aaac9eee213d566850a9533f8d26332d626b82c"}, - {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:19df6e621f6d8b4b9c4d45f40a66839294ff2bb235e64d2178f7522d9170ac5b"}, - {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:83d78376d0d4fd884e2c114d0621624b73d2aba4e2788182d286309ebdeed770"}, - {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e391b1f0a8a5a10ab3b9bb6afcfd74f2175f24f8975fb87ecae700d1503cdee0"}, - {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:852542f9481f4a62dbb5dd99e8ab7aedfeb8fb6342349a181d4036877410f525"}, - {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59edc41b24031bc25108e210c0def6f6c2191210492a972d585a06ff246bb79b"}, - {file = "kiwisolver-1.4.5-cp310-cp310-win32.whl", hash = "sha256:a6aa6315319a052b4ee378aa171959c898a6183f15c1e541821c5c59beaa0238"}, - {file = "kiwisolver-1.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:d0ef46024e6a3d79c01ff13801cb19d0cad7fd859b15037aec74315540acc276"}, - {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5"}, - {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90"}, - {file = "kiwisolver-1.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797"}, - {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfdd7c0b105af050eb3d64997809dc21da247cf44e63dc73ff0fd20b96be55a9"}, - {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c6a5964640638cdeaa0c359382e5703e9293030fe730018ca06bc2010c4437"}, - {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbea0db94288e29afcc4c28afbf3a7ccaf2d7e027489c449cf7e8f83c6346eb9"}, - {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ceec1a6bc6cab1d6ff5d06592a91a692f90ec7505d6463a88a52cc0eb58545da"}, - {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:040c1aebeda72197ef477a906782b5ab0d387642e93bda547336b8957c61022e"}, - {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f91de7223d4c7b793867797bacd1ee53bfe7359bd70d27b7b58a04efbb9436c8"}, - {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:faae4860798c31530dd184046a900e652c95513796ef51a12bc086710c2eec4d"}, - {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0157420efcb803e71d1b28e2c287518b8808b7cf1ab8af36718fd0a2c453eb0"}, - {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:06f54715b7737c2fecdbf140d1afb11a33d59508a47bf11bb38ecf21dc9ab79f"}, - {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fdb7adb641a0d13bdcd4ef48e062363d8a9ad4a182ac7647ec88f695e719ae9f"}, - {file = "kiwisolver-1.4.5-cp311-cp311-win32.whl", hash = "sha256:bb86433b1cfe686da83ce32a9d3a8dd308e85c76b60896d58f082136f10bffac"}, - {file = "kiwisolver-1.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c08e1312a9cf1074d17b17728d3dfce2a5125b2d791527f33ffbe805200a355"}, - {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:32d5cf40c4f7c7b3ca500f8985eb3fb3a7dfc023215e876f207956b5ea26632a"}, - {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f846c260f483d1fd217fe5ed7c173fb109efa6b1fc8381c8b7552c5781756192"}, - {file = "kiwisolver-1.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ff5cf3571589b6d13bfbfd6bcd7a3f659e42f96b5fd1c4830c4cf21d4f5ef45"}, - {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7269d9e5f1084a653d575c7ec012ff57f0c042258bf5db0954bf551c158466e7"}, - {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da802a19d6e15dffe4b0c24b38b3af68e6c1a68e6e1d8f30148c83864f3881db"}, - {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3aba7311af82e335dd1e36ffff68aaca609ca6290c2cb6d821a39aa075d8e3ff"}, - {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763773d53f07244148ccac5b084da5adb90bfaee39c197554f01b286cf869228"}, - {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2270953c0d8cdab5d422bee7d2007f043473f9d2999631c86a223c9db56cbd16"}, - {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d099e745a512f7e3bbe7249ca835f4d357c586d78d79ae8f1dcd4d8adeb9bda9"}, - {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:74db36e14a7d1ce0986fa104f7d5637aea5c82ca6326ed0ec5694280942d1162"}, - {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e5bab140c309cb3a6ce373a9e71eb7e4873c70c2dda01df6820474f9889d6d4"}, - {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0f114aa76dc1b8f636d077979c0ac22e7cd8f3493abbab152f20eb8d3cda71f3"}, - {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:88a2df29d4724b9237fc0c6eaf2a1adae0cdc0b3e9f4d8e7dc54b16812d2d81a"}, - {file = "kiwisolver-1.4.5-cp312-cp312-win32.whl", hash = "sha256:72d40b33e834371fd330fb1472ca19d9b8327acb79a5821d4008391db8e29f20"}, - {file = "kiwisolver-1.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:2c5674c4e74d939b9d91dda0fae10597ac7521768fec9e399c70a1f27e2ea2d9"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3a2b053a0ab7a3960c98725cfb0bf5b48ba82f64ec95fe06f1d06c99b552e130"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cd32d6c13807e5c66a7cbb79f90b553642f296ae4518a60d8d76243b0ad2898"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59ec7b7c7e1a61061850d53aaf8e93db63dce0c936db1fda2658b70e4a1be709"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da4cfb373035def307905d05041c1d06d8936452fe89d464743ae7fb8371078b"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2400873bccc260b6ae184b2b8a4fec0e4082d30648eadb7c3d9a13405d861e89"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1b04139c4236a0f3aff534479b58f6f849a8b351e1314826c2d230849ed48985"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4e66e81a5779b65ac21764c295087de82235597a2293d18d943f8e9e32746265"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7931d8f1f67c4be9ba1dd9c451fb0eeca1a25b89e4d3f89e828fe12a519b782a"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b3f7e75f3015df442238cca659f8baa5f42ce2a8582727981cbfa15fee0ee205"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:bbf1d63eef84b2e8c89011b7f2235b1e0bf7dacc11cac9431fc6468e99ac77fb"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4c380469bd3f970ef677bf2bcba2b6b0b4d5c75e7a020fb863ef75084efad66f"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-win32.whl", hash = "sha256:9408acf3270c4b6baad483865191e3e582b638b1654a007c62e3efe96f09a9a3"}, - {file = "kiwisolver-1.4.5-cp37-cp37m-win_amd64.whl", hash = "sha256:5b94529f9b2591b7af5f3e0e730a4e0a41ea174af35a4fd067775f9bdfeee01a"}, - {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:11c7de8f692fc99816e8ac50d1d1aef4f75126eefc33ac79aac02c099fd3db71"}, - {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:53abb58632235cd154176ced1ae8f0d29a6657aa1aa9decf50b899b755bc2b93"}, - {file = "kiwisolver-1.4.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:88b9f257ca61b838b6f8094a62418421f87ac2a1069f7e896c36a7d86b5d4c29"}, - {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3195782b26fc03aa9c6913d5bad5aeb864bdc372924c093b0f1cebad603dd712"}, - {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc579bf0f502e54926519451b920e875f433aceb4624a3646b3252b5caa9e0b6"}, - {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a580c91d686376f0f7c295357595c5a026e6cbc3d77b7c36e290201e7c11ecb"}, - {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cfe6ab8da05c01ba6fbea630377b5da2cd9bcbc6338510116b01c1bc939a2c18"}, - {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d2e5a98f0ec99beb3c10e13b387f8db39106d53993f498b295f0c914328b1333"}, - {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a51a263952b1429e429ff236d2f5a21c5125437861baeed77f5e1cc2d2c7c6da"}, - {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3edd2fa14e68c9be82c5b16689e8d63d89fe927e56debd6e1dbce7a26a17f81b"}, - {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:74d1b44c6cfc897df648cc9fdaa09bc3e7679926e6f96df05775d4fb3946571c"}, - {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:76d9289ed3f7501012e05abb8358bbb129149dbd173f1f57a1bf1c22d19ab7cc"}, - {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:92dea1ffe3714fa8eb6a314d2b3c773208d865a0e0d35e713ec54eea08a66250"}, - {file = "kiwisolver-1.4.5-cp38-cp38-win32.whl", hash = "sha256:5c90ae8c8d32e472be041e76f9d2f2dbff4d0b0be8bd4041770eddb18cf49a4e"}, - {file = "kiwisolver-1.4.5-cp38-cp38-win_amd64.whl", hash = "sha256:c7940c1dc63eb37a67721b10d703247552416f719c4188c54e04334321351ced"}, - {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9407b6a5f0d675e8a827ad8742e1d6b49d9c1a1da5d952a67d50ef5f4170b18d"}, - {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15568384086b6df3c65353820a4473575dbad192e35010f622c6ce3eebd57af9"}, - {file = "kiwisolver-1.4.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0dc9db8e79f0036e8173c466d21ef18e1befc02de8bf8aa8dc0813a6dc8a7046"}, - {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cdc8a402aaee9a798b50d8b827d7ecf75edc5fb35ea0f91f213ff927c15f4ff0"}, - {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6c3bd3cde54cafb87d74d8db50b909705c62b17c2099b8f2e25b461882e544ff"}, - {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:955e8513d07a283056b1396e9a57ceddbd272d9252c14f154d450d227606eb54"}, - {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:346f5343b9e3f00b8db8ba359350eb124b98c99efd0b408728ac6ebf38173958"}, - {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9098e0049e88c6a24ff64545cdfc50807818ba6c1b739cae221bbbcbc58aad3"}, - {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:00bd361b903dc4bbf4eb165f24d1acbee754fce22ded24c3d56eec268658a5cf"}, - {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7b8b454bac16428b22560d0a1cf0a09875339cab69df61d7805bf48919415901"}, - {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f1d072c2eb0ad60d4c183f3fb44ac6f73fb7a8f16a2694a91f988275cbf352f9"}, - {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:31a82d498054cac9f6d0b53d02bb85811185bcb477d4b60144f915f3b3126342"}, - {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6512cb89e334e4700febbffaaa52761b65b4f5a3cf33f960213d5656cea36a77"}, - {file = "kiwisolver-1.4.5-cp39-cp39-win32.whl", hash = "sha256:9db8ea4c388fdb0f780fe91346fd438657ea602d58348753d9fb265ce1bca67f"}, - {file = "kiwisolver-1.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:59415f46a37f7f2efeec758353dd2eae1b07640d8ca0f0c42548ec4125492635"}, - {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5c7b3b3a728dc6faf3fc372ef24f21d1e3cee2ac3e9596691d746e5a536de920"}, - {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:620ced262a86244e2be10a676b646f29c34537d0d9cc8eb26c08f53d98013390"}, - {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:378a214a1e3bbf5ac4a8708304318b4f890da88c9e6a07699c4ae7174c09a68d"}, - {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf7be1207676ac608a50cd08f102f6742dbfc70e8d60c4db1c6897f62f71523"}, - {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ba55dce0a9b8ff59495ddd050a0225d58bd0983d09f87cfe2b6aec4f2c1234e4"}, - {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd32ea360bcbb92d28933fc05ed09bffcb1704ba3fc7942e81db0fd4f81a7892"}, - {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5e7139af55d1688f8b960ee9ad5adafc4ac17c1c473fe07133ac092310d76544"}, - {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dced8146011d2bc2e883f9bd68618b8247387f4bbec46d7392b3c3b032640126"}, - {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9bf3325c47b11b2e51bca0824ea217c7cd84491d8ac4eefd1e409705ef092bd"}, - {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5794cf59533bc3f1b1c821f7206a3617999db9fbefc345360aafe2e067514929"}, - {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e368f200bbc2e4f905b8e71eb38b3c04333bddaa6a2464a6355487b02bb7fb09"}, - {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5d706eba36b4c4d5bc6c6377bb6568098765e990cfc21ee16d13963fab7b3e7"}, - {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85267bd1aa8880a9c88a8cb71e18d3d64d2751a790e6ca6c27b8ccc724bcd5ad"}, - {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210ef2c3a1f03272649aff1ef992df2e724748918c4bc2d5a90352849eb40bea"}, - {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11d011a7574eb3b82bcc9c1a1d35c1d7075677fdd15de527d91b46bd35e935ee"}, - {file = "kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec"}, +python-versions = ">=3.10" +files = [ + {file = "kiwisolver-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374"}, + {file = "kiwisolver-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd"}, + {file = "kiwisolver-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476"}, + {file = "kiwisolver-1.5.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22"}, + {file = "kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b"}, + {file = "kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e"}, + {file = "kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb"}, + {file = "kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537"}, + {file = "kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4"}, + {file = "kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c"}, + {file = "kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede"}, + {file = "kiwisolver-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2"}, + {file = "kiwisolver-1.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875"}, + {file = "kiwisolver-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c"}, + {file = "kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb"}, + {file = "kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac"}, + {file = "kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27"}, + {file = "kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398"}, + {file = "kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db"}, + {file = "kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc"}, + {file = "kiwisolver-1.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679"}, + {file = "kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309"}, + {file = "kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2"}, + {file = "kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c"}, + {file = "kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08"}, + {file = "kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4"}, + {file = "kiwisolver-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b"}, + {file = "kiwisolver-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac"}, + {file = "kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9"}, + {file = "kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588"}, + {file = "kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819"}, + {file = "kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f"}, + {file = "kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf"}, + {file = "kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d"}, + {file = "kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083"}, + {file = "kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6"}, + {file = "kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1"}, + {file = "kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0"}, + {file = "kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15"}, + {file = "kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314"}, + {file = "kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9"}, + {file = "kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384"}, + {file = "kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7"}, + {file = "kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09"}, + {file = "kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3"}, + {file = "kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd"}, + {file = "kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3"}, + {file = "kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96"}, + {file = "kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099"}, + {file = "kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8"}, + {file = "kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87"}, + {file = "kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23"}, + {file = "kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859"}, + {file = "kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902"}, + {file = "kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167"}, + {file = "kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0"}, + {file = "kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276"}, + {file = "kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53"}, + {file = "kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615"}, + {file = "kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02"}, + {file = "kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e"}, + {file = "kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac"}, + {file = "kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05"}, + {file = "kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd"}, + {file = "kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a"}, + {file = "kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554"}, + {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581"}, + {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303"}, + {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9"}, + {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79"}, + {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796"}, + {file = "kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e"}, + {file = "kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57"}, + {file = "kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797"}, + {file = "kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203"}, + {file = "kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7"}, + {file = "kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57"}, + {file = "kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4"}, + {file = "kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca"}, + {file = "kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f"}, + {file = "kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed"}, + {file = "kiwisolver-1.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc"}, + {file = "kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232"}, + {file = "kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a"}, + {file = "kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737"}, + {file = "kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16"}, + {file = "kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1"}, + {file = "kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a"}, ] [[package]] name = "lxml" -version = "5.1.0" +version = "6.1.1" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "lxml-5.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:704f5572ff473a5f897745abebc6df40f22d4133c1e0a1f124e4f2bd3330ff7e"}, - {file = "lxml-5.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d3c0f8567ffe7502d969c2c1b809892dc793b5d0665f602aad19895f8d508da"}, - {file = "lxml-5.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5fcfbebdb0c5d8d18b84118842f31965d59ee3e66996ac842e21f957eb76138c"}, - {file = "lxml-5.1.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f37c6d7106a9d6f0708d4e164b707037b7380fcd0b04c5bd9cae1fb46a856fb"}, - {file = "lxml-5.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2befa20a13f1a75c751f47e00929fb3433d67eb9923c2c0b364de449121f447c"}, - {file = "lxml-5.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22b7ee4c35f374e2c20337a95502057964d7e35b996b1c667b5c65c567d2252a"}, - {file = "lxml-5.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bf8443781533b8d37b295016a4b53c1494fa9a03573c09ca5104550c138d5c05"}, - {file = "lxml-5.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:82bddf0e72cb2af3cbba7cec1d2fd11fda0de6be8f4492223d4a268713ef2147"}, - {file = "lxml-5.1.0-cp310-cp310-win32.whl", hash = "sha256:b66aa6357b265670bb574f050ffceefb98549c721cf28351b748be1ef9577d93"}, - {file = "lxml-5.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:4946e7f59b7b6a9e27bef34422f645e9a368cb2be11bf1ef3cafc39a1f6ba68d"}, - {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:14deca1460b4b0f6b01f1ddc9557704e8b365f55c63070463f6c18619ebf964f"}, - {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ed8c3d2cd329bf779b7ed38db176738f3f8be637bb395ce9629fc76f78afe3d4"}, - {file = "lxml-5.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:436a943c2900bb98123b06437cdd30580a61340fbdb7b28aaf345a459c19046a"}, - {file = "lxml-5.1.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acb6b2f96f60f70e7f34efe0c3ea34ca63f19ca63ce90019c6cbca6b676e81fa"}, - {file = "lxml-5.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af8920ce4a55ff41167ddbc20077f5698c2e710ad3353d32a07d3264f3a2021e"}, - {file = "lxml-5.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cfced4a069003d8913408e10ca8ed092c49a7f6cefee9bb74b6b3e860683b45"}, - {file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9e5ac3437746189a9b4121db2a7b86056ac8786b12e88838696899328fc44bb2"}, - {file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4c9bda132ad108b387c33fabfea47866af87f4ea6ffb79418004f0521e63204"}, - {file = "lxml-5.1.0-cp311-cp311-win32.whl", hash = "sha256:bc64d1b1dab08f679fb89c368f4c05693f58a9faf744c4d390d7ed1d8223869b"}, - {file = "lxml-5.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5ab722ae5a873d8dcee1f5f45ddd93c34210aed44ff2dc643b5025981908cda"}, - {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9aa543980ab1fbf1720969af1d99095a548ea42e00361e727c58a40832439114"}, - {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6f11b77ec0979f7e4dc5ae081325a2946f1fe424148d3945f943ceaede98adb8"}, - {file = "lxml-5.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a36c506e5f8aeb40680491d39ed94670487ce6614b9d27cabe45d94cd5d63e1e"}, - {file = "lxml-5.1.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f643ffd2669ffd4b5a3e9b41c909b72b2a1d5e4915da90a77e119b8d48ce867a"}, - {file = "lxml-5.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16dd953fb719f0ffc5bc067428fc9e88f599e15723a85618c45847c96f11f431"}, - {file = "lxml-5.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16018f7099245157564d7148165132c70adb272fb5a17c048ba70d9cc542a1a1"}, - {file = "lxml-5.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:82cd34f1081ae4ea2ede3d52f71b7be313756e99b4b5f829f89b12da552d3aa3"}, - {file = "lxml-5.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:19a1bc898ae9f06bccb7c3e1dfd73897ecbbd2c96afe9095a6026016e5ca97b8"}, - {file = "lxml-5.1.0-cp312-cp312-win32.whl", hash = "sha256:13521a321a25c641b9ea127ef478b580b5ec82aa2e9fc076c86169d161798b01"}, - {file = "lxml-5.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:1ad17c20e3666c035db502c78b86e58ff6b5991906e55bdbef94977700c72623"}, - {file = "lxml-5.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:24ef5a4631c0b6cceaf2dbca21687e29725b7c4e171f33a8f8ce23c12558ded1"}, - {file = "lxml-5.1.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d2900b7f5318bc7ad8631d3d40190b95ef2aa8cc59473b73b294e4a55e9f30f"}, - {file = "lxml-5.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:601f4a75797d7a770daed8b42b97cd1bb1ba18bd51a9382077a6a247a12aa38d"}, - {file = "lxml-5.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4b68c961b5cc402cbd99cca5eb2547e46ce77260eb705f4d117fd9c3f932b95"}, - {file = "lxml-5.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:afd825e30f8d1f521713a5669b63657bcfe5980a916c95855060048b88e1adb7"}, - {file = "lxml-5.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:262bc5f512a66b527d026518507e78c2f9c2bd9eb5c8aeeb9f0eb43fcb69dc67"}, - {file = "lxml-5.1.0-cp36-cp36m-win32.whl", hash = "sha256:e856c1c7255c739434489ec9c8aa9cdf5179785d10ff20add308b5d673bed5cd"}, - {file = "lxml-5.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c7257171bb8d4432fe9d6fdde4d55fdbe663a63636a17f7f9aaba9bcb3153ad7"}, - {file = "lxml-5.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b9e240ae0ba96477682aa87899d94ddec1cc7926f9df29b1dd57b39e797d5ab5"}, - {file = "lxml-5.1.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a96f02ba1bcd330807fc060ed91d1f7a20853da6dd449e5da4b09bfcc08fdcf5"}, - {file = "lxml-5.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3898ae2b58eeafedfe99e542a17859017d72d7f6a63de0f04f99c2cb125936"}, - {file = "lxml-5.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61c5a7edbd7c695e54fca029ceb351fc45cd8860119a0f83e48be44e1c464862"}, - {file = "lxml-5.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3aeca824b38ca78d9ee2ab82bd9883083d0492d9d17df065ba3b94e88e4d7ee6"}, - {file = "lxml-5.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8f52fe6859b9db71ee609b0c0a70fea5f1e71c3462ecf144ca800d3f434f0764"}, - {file = "lxml-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:d42e3a3fc18acc88b838efded0e6ec3edf3e328a58c68fbd36a7263a874906c8"}, - {file = "lxml-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:eac68f96539b32fce2c9b47eb7c25bb2582bdaf1bbb360d25f564ee9e04c542b"}, - {file = "lxml-5.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ae15347a88cf8af0949a9872b57a320d2605ae069bcdf047677318bc0bba45b1"}, - {file = "lxml-5.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c26aab6ea9c54d3bed716b8851c8bfc40cb249b8e9880e250d1eddde9f709bf5"}, - {file = "lxml-5.1.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:342e95bddec3a698ac24378d61996b3ee5ba9acfeb253986002ac53c9a5f6f84"}, - {file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:725e171e0b99a66ec8605ac77fa12239dbe061482ac854d25720e2294652eeaa"}, - {file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d184e0d5c918cff04cdde9dbdf9600e960161d773666958c9d7b565ccc60c45"}, - {file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:98f3f020a2b736566c707c8e034945c02aa94e124c24f77ca097c446f81b01f1"}, - {file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d48fc57e7c1e3df57be5ae8614bab6d4e7b60f65c5457915c26892c41afc59e"}, - {file = "lxml-5.1.0-cp38-cp38-win32.whl", hash = "sha256:7ec465e6549ed97e9f1e5ed51c657c9ede767bc1c11552f7f4d022c4df4a977a"}, - {file = "lxml-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:b21b4031b53d25b0858d4e124f2f9131ffc1530431c6d1321805c90da78388d1"}, - {file = "lxml-5.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:52427a7eadc98f9e62cb1368a5079ae826f94f05755d2d567d93ee1bc3ceb354"}, - {file = "lxml-5.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6a2a2c724d97c1eb8cf966b16ca2915566a4904b9aad2ed9a09c748ffe14f969"}, - {file = "lxml-5.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:843b9c835580d52828d8f69ea4302537337a21e6b4f1ec711a52241ba4a824f3"}, - {file = "lxml-5.1.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b99f564659cfa704a2dd82d0684207b1aadf7d02d33e54845f9fc78e06b7581"}, - {file = "lxml-5.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8b0c78e7aac24979ef09b7f50da871c2de2def043d468c4b41f512d831e912"}, - {file = "lxml-5.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bcf86dfc8ff3e992fed847c077bd875d9e0ba2fa25d859c3a0f0f76f07f0c8d"}, - {file = "lxml-5.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:49a9b4af45e8b925e1cd6f3b15bbba2c81e7dba6dce170c677c9cda547411e14"}, - {file = "lxml-5.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:280f3edf15c2a967d923bcfb1f8f15337ad36f93525828b40a0f9d6c2ad24890"}, - {file = "lxml-5.1.0-cp39-cp39-win32.whl", hash = "sha256:ed7326563024b6e91fef6b6c7a1a2ff0a71b97793ac33dbbcf38f6005e51ff6e"}, - {file = "lxml-5.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:8d7b4beebb178e9183138f552238f7e6613162a42164233e2bda00cb3afac58f"}, - {file = "lxml-5.1.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9bd0ae7cc2b85320abd5e0abad5ccee5564ed5f0cc90245d2f9a8ef330a8deae"}, - {file = "lxml-5.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c1d679df4361408b628f42b26a5d62bd3e9ba7f0c0e7969f925021554755aa"}, - {file = "lxml-5.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2ad3a8ce9e8a767131061a22cd28fdffa3cd2dc193f399ff7b81777f3520e372"}, - {file = "lxml-5.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:304128394c9c22b6569eba2a6d98392b56fbdfbad58f83ea702530be80d0f9df"}, - {file = "lxml-5.1.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d74fcaf87132ffc0447b3c685a9f862ffb5b43e70ea6beec2fb8057d5d2a1fea"}, - {file = "lxml-5.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:8cf5877f7ed384dabfdcc37922c3191bf27e55b498fecece9fd5c2c7aaa34c33"}, - {file = "lxml-5.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:877efb968c3d7eb2dad540b6cabf2f1d3c0fbf4b2d309a3c141f79c7e0061324"}, - {file = "lxml-5.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f14a4fb1c1c402a22e6a341a24c1341b4a3def81b41cd354386dcb795f83897"}, - {file = "lxml-5.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:25663d6e99659544ee8fe1b89b1a8c0aaa5e34b103fab124b17fa958c4a324a6"}, - {file = "lxml-5.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8b9f19df998761babaa7f09e6bc169294eefafd6149aaa272081cbddc7ba4ca3"}, - {file = "lxml-5.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e53d7e6a98b64fe54775d23a7c669763451340c3d44ad5e3a3b48a1efbdc96f"}, - {file = "lxml-5.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c3cd1fc1dc7c376c54440aeaaa0dcc803d2126732ff5c6b68ccd619f2e64be4f"}, - {file = "lxml-5.1.0.tar.gz", hash = "sha256:3eea6ed6e6c918e468e693c41ef07f3c3acc310b70ddd9cc72d9ef84bc9564ca"}, + {file = "lxml-6.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:09dd5b7075dc2f7709654a46543ba1ea3c2e217b2ed8fbd413a8a945a0f40f60"}, + {file = "lxml-6.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f6ac4ef4d82dff54670227a69c67782ae0b811b5cf6b17954f1e8f7502fc0d1d"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:556e94a63c9b04716f8e4de2abb65775061f846e89331b6c5be79183a24f98ea"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6bf403fbb3b3e348a561a5f4f0b9961835657981c802a1df03653eef8a9074"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dde6131244bba38a17c745836ba190bc753fd73c9291666287fd0a3fa3dcf30"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98fc784c2c1440667aeedf8465bdfe10208acf0ead656a2c68627299f546b315"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:add8cf6ddf9a65116119a28ece0f7886e30af27ba724a7594305f1d1b58a92a1"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cf9d57306d848218f3601fee7601fab1a327c942d56e2e97610583cb4dd74206"}, + {file = "lxml-6.1.1-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88136950da4d13c318bde414ce10219931937851327f44328f2df4d2c4614067"}, + {file = "lxml-6.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cecdd5dfdc87b1fd87dbf81d4b037a544f47f4c744200a67013771682d67686a"}, + {file = "lxml-6.1.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd312b9692e831d2ffcad61eab31d91d4b4655a962e61de8fb410472cbcd37aa"}, + {file = "lxml-6.1.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:5b7328b46d49fc9477d91ae8f6d55340347d827b7734ba3ea33faae0efef1383"}, + {file = "lxml-6.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37a58976370f36d9329d118ad0b953c5aeb9119ac9c6a4e258942a225d0573a1"}, + {file = "lxml-6.1.1-cp310-cp310-win32.whl", hash = "sha256:cea3f4c1af79af13cdb2da0c028111d8f8522d4f22a000c82385535f24e5cf3a"}, + {file = "lxml-6.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:3abf332af33a74288675d936fe861fd4344da0dd6622193fbc4f2bfbb35536b5"}, + {file = "lxml-6.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:8dadbe5b217ff35b6a8d16610dd710219b59b76d13f0e3f0d9f36786206e4485"}, + {file = "lxml-6.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:53b7d2b7a10b1c35c0a5e21e9224accf60c1bbfba523990732e521b2b73adef2"}, + {file = "lxml-6.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3f333630ab480244a1bff72043e511a91eb22e7595dead8653ee5612dd8f3d"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a4bbea04c97f6d78a48e3fbc1cb9116d2780b1b39e03a23f6eb9b603fd61f510"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db1d75f6617a49c1c01bc7023713e0ff59ab32c9579ae62a7674c0e34f3b0b0a"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a12689be69a28ddaa0ab99a5a1137da2afd5f8f16df7b5680b66f616d3eda1d"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b73c339ae29b90fd2d06e58ebd555a751bde9cd6bbd36cc0281b9a2c94e9d8"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux_2_28_i686.whl", hash = "sha256:752d3bbfe874715ccd0aec7f88d7fc623c0f1fd7aa7b3238a084e017bad2a009"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:6b1761fbf9ec984e2e9d9c589ef5f5fd684b7c19f92aadd567a26c5224958db6"}, + {file = "lxml-6.1.1-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d680fbcb768404c601ecb43519ecd8461f6954cb11c06a78962f666832ccfca8"}, + {file = "lxml-6.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:162af1091cd785f2f27e62d3547ae9bc58ec5c86dd314d67021fd02463708d83"}, + {file = "lxml-6.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e9308ff8241c532df3f3e570f9a5aeed6c853f888512ba4b75638d7c11c95ef6"}, + {file = "lxml-6.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5f6994074ebae6ffb04447268e37dc16edc304f9859cf91acb86e0af6c1b395c"}, + {file = "lxml-6.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80c2dfadb855da477cf73373ad29a333535dedb9b12bad02c9814c8e2b43bf08"}, + {file = "lxml-6.1.1-cp311-cp311-win32.whl", hash = "sha256:30a89d3ac8faec007453fb541f3f46807eeec88edd5826f6e3fe001752a2c621"}, + {file = "lxml-6.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:abbefa31eee84842140f67acef1c828e28bba8bbf0c3bc6e5492a9af88152c28"}, + {file = "lxml-6.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:dcb292aa7fe485ceff7af4f92e46c5af397daec5dff64871a528f0fc47a3cc5b"}, + {file = "lxml-6.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:104c09bda8d2a562824c0e319d0768ce26a779b7601e0931d33b09b53c392ef7"}, + {file = "lxml-6.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:25c6997a9a534e016695a0ba06b2f07945de682731ff01065b6d5a4474179da1"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c921ba5c51e4e9f63b8b00267d06566e1f63407408a0496da2d1d0bfc819c7fc"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:54a7f95e4de5fb94e2f9f4b9055c6ba33bf3d628fd77a1d647c5923caa2cdcdc"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f2ec43df44b1f76249ee0a615334f9b5b060e1c8bd90e706dad2d14d02f383"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70ef8a7e102a1508f8121aae5b0867abd663f72c14f0a9c937e6554cb4587b7b"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebe6af670449830d6d9b752c256a983291c766a1365ba5d5460048f9e33a7818"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:27acc820660aaffa4f7c087f29120e12980f7779d56d8492d263170111284740"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:1db753c9115ec7100d073b744d17e25e88a8f90f5c39b2f5dd878149af59671f"}, + {file = "lxml-6.1.1-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4f469aebd783bb741c2ecb2a681008fd26bfe5c16a9a72ed5467f834e810df2"}, + {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:766b010012d59470072c1816b5b6c69f1d243e5db36ea5968e94accf430a4635"}, + {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b8d812c6011c08b8111a15e54dd990b8923692d80adf35488bee34026c35accf"}, + {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:fe0306bd29505a9177aac19f1877174b0e7422c222a59f70b2cd41633448c3dc"}, + {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5ba186ad207446c65d3bb3d3e0412b032b1d9f595e59861e2354798c5703d955"}, + {file = "lxml-6.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a"}, + {file = "lxml-6.1.1-cp312-cp312-win32.whl", hash = "sha256:126c93f7f56f0eda92f6d8c619edc463a4f23d9252f1c9d0405a76f25fa9f11a"}, + {file = "lxml-6.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:26e6eda8d38c1fcab1090dd196ee87cbd13788e531937610e2589085de074e77"}, + {file = "lxml-6.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:6540377fbd53fe1b629172288c464fb18db11ce1fa7dc15891da10aa9dcc3e7f"}, + {file = "lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736"}, + {file = "lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f"}, + {file = "lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c"}, + {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b"}, + {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2"}, + {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5"}, + {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785"}, + {file = "lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947"}, + {file = "lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca"}, + {file = "lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660"}, + {file = "lxml-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc"}, + {file = "lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0"}, + {file = "lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245"}, + {file = "lxml-6.1.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5"}, + {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462"}, + {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465"}, + {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a"}, + {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590"}, + {file = "lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb"}, + {file = "lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603"}, + {file = "lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137"}, + {file = "lxml-6.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf"}, + {file = "lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee"}, + {file = "lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038"}, + {file = "lxml-6.1.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e"}, + {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072"}, + {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52"}, + {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b"}, + {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2"}, + {file = "lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e"}, + {file = "lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1"}, + {file = "lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e"}, + {file = "lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c"}, + {file = "lxml-6.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6689e828a94eee4f139408c337bb198e014724bb8a8c26d3cfac49d119ed69a6"}, + {file = "lxml-6.1.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bdebcc8a75d38c7598dfb2c9ed852d7a9eb4a10d6e2d0764b919b802bf32ac88"}, + {file = "lxml-6.1.1-cp38-cp38-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8be8ad51249698103d24b0571df35a10990fbe93dd043b6c024172189485f5e3"}, + {file = "lxml-6.1.1-cp38-cp38-manylinux_2_28_i686.whl", hash = "sha256:76447f65250ed2501ead1a1552f5ce8edff159a86f308348e6a9c4acb5e1f1b4"}, + {file = "lxml-6.1.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ffecec8eb889b58ba9be5b95fb1cc78e22ea8eedea38e8736a1568fe1979250e"}, + {file = "lxml-6.1.1-cp38-cp38-win32.whl", hash = "sha256:c674693f055fa2495de12292cb45e9944199d8eaef5a2dec45175c7c61cb73e3"}, + {file = "lxml-6.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:55b03549819867ea141c0202242c4816c82e52ec36e7e648db9d8da5a3dc3ed6"}, + {file = "lxml-6.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c9f79d5325907f13e1be0b3e4dacc1049d1dffc4aeee3c995284bea5fe0fab7d"}, + {file = "lxml-6.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:83b6b30eb131da7a75b601f28c5d6971e6ed3e887919bf6b6a1ad3c2df289080"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:441dd227fa0690eb9fc81edabc63cdcefc212bba99b906dcf6e32cc1a9d3e533"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e07c65f443c887bbcf31cc1771d932ecc192a5273943589b3c7572b749f1ffb2"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5bec7d03d78d853597d6107854c2310ce3f761fd218fe9fe91d5101fcf6c2efe"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f76acfb5f68ba982635a53fd985a8044be98a35b43232c2a1ee235ffab3e1dd"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux_2_28_i686.whl", hash = "sha256:8d43ca737b20e106e4aebc42b2f3ae19f00ba63d7eb731698ee083d72d15646f"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:32ab449a5486f6c758e849bb86710d0e45edc24a04e250c01555f8f5653958f8"}, + {file = "lxml-6.1.1-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53c909b62a0532183542fed00c5a7218258c56292d409bc789886fe1cb04c438"}, + {file = "lxml-6.1.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:640f97d43d867bcb9c75b3af013b64850756b746cb6bce8ace83b70da3abba9d"}, + {file = "lxml-6.1.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:469e3618338bd7ab5beb412d2439825479fcf0dab99e394ca563dbc4eaf6c834"}, + {file = "lxml-6.1.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:aae97dfdb60715c164419ac2532a76d013c3918a665eb6cb7288098b5f349aaf"}, + {file = "lxml-6.1.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c9a4b821dc7055bf9e05ff5719e18ec501f75c0f0bbfabd573b277559780833d"}, + {file = "lxml-6.1.1-cp39-cp39-win32.whl", hash = "sha256:639f6c857d91d9be29bd7502348d6736dab168b54b5158cd899abf11684dc186"}, + {file = "lxml-6.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:34c2d737beabfe35baada43941ed519251e9a12e779031496bcd5d539fcfd730"}, + {file = "lxml-6.1.1-cp39-cp39-win_arm64.whl", hash = "sha256:07a4a68e286ee7a1ed7dfb8af83e615757c0ccfe9f18c6b4ea6771388d9ba8c9"}, + {file = "lxml-6.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:31033dc34636ea6b7d5cc11b1ddbda78a14de858ba9d3e1ed4b69a3085bc521e"}, + {file = "lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3893c14c4b6ac5b2d54ba8cf03e99fe5104e592de491f19bd6b82756c09f8004"}, + {file = "lxml-6.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c07da4cebf6889f03ebac8d238f62318e29f495de0aa18a51ea14e61ae907e2e"}, + {file = "lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6f0ce10945fab9c4c06ce14e22af9059d1a87493a9af4501a5b0b9187e21cf2"}, + {file = "lxml-6.1.1-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f8844cd288697c6425c9beba919302241e3278871dc6519515e72b04e987abcf"}, + {file = "lxml-6.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ed21202aec73cda4d55d1ce57b389aadb90ffb044e6cd1080b8347efe1b1ec84"}, + {file = "lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40"}, ] [package.extras] cssselect = ["cssselect (>=0.7)"] +html-clean = ["lxml_html_clean"] html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] -source = ["Cython (>=3.0.7)"] [[package]] name = "mako" -version = "1.3.2" +version = "1.3.12" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." optional = false python-versions = ">=3.8" files = [ - {file = "Mako-1.3.2-py3-none-any.whl", hash = "sha256:32a99d70754dfce237019d17ffe4a282d2d3351b9c476e90d8a60e63f133b80c"}, - {file = "Mako-1.3.2.tar.gz", hash = "sha256:2a0c8ad7f6274271b3bb7467dd37cf9cc6dab4bc19cb69a4ef10669402de698e"}, + {file = "mako-1.3.12-py3-none-any.whl", hash = "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9"}, + {file = "mako-1.3.12.tar.gz", hash = "sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a"}, ] [package.dependencies] @@ -2382,13 +2630,13 @@ testing = ["pytest"] [[package]] name = "markdown-it-py" -version = "3.0.0" +version = "4.2.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, + {file = "markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a"}, + {file = "markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49"}, ] [package.dependencies] @@ -2396,118 +2644,173 @@ mdurl = ">=0.1,<1.0" [package.extras] benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] +plugins = ["mdit-py-plugins (>=0.5.0)"] profiling = ["gprof2dot"] -rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] +rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "pytest-timeout", "requests"] [[package]] name = "markupsafe" -version = "2.1.4" +version = "3.0.3" description = "Safely add untrusted strings to HTML/XML markup." optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de8153a7aae3835484ac168a9a9bdaa0c5eee4e0bc595503c95d53b942879c84"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e888ff76ceb39601c59e219f281466c6d7e66bd375b4ec1ce83bcdc68306796b"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0b838c37ba596fcbfca71651a104a611543077156cb0a26fe0c475e1f152ee8"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac1ebf6983148b45b5fa48593950f90ed6d1d26300604f321c74a9ca1609f8e"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fbad3d346df8f9d72622ac71b69565e621ada2ce6572f37c2eae8dacd60385d"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5291d98cd3ad9a562883468c690a2a238c4a6388ab3bd155b0c75dd55ece858"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a7cc49ef48a3c7a0005a949f3c04f8baa5409d3f663a1b36f0eba9bfe2a0396e"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83041cda633871572f0d3c41dddd5582ad7d22f65a72eacd8d3d6d00291df26"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-win32.whl", hash = "sha256:0c26f67b3fe27302d3a412b85ef696792c4a2386293c53ba683a89562f9399b0"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-win_amd64.whl", hash = "sha256:a76055d5cb1c23485d7ddae533229039b850db711c554a12ea64a0fd8a0129e2"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9e9e3c4020aa2dc62d5dd6743a69e399ce3de58320522948af6140ac959ab863"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0042d6a9880b38e1dd9ff83146cc3c9c18a059b9360ceae207805567aacccc69"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d03fea4c4e9fd0ad75dc2e7e2b6757b80c152c032ea1d1de487461d8140efc"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ab3a886a237f6e9c9f4f7d272067e712cdb4efa774bef494dccad08f39d8ae6"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abf5ebbec056817057bfafc0445916bb688a255a5146f900445d081db08cbabb"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e1a0d1924a5013d4f294087e00024ad25668234569289650929ab871231668e7"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e7902211afd0af05fbadcc9a312e4cf10f27b779cf1323e78d52377ae4b72bea"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c669391319973e49a7c6230c218a1e3044710bc1ce4c8e6eb71f7e6d43a2c131"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-win32.whl", hash = "sha256:31f57d64c336b8ccb1966d156932f3daa4fee74176b0fdc48ef580be774aae74"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-win_amd64.whl", hash = "sha256:54a7e1380dfece8847c71bf7e33da5d084e9b889c75eca19100ef98027bd9f56"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a76cd37d229fc385738bd1ce4cba2a121cf26b53864c1772694ad0ad348e509e"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:987d13fe1d23e12a66ca2073b8d2e2a75cec2ecb8eab43ff5624ba0ad42764bc"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5244324676254697fe5c181fc762284e2c5fceeb1c4e3e7f6aca2b6f107e60dc"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78bc995e004681246e85e28e068111a4c3f35f34e6c62da1471e844ee1446250"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4d176cfdfde84f732c4a53109b293d05883e952bbba68b857ae446fa3119b4f"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f9917691f410a2e0897d1ef99619fd3f7dd503647c8ff2475bf90c3cf222ad74"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f06e5a9e99b7df44640767842f414ed5d7bedaaa78cd817ce04bbd6fd86e2dd6"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396549cea79e8ca4ba65525470d534e8a41070e6b3500ce2414921099cb73e8d"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-win32.whl", hash = "sha256:f6be2d708a9d0e9b0054856f07ac7070fbe1754be40ca8525d5adccdbda8f475"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-win_amd64.whl", hash = "sha256:5045e892cfdaecc5b4c01822f353cf2c8feb88a6ec1c0adef2a2e705eef0f656"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a07f40ef8f0fbc5ef1000d0c78771f4d5ca03b4953fc162749772916b298fc4"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d18b66fe626ac412d96c2ab536306c736c66cf2a31c243a45025156cc190dc8a"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:698e84142f3f884114ea8cf83e7a67ca8f4ace8454e78fe960646c6c91c63bfa"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a3b78a5af63ec10d8604180380c13dcd870aba7928c1fe04e881d5c792dc4e"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:15866d7f2dc60cfdde12ebb4e75e41be862348b4728300c36cdf405e258415ec"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6aa5e2e7fc9bc042ae82d8b79d795b9a62bd8f15ba1e7594e3db243f158b5565"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:54635102ba3cf5da26eb6f96c4b8c53af8a9c0d97b64bdcb592596a6255d8518"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-win32.whl", hash = "sha256:3583a3a3ab7958e354dc1d25be74aee6228938312ee875a22330c4dc2e41beb0"}, - {file = "MarkupSafe-2.1.4-cp37-cp37m-win_amd64.whl", hash = "sha256:d6e427c7378c7f1b2bef6a344c925b8b63623d3321c09a237b7cc0e77dd98ceb"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:bf1196dcc239e608605b716e7b166eb5faf4bc192f8a44b81e85251e62584bd2"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4df98d4a9cd6a88d6a585852f56f2155c9cdb6aec78361a19f938810aa020954"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b835aba863195269ea358cecc21b400276747cc977492319fd7682b8cd2c253d"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23984d1bdae01bee794267424af55eef4dfc038dc5d1272860669b2aa025c9e3"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c98c33ffe20e9a489145d97070a435ea0679fddaabcafe19982fe9c971987d5"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9896fca4a8eb246defc8b2a7ac77ef7553b638e04fbf170bff78a40fa8a91474"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b0fe73bac2fed83839dbdbe6da84ae2a31c11cfc1c777a40dbd8ac8a6ed1560f"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c7556bafeaa0a50e2fe7dc86e0382dea349ebcad8f010d5a7dc6ba568eaaa789"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-win32.whl", hash = "sha256:fc1a75aa8f11b87910ffd98de62b29d6520b6d6e8a3de69a70ca34dea85d2a8a"}, - {file = "MarkupSafe-2.1.4-cp38-cp38-win_amd64.whl", hash = "sha256:3a66c36a3864df95e4f62f9167c734b3b1192cb0851b43d7cc08040c074c6279"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:765f036a3d00395a326df2835d8f86b637dbaf9832f90f5d196c3b8a7a5080cb"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:21e7af8091007bf4bebf4521184f4880a6acab8df0df52ef9e513d8e5db23411"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5c31fe855c77cad679b302aabc42d724ed87c043b1432d457f4976add1c2c3e"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7653fa39578957bc42e5ebc15cf4361d9e0ee4b702d7d5ec96cdac860953c5b4"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47bb5f0142b8b64ed1399b6b60f700a580335c8e1c57f2f15587bd072012decc"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fe8512ed897d5daf089e5bd010c3dc03bb1bdae00b35588c49b98268d4a01e00"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:36d7626a8cca4d34216875aee5a1d3d654bb3dac201c1c003d182283e3205949"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b6f14a9cd50c3cb100eb94b3273131c80d102e19bb20253ac7bd7336118a673a"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-win32.whl", hash = "sha256:c8f253a84dbd2c63c19590fa86a032ef3d8cc18923b8049d91bcdeeb2581fbf6"}, - {file = "MarkupSafe-2.1.4-cp39-cp39-win_amd64.whl", hash = "sha256:8b570a1537367b52396e53325769608f2a687ec9a4363647af1cded8928af959"}, - {file = "MarkupSafe-2.1.4.tar.gz", hash = "sha256:3aae9af4cac263007fd6309c64c6ab4506dd2b79382d9d19a1994f9240b8db4f"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, + {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, + {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, + {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, + {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, + {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, + {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, + {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, ] [[package]] name = "matplotlib" -version = "3.8.2" +version = "3.10.9" description = "Python plotting package" optional = false -python-versions = ">=3.9" -files = [ - {file = "matplotlib-3.8.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:09796f89fb71a0c0e1e2f4bdaf63fb2cefc84446bb963ecdeb40dfee7dfa98c7"}, - {file = "matplotlib-3.8.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f9c6976748a25e8b9be51ea028df49b8e561eed7809146da7a47dbecebab367"}, - {file = "matplotlib-3.8.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b78e4f2cedf303869b782071b55fdde5987fda3038e9d09e58c91cc261b5ad18"}, - {file = "matplotlib-3.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e208f46cf6576a7624195aa047cb344a7f802e113bb1a06cfd4bee431de5e31"}, - {file = "matplotlib-3.8.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:46a569130ff53798ea5f50afce7406e91fdc471ca1e0e26ba976a8c734c9427a"}, - {file = "matplotlib-3.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:830f00640c965c5b7f6bc32f0d4ce0c36dfe0379f7dd65b07a00c801713ec40a"}, - {file = "matplotlib-3.8.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d86593ccf546223eb75a39b44c32788e6f6440d13cfc4750c1c15d0fcb850b63"}, - {file = "matplotlib-3.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a5430836811b7652991939012f43d2808a2db9b64ee240387e8c43e2e5578c8"}, - {file = "matplotlib-3.8.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9576723858a78751d5aacd2497b8aef29ffea6d1c95981505877f7ac28215c6"}, - {file = "matplotlib-3.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ba9cbd8ac6cf422f3102622b20f8552d601bf8837e49a3afed188d560152788"}, - {file = "matplotlib-3.8.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:03f9d160a29e0b65c0790bb07f4f45d6a181b1ac33eb1bb0dd225986450148f0"}, - {file = "matplotlib-3.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:3773002da767f0a9323ba1a9b9b5d00d6257dbd2a93107233167cfb581f64717"}, - {file = "matplotlib-3.8.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:4c318c1e95e2f5926fba326f68177dee364aa791d6df022ceb91b8221bd0a627"}, - {file = "matplotlib-3.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:091275d18d942cf1ee9609c830a1bc36610607d8223b1b981c37d5c9fc3e46a4"}, - {file = "matplotlib-3.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b0f3b8ea0e99e233a4bcc44590f01604840d833c280ebb8fe5554fd3e6cfe8d"}, - {file = "matplotlib-3.8.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7b1704a530395aaf73912be741c04d181f82ca78084fbd80bc737be04848331"}, - {file = "matplotlib-3.8.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533b0e3b0c6768eef8cbe4b583731ce25a91ab54a22f830db2b031e83cca9213"}, - {file = "matplotlib-3.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:0f4fc5d72b75e2c18e55eb32292659cf731d9d5b312a6eb036506304f4675630"}, - {file = "matplotlib-3.8.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:deaed9ad4da0b1aea77fe0aa0cebb9ef611c70b3177be936a95e5d01fa05094f"}, - {file = "matplotlib-3.8.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:172f4d0fbac3383d39164c6caafd3255ce6fa58f08fc392513a0b1d3b89c4f89"}, - {file = "matplotlib-3.8.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7d36c2209d9136cd8e02fab1c0ddc185ce79bc914c45054a9f514e44c787917"}, - {file = "matplotlib-3.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5864bdd7da445e4e5e011b199bb67168cdad10b501750367c496420f2ad00843"}, - {file = "matplotlib-3.8.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ef8345b48e95cee45ff25192ed1f4857273117917a4dcd48e3905619bcd9c9b8"}, - {file = "matplotlib-3.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:7c48d9e221b637c017232e3760ed30b4e8d5dfd081daf327e829bf2a72c731b4"}, - {file = "matplotlib-3.8.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:aa11b3c6928a1e496c1a79917d51d4cd5d04f8a2e75f21df4949eeefdf697f4b"}, - {file = "matplotlib-3.8.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1095fecf99eeb7384dabad4bf44b965f929a5f6079654b681193edf7169ec20"}, - {file = "matplotlib-3.8.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:bddfb1db89bfaa855912261c805bd0e10218923cc262b9159a49c29a7a1c1afa"}, - {file = "matplotlib-3.8.2.tar.gz", hash = "sha256:01a978b871b881ee76017152f1f1a0cbf6bd5f7b8ff8c96df0df1bd57d8755a1"}, +python-versions = ">=3.10" +files = [ + {file = "matplotlib-3.10.9-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77210dce9cb8153dffc967efaae990543392563d5a376d4dd8539bebcb0ed217"}, + {file = "matplotlib-3.10.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1e7698ac9868428e84d2c967424803b2472ff7167d9d6590d4204ed775343c3b"}, + {file = "matplotlib-3.10.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1aa972116abb4c9d201bf245620b433726cb6856f3bef6a78f776a00f5c92d37"}, + {file = "matplotlib-3.10.9-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae2f11957b27ce53497dd4d7b235c4d4f1faf383dfb39d0c5beb833bff883294"}, + {file = "matplotlib-3.10.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b049278ddce116aaa1c1377ebf58adea909132dfce0281cf7e3a1ea9fc2e2c65"}, + {file = "matplotlib-3.10.9-cp310-cp310-win_amd64.whl", hash = "sha256:82834c3c292d24d3a8aae77cd2d20019de69d692a34a970e4fdb8d33e2ea3dda"}, + {file = "matplotlib-3.10.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:68cfdcede415f7c8f5577b03303dd94526cdb6d11036cecdc205e08733b2d2bb"}, + {file = "matplotlib-3.10.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfca0129678bd56379db26c52b5d77ed7de314c047492fbdc763aa7501710cfb"}, + {file = "matplotlib-3.10.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e436d155fa8a3399dc62683f8f5d0e2e50d25d0144a73edd73f82eec8f4abfb"}, + {file = "matplotlib-3.10.9-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56fc0bd271b00025c6edfdc7c2dcd247372c8e1544971d62e1dc7c17367e8bf9"}, + {file = "matplotlib-3.10.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5a6104ed666402ba5106d7f36e0e0cdca4e8d7fa4d39708ca88019e2835a2eb"}, + {file = "matplotlib-3.10.9-cp311-cp311-win_amd64.whl", hash = "sha256:d730e984eddf56974c3e72b6129c7ca462ac38dc624338f4b0b23eb23ecba00f"}, + {file = "matplotlib-3.10.9-cp311-cp311-win_arm64.whl", hash = "sha256:51bf0ddbdc598e060d46c16b5590708f81a1624cefbaaf62f6a81bf9285b8c80"}, + {file = "matplotlib-3.10.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1"}, + {file = "matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320"}, + {file = "matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285"}, + {file = "matplotlib-3.10.9-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2"}, + {file = "matplotlib-3.10.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf"}, + {file = "matplotlib-3.10.9-cp312-cp312-win_amd64.whl", hash = "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6"}, + {file = "matplotlib-3.10.9-cp312-cp312-win_arm64.whl", hash = "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42"}, + {file = "matplotlib-3.10.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f"}, + {file = "matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e"}, + {file = "matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f"}, + {file = "matplotlib-3.10.9-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838"}, + {file = "matplotlib-3.10.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2"}, + {file = "matplotlib-3.10.9-cp313-cp313-win_amd64.whl", hash = "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921"}, + {file = "matplotlib-3.10.9-cp313-cp313-win_arm64.whl", hash = "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8"}, + {file = "matplotlib-3.10.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9"}, + {file = "matplotlib-3.10.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4"}, + {file = "matplotlib-3.10.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc"}, + {file = "matplotlib-3.10.9-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99"}, + {file = "matplotlib-3.10.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d"}, + {file = "matplotlib-3.10.9-cp313-cp313t-win_amd64.whl", hash = "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8"}, + {file = "matplotlib-3.10.9-cp313-cp313t-win_arm64.whl", hash = "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38"}, + {file = "matplotlib-3.10.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d"}, + {file = "matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f"}, + {file = "matplotlib-3.10.9-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b"}, + {file = "matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2"}, + {file = "matplotlib-3.10.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716"}, + {file = "matplotlib-3.10.9-cp314-cp314-win_amd64.whl", hash = "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f"}, + {file = "matplotlib-3.10.9-cp314-cp314-win_arm64.whl", hash = "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456"}, + {file = "matplotlib-3.10.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe"}, + {file = "matplotlib-3.10.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6"}, + {file = "matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c"}, + {file = "matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4"}, + {file = "matplotlib-3.10.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf"}, + {file = "matplotlib-3.10.9-cp314-cp314t-win_amd64.whl", hash = "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39"}, + {file = "matplotlib-3.10.9-cp314-cp314t-win_arm64.whl", hash = "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c"}, + {file = "matplotlib-3.10.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1872fb212a05b729e649754a72d5da61d03e0554d76e80303b6f83d1d2c0552b"}, + {file = "matplotlib-3.10.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:985f2238880e2e69093f588f5fe2e46771747febf0649f3cf7f7b7480875317f"}, + {file = "matplotlib-3.10.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6640f75af2c6148293caa0a2b39dd806a492dd66c8a8b04035813e33d0fd2585"}, + {file = "matplotlib-3.10.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:42fb814efabe95c06c1994d8ab5a8385f43a249e23badd3ba931d4308e5bca20"}, + {file = "matplotlib-3.10.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f76e640a5268850bfda54b5131b1b1941cc685e42c5fa98ed9f2d64038308cba"}, + {file = "matplotlib-3.10.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3fc0364dfbe1d07f6d15c5ebd0c5bf89e126916e5a8667dd4a7a6e84c36653d4"}, + {file = "matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358"}, ] [package.dependencies] @@ -2515,12 +2818,15 @@ contourpy = ">=1.0.1" cycler = ">=0.10" fonttools = ">=4.22.0" kiwisolver = ">=1.3.1" -numpy = ">=1.21,<2" +numpy = ">=1.23" packaging = ">=20.0" pillow = ">=8" -pyparsing = ">=2.3.1" +pyparsing = ">=3" python-dateutil = ">=2.7" +[package.extras] +dev = ["meson-python (>=0.13.1,<0.17.0)", "pybind11 (>=2.13.2,!=2.13.3)", "setuptools (>=64)", "setuptools_scm (>=7,<10)"] + [[package]] name = "mdurl" version = "0.1.2" @@ -2534,176 +2840,232 @@ files = [ [[package]] name = "msgpack" -version = "1.1.0" +version = "1.2.1" description = "MessagePack serializer" optional = false -python-versions = ">=3.8" -files = [ - {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd"}, - {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d"}, - {file = "msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5"}, - {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5"}, - {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e"}, - {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b"}, - {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f"}, - {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68"}, - {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b"}, - {file = "msgpack-1.1.0-cp310-cp310-win32.whl", hash = "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044"}, - {file = "msgpack-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f"}, - {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7"}, - {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa"}, - {file = "msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701"}, - {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6"}, - {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59"}, - {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0"}, - {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e"}, - {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6"}, - {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5"}, - {file = "msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88"}, - {file = "msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788"}, - {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d"}, - {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2"}, - {file = "msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420"}, - {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2"}, - {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39"}, - {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f"}, - {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247"}, - {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c"}, - {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b"}, - {file = "msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b"}, - {file = "msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f"}, - {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf"}, - {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330"}, - {file = "msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734"}, - {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e"}, - {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca"}, - {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915"}, - {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d"}, - {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434"}, - {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c"}, - {file = "msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc"}, - {file = "msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f"}, - {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c40ffa9a15d74e05ba1fe2681ea33b9caffd886675412612d93ab17b58ea2fec"}, - {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1ba6136e650898082d9d5a5217d5906d1e138024f836ff48691784bbe1adf96"}, - {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0856a2b7e8dcb874be44fea031d22e5b3a19121be92a1e098f46068a11b0870"}, - {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:471e27a5787a2e3f974ba023f9e265a8c7cfd373632247deb225617e3100a3c7"}, - {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:646afc8102935a388ffc3914b336d22d1c2d6209c773f3eb5dd4d6d3b6f8c1cb"}, - {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:13599f8829cfbe0158f6456374e9eea9f44eee08076291771d8ae93eda56607f"}, - {file = "msgpack-1.1.0-cp38-cp38-win32.whl", hash = "sha256:8a84efb768fb968381e525eeeb3d92857e4985aacc39f3c47ffd00eb4509315b"}, - {file = "msgpack-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:879a7b7b0ad82481c52d3c7eb99bf6f0645dbdec5134a4bddbd16f3506947feb"}, - {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:53258eeb7a80fc46f62fd59c876957a2d0e15e6449a9e71842b6d24419d88ca1"}, - {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e7b853bbc44fb03fbdba34feb4bd414322180135e2cb5164f20ce1c9795ee48"}, - {file = "msgpack-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3e9b4936df53b970513eac1758f3882c88658a220b58dcc1e39606dccaaf01c"}, - {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46c34e99110762a76e3911fc923222472c9d681f1094096ac4102c18319e6468"}, - {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a706d1e74dd3dea05cb54580d9bd8b2880e9264856ce5068027eed09680aa74"}, - {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:534480ee5690ab3cbed89d4c8971a5c631b69a8c0883ecfea96c19118510c846"}, - {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8cf9e8c3a2153934a23ac160cc4cba0ec035f6867c8013cc6077a79823370346"}, - {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3180065ec2abbe13a4ad37688b61b99d7f9e012a535b930e0e683ad6bc30155b"}, - {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c5a91481a3cc573ac8c0d9aace09345d989dc4a0202b7fcb312c88c26d4e71a8"}, - {file = "msgpack-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f80bc7d47f76089633763f952e67f8214cb7b3ee6bfa489b3cb6a84cfac114cd"}, - {file = "msgpack-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:4d1b7ff2d6146e16e8bd665ac726a89c74163ef8cd39fa8c1087d4e52d3a2325"}, - {file = "msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e"}, +python-versions = ">=3.10" +files = [ + {file = "msgpack-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c7b398c56ff125feae96c2737abfec5595f1fa0aa186df60c56040b8accb95c"}, + {file = "msgpack-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1548006a91aa93c5da81f3bdcebc1a0d10cea2d25969754fbe848da622b2b895"}, + {file = "msgpack-1.2.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dabedcd0f23559f3596428c6589c1cd8c6eaed3a0d720795b07b0225d769203"}, + {file = "msgpack-1.2.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83efa1c898e0fc5380fc0cabbf75164c52e3b5cbb45973710d75821928380c73"}, + {file = "msgpack-1.2.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01e2dd6c9b19d333a00282330cc8a73d38d8dabc306dc5b42cd668c3ac82e833"}, + {file = "msgpack-1.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:350cb813d0af6e65d2f7ef0d729f7ff5be5a8bce03665892f43e5883d4ecc1b8"}, + {file = "msgpack-1.2.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ee1d9ed27d0497b848923746cf762ed2e7db24f4be7eec8e5cbe8c766aa707b7"}, + {file = "msgpack-1.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:633727297ed063441fd1cda2288865487f33ad14eeb8831afb5f0c396a62cfce"}, + {file = "msgpack-1.2.1-cp310-cp310-win32.whl", hash = "sha256:298872ecf9e61950f1c6af4ca969b859ee91783bb920ef6e6172697d0c8aad74"}, + {file = "msgpack-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2ff164c1b0bcb740b073b99e945234d0212852fa378e44a208c425379140dbeb"}, + {file = "msgpack-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22"}, + {file = "msgpack-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5"}, + {file = "msgpack-1.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06"}, + {file = "msgpack-1.2.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4"}, + {file = "msgpack-1.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8"}, + {file = "msgpack-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b"}, + {file = "msgpack-1.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e"}, + {file = "msgpack-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f"}, + {file = "msgpack-1.2.1-cp311-cp311-win32.whl", hash = "sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d"}, + {file = "msgpack-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8"}, + {file = "msgpack-1.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66"}, + {file = "msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35"}, + {file = "msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c"}, + {file = "msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0"}, + {file = "msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a"}, + {file = "msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6"}, + {file = "msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a"}, + {file = "msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1"}, + {file = "msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64"}, + {file = "msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac"}, + {file = "msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24"}, + {file = "msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07"}, + {file = "msgpack-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064"}, + {file = "msgpack-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056"}, + {file = "msgpack-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc"}, + {file = "msgpack-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d"}, + {file = "msgpack-1.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155"}, + {file = "msgpack-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402"}, + {file = "msgpack-1.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c"}, + {file = "msgpack-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6"}, + {file = "msgpack-1.2.1-cp313-cp313-win32.whl", hash = "sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707"}, + {file = "msgpack-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9"}, + {file = "msgpack-1.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a"}, + {file = "msgpack-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d"}, + {file = "msgpack-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7"}, + {file = "msgpack-1.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889"}, + {file = "msgpack-1.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720"}, + {file = "msgpack-1.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190"}, + {file = "msgpack-1.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d"}, + {file = "msgpack-1.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24"}, + {file = "msgpack-1.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7"}, + {file = "msgpack-1.2.1-cp314-cp314-win32.whl", hash = "sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb"}, + {file = "msgpack-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b"}, + {file = "msgpack-1.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7"}, + {file = "msgpack-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273"}, + {file = "msgpack-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1"}, + {file = "msgpack-1.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc"}, + {file = "msgpack-1.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde"}, + {file = "msgpack-1.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4"}, + {file = "msgpack-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d"}, + {file = "msgpack-1.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355"}, + {file = "msgpack-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c"}, + {file = "msgpack-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1"}, + {file = "msgpack-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2"}, + {file = "msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107"}, + {file = "msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647"}, ] [[package]] name = "multidict" -version = "6.1.0" +version = "6.7.1" description = "multidict implementation" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, - {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, - {file = "multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53"}, - {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5"}, - {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581"}, - {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56"}, - {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429"}, - {file = "multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748"}, - {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db"}, - {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056"}, - {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76"}, - {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160"}, - {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7"}, - {file = "multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0"}, - {file = "multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d"}, - {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6"}, - {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156"}, - {file = "multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351"}, - {file = "multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3"}, - {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753"}, - {file = "multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80"}, - {file = "multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926"}, - {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa"}, - {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436"}, - {file = "multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925"}, - {file = "multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6"}, - {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3"}, - {file = "multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133"}, - {file = "multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1"}, - {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008"}, - {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f"}, - {file = "multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28"}, - {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b"}, - {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c"}, - {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3"}, - {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44"}, - {file = "multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2"}, - {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3"}, - {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa"}, - {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa"}, - {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4"}, - {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6"}, - {file = "multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81"}, - {file = "multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774"}, - {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392"}, - {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a"}, - {file = "multidict-6.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2"}, - {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc"}, - {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478"}, - {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4"}, - {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d"}, - {file = "multidict-6.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6"}, - {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2"}, - {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd"}, - {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6"}, - {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492"}, - {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd"}, - {file = "multidict-6.1.0-cp38-cp38-win32.whl", hash = "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167"}, - {file = "multidict-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef"}, - {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c"}, - {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1"}, - {file = "multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c"}, - {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c"}, - {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f"}, - {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875"}, - {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255"}, - {file = "multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30"}, - {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057"}, - {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657"}, - {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28"}, - {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972"}, - {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43"}, - {file = "multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada"}, - {file = "multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a"}, - {file = "multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506"}, - {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"}, + {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5"}, + {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8"}, + {file = "multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505"}, + {file = "multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122"}, + {file = "multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df"}, + {file = "multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa"}, + {file = "multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a"}, + {file = "multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b"}, + {file = "multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba"}, + {file = "multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511"}, + {file = "multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19"}, + {file = "multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33"}, + {file = "multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3"}, + {file = "multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5"}, + {file = "multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108"}, + {file = "multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32"}, + {file = "multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8"}, + {file = "multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b"}, + {file = "multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d"}, + {file = "multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f"}, + {file = "multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2"}, + {file = "multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7"}, + {file = "multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5"}, + {file = "multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5"}, + {file = "multidict-6.7.1-cp39-cp39-win32.whl", hash = "sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0"}, + {file = "multidict-6.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4"}, + {file = "multidict-6.7.1-cp39-cp39-win_arm64.whl", hash = "sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9"}, + {file = "multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56"}, + {file = "multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d"}, ] [package.dependencies] @@ -2711,124 +3073,121 @@ typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} [[package]] name = "networkx" -version = "3.2.1" +version = "3.4.2" description = "Python package for creating and manipulating graphs and networks" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2"}, - {file = "networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6"}, + {file = "networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f"}, + {file = "networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1"}, ] [package.extras] -default = ["matplotlib (>=3.5)", "numpy (>=1.22)", "pandas (>=1.4)", "scipy (>=1.9,!=1.11.0,!=1.11.1)"] -developer = ["changelist (==0.4)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"] -doc = ["nb2plots (>=0.7)", "nbconvert (<7.9)", "numpydoc (>=1.6)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.14)", "sphinx (>=7)", "sphinx-gallery (>=0.14)", "texext (>=0.6.7)"] -extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.11)", "sympy (>=1.10)"] +default = ["matplotlib (>=3.7)", "numpy (>=1.24)", "pandas (>=2.0)", "scipy (>=1.10,!=1.11.0,!=1.11.1)"] +developer = ["changelist (==0.5)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"] +doc = ["intersphinx-registry", "myst-nb (>=1.1)", "numpydoc (>=1.8.0)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.15)", "sphinx (>=7.3)", "sphinx-gallery (>=0.16)", "texext (>=0.6.7)"] +example = ["cairocffi (>=1.7)", "contextily (>=1.6)", "igraph (>=0.11)", "momepy (>=0.7.2)", "osmnx (>=1.9)", "scikit-learn (>=1.5)", "seaborn (>=0.13)"] +extra = ["lxml (>=4.6)", "pydot (>=3.0.1)", "pygraphviz (>=1.14)", "sympy (>=1.10)"] test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"] [[package]] name = "nibabel" -version = "5.2.0" +version = "5.4.2" description = "Access a multitude of neuroimaging data formats" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" files = [ - {file = "nibabel-5.2.0-py3-none-any.whl", hash = "sha256:77724af6e29fd9c4173702e4d031e7d8c45b5963887905a0f90edab880381b7f"}, - {file = "nibabel-5.2.0.tar.gz", hash = "sha256:3df8f1ab981d1bd92f4331d565528d126ab9717fdbd4cfe68f43fcd1c2bf3f52"}, + {file = "nibabel-5.4.2-py3-none-any.whl", hash = "sha256:553482c5f1e1034fc312edf6fb7f32236c0056439845d1c29293b7e8c98d4854"}, + {file = "nibabel-5.4.2.tar.gz", hash = "sha256:d5f4b9076a13178ae7f7acf18c8dbd503ee1c4d5c0c23b85df7be87efcbb49da"}, ] [package.dependencies] -numpy = ">=1.20" -packaging = ">=17" +importlib-resources = {version = ">=5.12", markers = "python_version < \"3.12\""} +numpy = ">=1.25" +packaging = ">=20" +typing-extensions = {version = ">=4.6", markers = "python_version < \"3.13\""} [package.extras] -all = ["nibabel[dicomfs,minc2,spm,zstd]"] -dev = ["tox"] -dicom = ["pydicom (>=1.0.0)"] -dicomfs = ["nibabel[dicom]", "pillow"] -doc = ["matplotlib (>=1.5.3)", "numpydoc", "sphinx", "texext", "tomli"] -doctest = ["tox"] -minc2 = ["h5py"] -spm = ["scipy"] -style = ["tox"] -test = ["pytest", "pytest-cov", "pytest-doctestplus", "pytest-httpserver", "pytest-xdist"] -typing = ["tox"] -zstd = ["pyzstd (>=0.14.3)"] +all = ["backports-zstd (>=1.1)", "h5py (>=3.8)", "indexed-gzip (>=1.6)", "pillow (>=8.4)", "pydicom (>=2.3)", "scipy (>=1.11)"] +dicom = ["pydicom (>=2.3)"] +dicomfs = ["pillow (>=8.4)", "pydicom (>=2.3)"] +indexed-gzip = ["indexed-gzip (>=1.6)"] +minc2 = ["h5py (>=3.8)"] +spm = ["scipy (>=1.11)"] +test = ["coverage[toml] (>=7.2)", "pytest (>=8)", "pytest-cov (>=6)", "pytest-doctestplus (>=1.4)", "pytest-httpserver (>=1.0.7)", "pytest-xdist (>=3.5)"] +viewers = ["matplotlib (>=3.7)"] +zstd = ["backports-zstd (>=1.1)"] [[package]] name = "nodeenv" -version = "1.8.0" +version = "1.10.0" description = "Node.js virtual environment builder" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ - {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, - {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, + {file = "nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827"}, + {file = "nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb"}, ] -[package.dependencies] -setuptools = "*" - [[package]] name = "numpy" -version = "1.26.3" +version = "1.26.4" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.9" files = [ - {file = "numpy-1.26.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:806dd64230dbbfaca8a27faa64e2f414bf1c6622ab78cc4264f7f5f028fee3bf"}, - {file = "numpy-1.26.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02f98011ba4ab17f46f80f7f8f1c291ee7d855fcef0a5a98db80767a468c85cd"}, - {file = "numpy-1.26.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d45b3ec2faed4baca41c76617fcdcfa4f684ff7a151ce6fc78ad3b6e85af0a6"}, - {file = "numpy-1.26.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdd2b45bf079d9ad90377048e2747a0c82351989a2165821f0c96831b4a2a54b"}, - {file = "numpy-1.26.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:211ddd1e94817ed2d175b60b6374120244a4dd2287f4ece45d49228b4d529178"}, - {file = "numpy-1.26.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b1240f767f69d7c4c8a29adde2310b871153df9b26b5cb2b54a561ac85146485"}, - {file = "numpy-1.26.3-cp310-cp310-win32.whl", hash = "sha256:21a9484e75ad018974a2fdaa216524d64ed4212e418e0a551a2d83403b0531d3"}, - {file = "numpy-1.26.3-cp310-cp310-win_amd64.whl", hash = "sha256:9e1591f6ae98bcfac2a4bbf9221c0b92ab49762228f38287f6eeb5f3f55905ce"}, - {file = "numpy-1.26.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b831295e5472954104ecb46cd98c08b98b49c69fdb7040483aff799a755a7374"}, - {file = "numpy-1.26.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9e87562b91f68dd8b1c39149d0323b42e0082db7ddb8e934ab4c292094d575d6"}, - {file = "numpy-1.26.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c66d6fec467e8c0f975818c1796d25c53521124b7cfb760114be0abad53a0a2"}, - {file = "numpy-1.26.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f25e2811a9c932e43943a2615e65fc487a0b6b49218899e62e426e7f0a57eeda"}, - {file = "numpy-1.26.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:af36e0aa45e25c9f57bf684b1175e59ea05d9a7d3e8e87b7ae1a1da246f2767e"}, - {file = "numpy-1.26.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:51c7f1b344f302067b02e0f5b5d2daa9ed4a721cf49f070280ac202738ea7f00"}, - {file = "numpy-1.26.3-cp311-cp311-win32.whl", hash = "sha256:7ca4f24341df071877849eb2034948459ce3a07915c2734f1abb4018d9c49d7b"}, - {file = "numpy-1.26.3-cp311-cp311-win_amd64.whl", hash = "sha256:39763aee6dfdd4878032361b30b2b12593fb445ddb66bbac802e2113eb8a6ac4"}, - {file = "numpy-1.26.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a7081fd19a6d573e1a05e600c82a1c421011db7935ed0d5c483e9dd96b99cf13"}, - {file = "numpy-1.26.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12c70ac274b32bc00c7f61b515126c9205323703abb99cd41836e8125ea0043e"}, - {file = "numpy-1.26.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f784e13e598e9594750b2ef6729bcd5a47f6cfe4a12cca13def35e06d8163e3"}, - {file = "numpy-1.26.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f24750ef94d56ce6e33e4019a8a4d68cfdb1ef661a52cdaee628a56d2437419"}, - {file = "numpy-1.26.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:77810ef29e0fb1d289d225cabb9ee6cf4d11978a00bb99f7f8ec2132a84e0166"}, - {file = "numpy-1.26.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8ed07a90f5450d99dad60d3799f9c03c6566709bd53b497eb9ccad9a55867f36"}, - {file = "numpy-1.26.3-cp312-cp312-win32.whl", hash = "sha256:f73497e8c38295aaa4741bdfa4fda1a5aedda5473074369eca10626835445511"}, - {file = "numpy-1.26.3-cp312-cp312-win_amd64.whl", hash = "sha256:da4b0c6c699a0ad73c810736303f7fbae483bcb012e38d7eb06a5e3b432c981b"}, - {file = "numpy-1.26.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1666f634cb3c80ccbd77ec97bc17337718f56d6658acf5d3b906ca03e90ce87f"}, - {file = "numpy-1.26.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18c3319a7d39b2c6a9e3bb75aab2304ab79a811ac0168a671a62e6346c29b03f"}, - {file = "numpy-1.26.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b7e807d6888da0db6e7e75838444d62495e2b588b99e90dd80c3459594e857b"}, - {file = "numpy-1.26.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4d362e17bcb0011738c2d83e0a65ea8ce627057b2fdda37678f4374a382a137"}, - {file = "numpy-1.26.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b8c275f0ae90069496068c714387b4a0eba5d531aace269559ff2b43655edd58"}, - {file = "numpy-1.26.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cc0743f0302b94f397a4a65a660d4cd24267439eb16493fb3caad2e4389bccbb"}, - {file = "numpy-1.26.3-cp39-cp39-win32.whl", hash = "sha256:9bc6d1a7f8cedd519c4b7b1156d98e051b726bf160715b769106661d567b3f03"}, - {file = "numpy-1.26.3-cp39-cp39-win_amd64.whl", hash = "sha256:867e3644e208c8922a3be26fc6bbf112a035f50f0a86497f98f228c50c607bb2"}, - {file = "numpy-1.26.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3c67423b3703f8fbd90f5adaa37f85b5794d3366948efe9a5190a5f3a83fc34e"}, - {file = "numpy-1.26.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46f47ee566d98849323f01b349d58f2557f02167ee301e5e28809a8c0e27a2d0"}, - {file = "numpy-1.26.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a8474703bffc65ca15853d5fd4d06b18138ae90c17c8d12169968e998e448bb5"}, - {file = "numpy-1.26.3.tar.gz", hash = "sha256:697df43e2b6310ecc9d95f05d5ef20eacc09c7c4ecc9da3f235d39e71b7da1e4"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, ] [[package]] name = "opencv-python" -version = "4.9.0.80" +version = "4.11.0.86" description = "Wrapper package for OpenCV python bindings." optional = false python-versions = ">=3.6" files = [ - {file = "opencv-python-4.9.0.80.tar.gz", hash = "sha256:1a9f0e6267de3a1a1db0c54213d022c7c8b5b9ca4b580e80bdc58516c922c9e1"}, - {file = "opencv_python-4.9.0.80-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:7e5f7aa4486651a6ebfa8ed4b594b65bd2d2f41beeb4241a3e4b1b85acbbbadb"}, - {file = "opencv_python-4.9.0.80-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:71dfb9555ccccdd77305fc3dcca5897fbf0cf28b297c51ee55e079c065d812a3"}, - {file = "opencv_python-4.9.0.80-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b34a52e9da36dda8c151c6394aed602e4b17fa041df0b9f5b93ae10b0fcca2a"}, - {file = "opencv_python-4.9.0.80-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4088cab82b66a3b37ffc452976b14a3c599269c247895ae9ceb4066d8188a57"}, - {file = "opencv_python-4.9.0.80-cp37-abi3-win32.whl", hash = "sha256:dcf000c36dd1651118a2462257e3a9e76db789a78432e1f303c7bac54f63ef6c"}, - {file = "opencv_python-4.9.0.80-cp37-abi3-win_amd64.whl", hash = "sha256:3f16f08e02b2a2da44259c7cc712e779eff1dd8b55fdb0323e8cab09548086c0"}, + {file = "opencv-python-4.11.0.86.tar.gz", hash = "sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4"}, + {file = "opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:432f67c223f1dc2824f5e73cdfcd9db0efc8710647d4e813012195dc9122a52a"}, + {file = "opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:9d05ef13d23fe97f575153558653e2d6e87103995d54e6a35db3f282fe1f9c66"}, + {file = "opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b92ae2c8852208817e6776ba1ea0d6b1e0a1b5431e971a2a0ddd2a8cc398202"}, + {file = "opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b02611523803495003bd87362db3e1d2a0454a6a63025dc6658a9830570aa0d"}, + {file = "opencv_python-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:810549cb2a4aedaa84ad9a1c92fbfdfc14090e2749cedf2c1589ad8359aa169b"}, + {file = "opencv_python-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec"}, ] [package.dependencies] @@ -2841,117 +3200,167 @@ numpy = [ [[package]] name = "orjson" -version = "3.9.12" +version = "3.11.9" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false -python-versions = ">=3.8" -files = [ - {file = "orjson-3.9.12-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6b4e2bed7d00753c438e83b613923afdd067564ff7ed696bfe3a7b073a236e07"}, - {file = "orjson-3.9.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd1b8ec63f0bf54a50b498eedeccdca23bd7b658f81c524d18e410c203189365"}, - {file = "orjson-3.9.12-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab8add018a53665042a5ae68200f1ad14c7953fa12110d12d41166f111724656"}, - {file = "orjson-3.9.12-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12756a108875526b76e505afe6d6ba34960ac6b8c5ec2f35faf73ef161e97e07"}, - {file = "orjson-3.9.12-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:890e7519c0c70296253660455f77e3a194554a3c45e42aa193cdebc76a02d82b"}, - {file = "orjson-3.9.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d664880d7f016efbae97c725b243b33c2cbb4851ddc77f683fd1eec4a7894146"}, - {file = "orjson-3.9.12-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cfdaede0fa5b500314ec7b1249c7e30e871504a57004acd116be6acdda3b8ab3"}, - {file = "orjson-3.9.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6492ff5953011e1ba9ed1bf086835fd574bd0a3cbe252db8e15ed72a30479081"}, - {file = "orjson-3.9.12-cp310-none-win32.whl", hash = "sha256:29bf08e2eadb2c480fdc2e2daae58f2f013dff5d3b506edd1e02963b9ce9f8a9"}, - {file = "orjson-3.9.12-cp310-none-win_amd64.whl", hash = "sha256:0fc156fba60d6b50743337ba09f052d8afc8b64595112996d22f5fce01ab57da"}, - {file = "orjson-3.9.12-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2849f88a0a12b8d94579b67486cbd8f3a49e36a4cb3d3f0ab352c596078c730c"}, - {file = "orjson-3.9.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3186b18754befa660b31c649a108a915493ea69b4fc33f624ed854ad3563ac65"}, - {file = "orjson-3.9.12-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbbf313c9fb9d4f6cf9c22ced4b6682230457741daeb3d7060c5d06c2e73884a"}, - {file = "orjson-3.9.12-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99e8cd005b3926c3db9b63d264bd05e1bf4451787cc79a048f27f5190a9a0311"}, - {file = "orjson-3.9.12-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59feb148392d9155f3bfed0a2a3209268e000c2c3c834fb8fe1a6af9392efcbf"}, - {file = "orjson-3.9.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4ae815a172a1f073b05b9e04273e3b23e608a0858c4e76f606d2d75fcabde0c"}, - {file = "orjson-3.9.12-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed398f9a9d5a1bf55b6e362ffc80ac846af2122d14a8243a1e6510a4eabcb71e"}, - {file = "orjson-3.9.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d3cfb76600c5a1e6be91326b8f3b83035a370e727854a96d801c1ea08b708073"}, - {file = "orjson-3.9.12-cp311-none-win32.whl", hash = "sha256:a2b6f5252c92bcab3b742ddb3ac195c0fa74bed4319acd74f5d54d79ef4715dc"}, - {file = "orjson-3.9.12-cp311-none-win_amd64.whl", hash = "sha256:c95488e4aa1d078ff5776b58f66bd29d628fa59adcb2047f4efd3ecb2bd41a71"}, - {file = "orjson-3.9.12-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d6ce2062c4af43b92b0221ed4f445632c6bf4213f8a7da5396a122931377acd9"}, - {file = "orjson-3.9.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:950951799967558c214cd6cceb7ceceed6f81d2c3c4135ee4a2c9c69f58aa225"}, - {file = "orjson-3.9.12-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2dfaf71499d6fd4153f5c86eebb68e3ec1bf95851b030a4b55c7637a37bbdee4"}, - {file = "orjson-3.9.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:659a8d7279e46c97661839035a1a218b61957316bf0202674e944ac5cfe7ed83"}, - {file = "orjson-3.9.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af17fa87bccad0b7f6fd8ac8f9cbc9ee656b4552783b10b97a071337616db3e4"}, - {file = "orjson-3.9.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd52dec9eddf4c8c74392f3fd52fa137b5f2e2bed1d9ae958d879de5f7d7cded"}, - {file = "orjson-3.9.12-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:640e2b5d8e36b970202cfd0799d11a9a4ab46cf9212332cd642101ec952df7c8"}, - {file = "orjson-3.9.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:daa438bd8024e03bcea2c5a92cd719a663a58e223fba967296b6ab9992259dbf"}, - {file = "orjson-3.9.12-cp312-none-win_amd64.whl", hash = "sha256:1bb8f657c39ecdb924d02e809f992c9aafeb1ad70127d53fb573a6a6ab59d549"}, - {file = "orjson-3.9.12-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f4098c7674901402c86ba6045a551a2ee345f9f7ed54eeffc7d86d155c8427e5"}, - {file = "orjson-3.9.12-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5586a533998267458fad3a457d6f3cdbddbcce696c916599fa8e2a10a89b24d3"}, - {file = "orjson-3.9.12-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54071b7398cd3f90e4bb61df46705ee96cb5e33e53fc0b2f47dbd9b000e238e1"}, - {file = "orjson-3.9.12-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:67426651faa671b40443ea6f03065f9c8e22272b62fa23238b3efdacd301df31"}, - {file = "orjson-3.9.12-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4a0cd56e8ee56b203abae7d482ac0d233dbfb436bb2e2d5cbcb539fe1200a312"}, - {file = "orjson-3.9.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a84a0c3d4841a42e2571b1c1ead20a83e2792644c5827a606c50fc8af7ca4bee"}, - {file = "orjson-3.9.12-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:09d60450cda3fa6c8ed17770c3a88473a16460cd0ff2ba74ef0df663b6fd3bb8"}, - {file = "orjson-3.9.12-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bc82a4db9934a78ade211cf2e07161e4f068a461c1796465d10069cb50b32a80"}, - {file = "orjson-3.9.12-cp38-none-win32.whl", hash = "sha256:61563d5d3b0019804d782137a4f32c72dc44c84e7d078b89d2d2a1adbaa47b52"}, - {file = "orjson-3.9.12-cp38-none-win_amd64.whl", hash = "sha256:410f24309fbbaa2fab776e3212a81b96a1ec6037259359a32ea79fbccfcf76aa"}, - {file = "orjson-3.9.12-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e773f251258dd82795fd5daeac081d00b97bacf1548e44e71245543374874bcf"}, - {file = "orjson-3.9.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b159baecfda51c840a619948c25817d37733a4d9877fea96590ef8606468b362"}, - {file = "orjson-3.9.12-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:975e72e81a249174840d5a8df977d067b0183ef1560a32998be340f7e195c730"}, - {file = "orjson-3.9.12-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:06e42e899dde61eb1851a9fad7f1a21b8e4be063438399b63c07839b57668f6c"}, - {file = "orjson-3.9.12-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c157e999e5694475a5515942aebeed6e43f7a1ed52267c1c93dcfde7d78d421"}, - {file = "orjson-3.9.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dde1bc7c035f2d03aa49dc8642d9c6c9b1a81f2470e02055e76ed8853cfae0c3"}, - {file = "orjson-3.9.12-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b0e9d73cdbdad76a53a48f563447e0e1ce34bcecef4614eb4b146383e6e7d8c9"}, - {file = "orjson-3.9.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:96e44b21fe407b8ed48afbb3721f3c8c8ce17e345fbe232bd4651ace7317782d"}, - {file = "orjson-3.9.12-cp39-none-win32.whl", hash = "sha256:cbd0f3555205bf2a60f8812133f2452d498dbefa14423ba90fe89f32276f7abf"}, - {file = "orjson-3.9.12-cp39-none-win_amd64.whl", hash = "sha256:03ea7ee7e992532c2f4a06edd7ee1553f0644790553a118e003e3c405add41fa"}, - {file = "orjson-3.9.12.tar.gz", hash = "sha256:da908d23a3b3243632b523344403b128722a5f45e278a8343c2bb67538dff0e4"}, +python-versions = ">=3.10" +files = [ + {file = "orjson-3.11.9-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:135869ef917b8704ea0a94e01620e0c05021c15c52036e4663baffe75e72f8ce"}, + {file = "orjson-3.11.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:115ab5f5f4a0f203cc2a5f0fb09aee503a3f771aa08392949ab5ca230c4fbdbd"}, + {file = "orjson-3.11.9-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4da3c38a2083ca4aaf9c2a36776cce3e9328e6647b10d118948f3cfb4913ffe4"}, + {file = "orjson-3.11.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53b50b0e14084b8f7e29c5ce84c5af0f1160169b30d8a6914231d97d2fe297d4"}, + {file = "orjson-3.11.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:231742b4a11dad8d5380a435962c57e91b7c37b79be858f4ef1c0df1a259897e"}, + {file = "orjson-3.11.9-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34fd2317602587321faab75ab76c623a0117e80841a6413654f04e47f339a8fb"}, + {file = "orjson-3.11.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71f3db16e69b667b132e0f305a833d5497da302d801508cbb051ed9a9819da47"}, + {file = "orjson-3.11.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0b34789fa0da61cf7bef0546b09c738fb195331e017e477096d129e9105ab03d"}, + {file = "orjson-3.11.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:87e4d4ab280b0c87424d47695bec2182caf8cfc17879ea78dab76680194abc13"}, + {file = "orjson-3.11.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ace6c58523302d3b97b6ac5c38a5298a54b473762b6be82726b4265c41029f92"}, + {file = "orjson-3.11.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:97d0d932803c1b164fde11cb542a9efcb1e0f63b184537cca65887147906ff48"}, + {file = "orjson-3.11.9-cp310-cp310-win32.whl", hash = "sha256:b3afcf569c15577a9fe64627292daa3e6b3a70f4fb77a5df246a87ec21681b94"}, + {file = "orjson-3.11.9-cp310-cp310-win_amd64.whl", hash = "sha256:8697ab6a080a5c46edaad50e2bc5bd8c7ca5c66442d24104fa44ec74910a8244"}, + {file = "orjson-3.11.9-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f"}, + {file = "orjson-3.11.9-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c"}, + {file = "orjson-3.11.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5"}, + {file = "orjson-3.11.9-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c"}, + {file = "orjson-3.11.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:277fefe9d76ee17eb14debf399e3533d4d63b5f677a4d3719eb763536af1f4bd"}, + {file = "orjson-3.11.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62"}, + {file = "orjson-3.11.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877"}, + {file = "orjson-3.11.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1"}, + {file = "orjson-3.11.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd"}, + {file = "orjson-3.11.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff"}, + {file = "orjson-3.11.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:16969c9d369c98eb084889c6e4d2d39b77c7eb38ceccf8da2a9fff62ae908980"}, + {file = "orjson-3.11.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2"}, + {file = "orjson-3.11.9-cp311-cp311-win32.whl", hash = "sha256:14ed654580c1ed2bc217352ec82f91b047aef82951aa71c7f64e0dcb03c0e180"}, + {file = "orjson-3.11.9-cp311-cp311-win_amd64.whl", hash = "sha256:57ea77fb70a448ce87d18fca050193202a3da5e54598f6501ca5476fb66cfe02"}, + {file = "orjson-3.11.9-cp311-cp311-win_arm64.whl", hash = "sha256:19b72ed11572a2ee51a67a903afbe5af504f84ed6f529c0fe44b0ab3fb5cc697"}, + {file = "orjson-3.11.9-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49"}, + {file = "orjson-3.11.9-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291"}, + {file = "orjson-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09"}, + {file = "orjson-3.11.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4"}, + {file = "orjson-3.11.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882"}, + {file = "orjson-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff"}, + {file = "orjson-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe"}, + {file = "orjson-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61"}, + {file = "orjson-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2"}, + {file = "orjson-3.11.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206"}, + {file = "orjson-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f"}, + {file = "orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa"}, + {file = "orjson-3.11.9-cp312-cp312-win32.whl", hash = "sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470"}, + {file = "orjson-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be"}, + {file = "orjson-3.11.9-cp312-cp312-win_arm64.whl", hash = "sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624"}, + {file = "orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021"}, + {file = "orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c"}, + {file = "orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81"}, + {file = "orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a"}, + {file = "orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10"}, + {file = "orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362"}, + {file = "orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97"}, + {file = "orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218"}, + {file = "orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9"}, + {file = "orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677"}, + {file = "orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4"}, + {file = "orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0"}, + {file = "orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32"}, + {file = "orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979"}, + {file = "orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254"}, + {file = "orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e"}, + {file = "orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e"}, + {file = "orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0"}, + {file = "orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124"}, + {file = "orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c"}, + {file = "orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7"}, + {file = "orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1"}, + {file = "orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db"}, + {file = "orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b"}, + {file = "orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972"}, + {file = "orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0"}, + {file = "orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586"}, + {file = "orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673"}, + {file = "orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b"}, + {file = "orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9"}, + {file = "orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f"}, ] [[package]] name = "packaging" -version = "23.2" +version = "26.2" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, + {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"}, + {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, ] [[package]] name = "pandas" -version = "2.2.0" +version = "2.3.3" description = "Powerful data structures for data analysis, time series, and statistics" optional = false python-versions = ">=3.9" files = [ - {file = "pandas-2.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8108ee1712bb4fa2c16981fba7e68b3f6ea330277f5ca34fa8d557e986a11670"}, - {file = "pandas-2.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:736da9ad4033aeab51d067fc3bd69a0ba36f5a60f66a527b3d72e2030e63280a"}, - {file = "pandas-2.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38e0b4fc3ddceb56ec8a287313bc22abe17ab0eb184069f08fc6a9352a769b18"}, - {file = "pandas-2.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20404d2adefe92aed3b38da41d0847a143a09be982a31b85bc7dd565bdba0f4e"}, - {file = "pandas-2.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7ea3ee3f125032bfcade3a4cf85131ed064b4f8dd23e5ce6fa16473e48ebcaf5"}, - {file = "pandas-2.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f9670b3ac00a387620489dfc1bca66db47a787f4e55911f1293063a78b108df1"}, - {file = "pandas-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a946f210383c7e6d16312d30b238fd508d80d927014f3b33fb5b15c2f895430"}, - {file = "pandas-2.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a1b438fa26b208005c997e78672f1aa8138f67002e833312e6230f3e57fa87d5"}, - {file = "pandas-2.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ce2fbc8d9bf303ce54a476116165220a1fedf15985b09656b4b4275300e920b"}, - {file = "pandas-2.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2707514a7bec41a4ab81f2ccce8b382961a29fbe9492eab1305bb075b2b1ff4f"}, - {file = "pandas-2.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85793cbdc2d5bc32620dc8ffa715423f0c680dacacf55056ba13454a5be5de88"}, - {file = "pandas-2.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cfd6c2491dc821b10c716ad6776e7ab311f7df5d16038d0b7458bc0b67dc10f3"}, - {file = "pandas-2.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a146b9dcacc3123aa2b399df1a284de5f46287a4ab4fbfc237eac98a92ebcb71"}, - {file = "pandas-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbc1b53c0e1fdf16388c33c3cca160f798d38aea2978004dd3f4d3dec56454c9"}, - {file = "pandas-2.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a41d06f308a024981dcaa6c41f2f2be46a6b186b902c94c2674e8cb5c42985bc"}, - {file = "pandas-2.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:159205c99d7a5ce89ecfc37cb08ed179de7783737cea403b295b5eda8e9c56d1"}, - {file = "pandas-2.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb1e1f3861ea9132b32f2133788f3b14911b68102d562715d71bd0013bc45440"}, - {file = "pandas-2.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:761cb99b42a69005dec2b08854fb1d4888fdf7b05db23a8c5a099e4b886a2106"}, - {file = "pandas-2.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a20628faaf444da122b2a64b1e5360cde100ee6283ae8effa0d8745153809a2e"}, - {file = "pandas-2.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f5be5d03ea2073627e7111f61b9f1f0d9625dc3c4d8dda72cc827b0c58a1d042"}, - {file = "pandas-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:a626795722d893ed6aacb64d2401d017ddc8a2341b49e0384ab9bf7112bdec30"}, - {file = "pandas-2.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9f66419d4a41132eb7e9a73dcec9486cf5019f52d90dd35547af11bc58f8637d"}, - {file = "pandas-2.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:57abcaeda83fb80d447f28ab0cc7b32b13978f6f733875ebd1ed14f8fbc0f4ab"}, - {file = "pandas-2.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e60f1f7dba3c2d5ca159e18c46a34e7ca7247a73b5dd1a22b6d59707ed6b899a"}, - {file = "pandas-2.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb61dc8567b798b969bcc1fc964788f5a68214d333cade8319c7ab33e2b5d88a"}, - {file = "pandas-2.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:52826b5f4ed658fa2b729264d63f6732b8b29949c7fd234510d57c61dbeadfcd"}, - {file = "pandas-2.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bde2bc699dbd80d7bc7f9cab1e23a95c4375de615860ca089f34e7c64f4a8de7"}, - {file = "pandas-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:3de918a754bbf2da2381e8a3dcc45eede8cd7775b047b923f9006d5f876802ae"}, - {file = "pandas-2.2.0.tar.gz", hash = "sha256:30b83f7c3eb217fb4d1b494a57a2fda5444f17834f5df2de6b2ffff68dc3c8e2"}, + {file = "pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c"}, + {file = "pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a"}, + {file = "pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1"}, + {file = "pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838"}, + {file = "pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250"}, + {file = "pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4"}, + {file = "pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826"}, + {file = "pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523"}, + {file = "pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45"}, + {file = "pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66"}, + {file = "pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b"}, + {file = "pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791"}, + {file = "pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151"}, + {file = "pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c"}, + {file = "pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53"}, + {file = "pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35"}, + {file = "pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908"}, + {file = "pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89"}, + {file = "pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98"}, + {file = "pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084"}, + {file = "pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b"}, + {file = "pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713"}, + {file = "pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8"}, + {file = "pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d"}, + {file = "pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac"}, + {file = "pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c"}, + {file = "pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493"}, + {file = "pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee"}, + {file = "pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5"}, + {file = "pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21"}, + {file = "pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78"}, + {file = "pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110"}, + {file = "pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86"}, + {file = "pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc"}, + {file = "pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0"}, + {file = "pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593"}, + {file = "pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c"}, + {file = "pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b"}, + {file = "pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6"}, + {file = "pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3"}, + {file = "pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5"}, + {file = "pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec"}, + {file = "pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7"}, + {file = "pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450"}, + {file = "pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5"}, + {file = "pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788"}, + {file = "pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87"}, + {file = "pandas-2.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c503ba5216814e295f40711470446bc3fd00f0faea8a086cbc688808e26f92a2"}, + {file = "pandas-2.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a637c5cdfa04b6d6e2ecedcb81fc52ffb0fd78ce2ebccc9ea964df9f658de8c8"}, + {file = "pandas-2.3.3-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:854d00d556406bffe66a4c0802f334c9ad5a96b4f1f868adf036a21b11ef13ff"}, + {file = "pandas-2.3.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf1f8a81d04ca90e32a0aceb819d34dbd378a98bf923b6398b9a3ec0bf44de29"}, + {file = "pandas-2.3.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23ebd657a4d38268c7dfbdf089fbc31ea709d82e4923c5ffd4fbd5747133ce73"}, + {file = "pandas-2.3.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5554c929ccc317d41a5e3d1234f3be588248e61f08a74dd17c9eabb535777dc9"}, + {file = "pandas-2.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:d3e28b3e83862ccf4d85ff19cf8c20b2ae7e503881711ff2d534dc8f761131aa"}, + {file = "pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b"}, ] [package.dependencies] numpy = [ - {version = ">=1.22.4,<2", markers = "python_version < \"3.11\""}, - {version = ">=1.23.2,<2", markers = "python_version == \"3.11\""}, - {version = ">=1.26.0,<2", markers = "python_version >= \"3.12\""}, + {version = ">=1.22.4", markers = "python_version < \"3.11\""}, + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, ] python-dateutil = ">=2.8.2" pytz = ">=2020.1" @@ -2976,6 +3385,7 @@ parquet = ["pyarrow (>=10.0.1)"] performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] plot = ["matplotlib (>=3.6.3)"] postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] +pyarrow = ["pyarrow (>=10.0.1)"] spss = ["pyreadstat (>=1.2.0)"] sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] @@ -3011,140 +3421,143 @@ pyasn1 = "*" [[package]] name = "pillow" -version = "10.4.0" -description = "Python Imaging Library (Fork)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"}, - {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"}, - {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"}, - {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"}, - {file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"}, - {file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"}, - {file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"}, - {file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"}, - {file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"}, - {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"}, - {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"}, - {file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"}, - {file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"}, - {file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"}, - {file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"}, - {file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"}, - {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"}, - {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"}, - {file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"}, - {file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"}, - {file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"}, - {file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"}, - {file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"}, - {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"}, - {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"}, - {file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"}, - {file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"}, - {file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"}, - {file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"}, - {file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"}, - {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"}, - {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"}, - {file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"}, - {file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"}, - {file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"}, - {file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"}, - {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"}, - {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"}, - {file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"}, - {file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"}, - {file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"}, - {file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"}, +version = "12.3.0" +description = "Python Imaging Library (fork)" +optional = false +python-versions = ">=3.10" +files = [ + {file = "pillow-12.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a"}, + {file = "pillow-12.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7"}, + {file = "pillow-12.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f"}, + {file = "pillow-12.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec"}, + {file = "pillow-12.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468"}, + {file = "pillow-12.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed"}, + {file = "pillow-12.3.0-cp310-cp310-win32.whl", hash = "sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1"}, + {file = "pillow-12.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb"}, + {file = "pillow-12.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f"}, + {file = "pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756"}, + {file = "pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6"}, + {file = "pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd"}, + {file = "pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd"}, + {file = "pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c"}, + {file = "pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5"}, + {file = "pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b"}, + {file = "pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a"}, + {file = "pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26"}, + {file = "pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965"}, + {file = "pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7"}, + {file = "pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9"}, + {file = "pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91"}, + {file = "pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c"}, + {file = "pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df"}, + {file = "pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f"}, + {file = "pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09"}, + {file = "pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510"}, + {file = "pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89"}, + {file = "pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace"}, + {file = "pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec"}, + {file = "pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66"}, + {file = "pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35"}, + {file = "pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65"}, + {file = "pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3"}, + {file = "pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a"}, + {file = "pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e"}, + {file = "pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f"}, + {file = "pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8"}, + {file = "pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b"}, + {file = "pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330"}, + {file = "pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217"}, + {file = "pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930"}, + {file = "pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8"}, + {file = "pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0"}, + {file = "pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321"}, + {file = "pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b"}, + {file = "pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198"}, + {file = "pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130"}, + {file = "pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a"}, + {file = "pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d"}, + {file = "pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838"}, + {file = "pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e"}, + {file = "pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17"}, + {file = "pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385"}, + {file = "pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c"}, + {file = "pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d"}, + {file = "pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931"}, + {file = "pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7"}, + {file = "pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c"}, + {file = "pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45"}, + {file = "pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139"}, + {file = "pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402"}, + {file = "pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c"}, + {file = "pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f"}, + {file = "pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701"}, + {file = "pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace"}, + {file = "pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4"}, + {file = "pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39"}, + {file = "pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71"}, + {file = "pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827"}, + {file = "pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5"}, + {file = "pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658"}, + {file = "pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf"}, + {file = "pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64"}, + {file = "pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e"}, + {file = "pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777"}, + {file = "pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1"}, + {file = "pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9"}, + {file = "pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8"}, + {file = "pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418"}, + {file = "pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59"}, + {file = "pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468"}, + {file = "pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94"}, + {file = "pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e"}, + {file = "pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3"}, + {file = "pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a"}, + {file = "pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce"}, ] [package.extras] -docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-autobuild", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] fpx = ["olefile"] mic = ["olefile"] -tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] -typing = ["typing-extensions"] +test-arrow = ["arro3-compute", "arro3-core", "nanoarrow", "pyarrow"] +tests = ["coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "setuptools", "trove-classifiers (>=2024.10.12)"] xmp = ["defusedxml"] [[package]] name = "platformdirs" -version = "4.1.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "4.10.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" files = [ - {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, - {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, + {file = "platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a"}, + {file = "platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7"}, ] -[package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] - [[package]] name = "pluggy" -version = "1.4.0" +version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, - {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, ] [package.extras] dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] +testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "3.6.0" +version = "3.8.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" files = [ - {file = "pre_commit-3.6.0-py2.py3-none-any.whl", hash = "sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376"}, - {file = "pre_commit-3.6.0.tar.gz", hash = "sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d"}, + {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, + {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, ] [package.dependencies] @@ -3156,261 +3569,326 @@ virtualenv = ">=20.10.0" [[package]] name = "propcache" -version = "0.3.0" +version = "0.5.2" description = "Accelerated property cache" optional = false -python-versions = ">=3.9" -files = [ - {file = "propcache-0.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:efa44f64c37cc30c9f05932c740a8b40ce359f51882c70883cc95feac842da4d"}, - {file = "propcache-0.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2383a17385d9800b6eb5855c2f05ee550f803878f344f58b6e194de08b96352c"}, - {file = "propcache-0.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3e7420211f5a65a54675fd860ea04173cde60a7cc20ccfbafcccd155225f8bc"}, - {file = "propcache-0.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3302c5287e504d23bb0e64d2a921d1eb4a03fb93a0a0aa3b53de059f5a5d737d"}, - {file = "propcache-0.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7e2e068a83552ddf7a39a99488bcba05ac13454fb205c847674da0352602082f"}, - {file = "propcache-0.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d913d36bdaf368637b4f88d554fb9cb9d53d6920b9c5563846555938d5450bf"}, - {file = "propcache-0.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ee1983728964d6070ab443399c476de93d5d741f71e8f6e7880a065f878e0b9"}, - {file = "propcache-0.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:36ca5e9a21822cc1746023e88f5c0af6fce3af3b85d4520efb1ce4221bed75cc"}, - {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9ecde3671e62eeb99e977f5221abcf40c208f69b5eb986b061ccec317c82ebd0"}, - {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d383bf5e045d7f9d239b38e6acadd7b7fdf6c0087259a84ae3475d18e9a2ae8b"}, - {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:8cb625bcb5add899cb8ba7bf716ec1d3e8f7cdea9b0713fa99eadf73b6d4986f"}, - {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5fa159dcee5dba00c1def3231c249cf261185189205073bde13797e57dd7540a"}, - {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a7080b0159ce05f179cfac592cda1a82898ca9cd097dacf8ea20ae33474fbb25"}, - {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed7161bccab7696a473fe7ddb619c1d75963732b37da4618ba12e60899fefe4f"}, - {file = "propcache-0.3.0-cp310-cp310-win32.whl", hash = "sha256:bf0d9a171908f32d54f651648c7290397b8792f4303821c42a74e7805bfb813c"}, - {file = "propcache-0.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:42924dc0c9d73e49908e35bbdec87adedd651ea24c53c29cac103ede0ea1d340"}, - {file = "propcache-0.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9ddd49258610499aab83b4f5b61b32e11fce873586282a0e972e5ab3bcadee51"}, - {file = "propcache-0.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2578541776769b500bada3f8a4eeaf944530516b6e90c089aa368266ed70c49e"}, - {file = "propcache-0.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8074c5dd61c8a3e915fa8fc04754fa55cfa5978200d2daa1e2d4294c1f136aa"}, - {file = "propcache-0.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b58229a844931bca61b3a20efd2be2a2acb4ad1622fc026504309a6883686fbf"}, - {file = "propcache-0.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e45377d5d6fefe1677da2a2c07b024a6dac782088e37c0b1efea4cfe2b1be19b"}, - {file = "propcache-0.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ec5060592d83454e8063e487696ac3783cc48c9a329498bafae0d972bc7816c9"}, - {file = "propcache-0.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15010f29fbed80e711db272909a074dc79858c6d28e2915704cfc487a8ac89c6"}, - {file = "propcache-0.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a254537b9b696ede293bfdbc0a65200e8e4507bc9f37831e2a0318a9b333c85c"}, - {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2b975528998de037dfbc10144b8aed9b8dd5a99ec547f14d1cb7c5665a43f075"}, - {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:19d36bb351ad5554ff20f2ae75f88ce205b0748c38b146c75628577020351e3c"}, - {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6032231d4a5abd67c7f71168fd64a47b6b451fbcb91c8397c2f7610e67683810"}, - {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6985a593417cdbc94c7f9c3403747335e450c1599da1647a5af76539672464d3"}, - {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6a1948df1bb1d56b5e7b0553c0fa04fd0e320997ae99689488201f19fa90d2e7"}, - {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8319293e85feadbbfe2150a5659dbc2ebc4afdeaf7d98936fb9a2f2ba0d4c35c"}, - {file = "propcache-0.3.0-cp311-cp311-win32.whl", hash = "sha256:63f26258a163c34542c24808f03d734b338da66ba91f410a703e505c8485791d"}, - {file = "propcache-0.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:cacea77ef7a2195f04f9279297684955e3d1ae4241092ff0cfcef532bb7a1c32"}, - {file = "propcache-0.3.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e53d19c2bf7d0d1e6998a7e693c7e87300dd971808e6618964621ccd0e01fe4e"}, - {file = "propcache-0.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a61a68d630e812b67b5bf097ab84e2cd79b48c792857dc10ba8a223f5b06a2af"}, - {file = "propcache-0.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fb91d20fa2d3b13deea98a690534697742029f4fb83673a3501ae6e3746508b5"}, - {file = "propcache-0.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67054e47c01b7b349b94ed0840ccae075449503cf1fdd0a1fdd98ab5ddc2667b"}, - {file = "propcache-0.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:997e7b8f173a391987df40f3b52c423e5850be6f6df0dcfb5376365440b56667"}, - {file = "propcache-0.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d663fd71491dde7dfdfc899d13a067a94198e90695b4321084c6e450743b8c7"}, - {file = "propcache-0.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8884ba1a0fe7210b775106b25850f5e5a9dc3c840d1ae9924ee6ea2eb3acbfe7"}, - {file = "propcache-0.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa806bbc13eac1ab6291ed21ecd2dd426063ca5417dd507e6be58de20e58dfcf"}, - {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6f4d7a7c0aff92e8354cceca6fe223973ddf08401047920df0fcb24be2bd5138"}, - {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:9be90eebc9842a93ef8335291f57b3b7488ac24f70df96a6034a13cb58e6ff86"}, - {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bf15fc0b45914d9d1b706f7c9c4f66f2b7b053e9517e40123e137e8ca8958b3d"}, - {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a16167118677d94bb48bfcd91e420088854eb0737b76ec374b91498fb77a70e"}, - {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:41de3da5458edd5678b0f6ff66691507f9885f5fe6a0fb99a5d10d10c0fd2d64"}, - {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:728af36011bb5d344c4fe4af79cfe186729efb649d2f8b395d1572fb088a996c"}, - {file = "propcache-0.3.0-cp312-cp312-win32.whl", hash = "sha256:6b5b7fd6ee7b54e01759f2044f936dcf7dea6e7585f35490f7ca0420fe723c0d"}, - {file = "propcache-0.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:2d15bc27163cd4df433e75f546b9ac31c1ba7b0b128bfb1b90df19082466ff57"}, - {file = "propcache-0.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a2b9bf8c79b660d0ca1ad95e587818c30ccdb11f787657458d6f26a1ea18c568"}, - {file = "propcache-0.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b0c1a133d42c6fc1f5fbcf5c91331657a1ff822e87989bf4a6e2e39b818d0ee9"}, - {file = "propcache-0.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bb2f144c6d98bb5cbc94adeb0447cfd4c0f991341baa68eee3f3b0c9c0e83767"}, - {file = "propcache-0.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1323cd04d6e92150bcc79d0174ce347ed4b349d748b9358fd2e497b121e03c8"}, - {file = "propcache-0.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b812b3cb6caacd072276ac0492d249f210006c57726b6484a1e1805b3cfeea0"}, - {file = "propcache-0.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:742840d1d0438eb7ea4280f3347598f507a199a35a08294afdcc560c3739989d"}, - {file = "propcache-0.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c6e7e4f9167fddc438cd653d826f2222222564daed4116a02a184b464d3ef05"}, - {file = "propcache-0.3.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a94ffc66738da99232ddffcf7910e0f69e2bbe3a0802e54426dbf0714e1c2ffe"}, - {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c6ec957025bf32b15cbc6b67afe233c65b30005e4c55fe5768e4bb518d712f1"}, - {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:549722908de62aa0b47a78b90531c022fa6e139f9166be634f667ff45632cc92"}, - {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5d62c4f6706bff5d8a52fd51fec6069bef69e7202ed481486c0bc3874912c787"}, - {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:24c04f8fbf60094c531667b8207acbae54146661657a1b1be6d3ca7773b7a545"}, - {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7c5f5290799a3f6539cc5e6f474c3e5c5fbeba74a5e1e5be75587746a940d51e"}, - {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4fa0e7c9c3cf7c276d4f6ab9af8adddc127d04e0fcabede315904d2ff76db626"}, - {file = "propcache-0.3.0-cp313-cp313-win32.whl", hash = "sha256:ee0bd3a7b2e184e88d25c9baa6a9dc609ba25b76daae942edfb14499ac7ec374"}, - {file = "propcache-0.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1c8f7d896a16da9455f882870a507567d4f58c53504dc2d4b1e1d386dfe4588a"}, - {file = "propcache-0.3.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e560fd75aaf3e5693b91bcaddd8b314f4d57e99aef8a6c6dc692f935cc1e6bbf"}, - {file = "propcache-0.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:65a37714b8ad9aba5780325228598a5b16c47ba0f8aeb3dc0514701e4413d7c0"}, - {file = "propcache-0.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:07700939b2cbd67bfb3b76a12e1412405d71019df00ca5697ce75e5ef789d829"}, - {file = "propcache-0.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c0fdbdf6983526e269e5a8d53b7ae3622dd6998468821d660d0daf72779aefa"}, - {file = "propcache-0.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:794c3dd744fad478b6232289c866c25406ecdfc47e294618bdf1697e69bd64a6"}, - {file = "propcache-0.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4544699674faf66fb6b4473a1518ae4999c1b614f0b8297b1cef96bac25381db"}, - {file = "propcache-0.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fddb8870bdb83456a489ab67c6b3040a8d5a55069aa6f72f9d872235fbc52f54"}, - {file = "propcache-0.3.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f857034dc68d5ceb30fb60afb6ff2103087aea10a01b613985610e007053a121"}, - {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:02df07041e0820cacc8f739510078f2aadcfd3fc57eaeeb16d5ded85c872c89e"}, - {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f47d52fd9b2ac418c4890aad2f6d21a6b96183c98021f0a48497a904199f006e"}, - {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9ff4e9ecb6e4b363430edf2c6e50173a63e0820e549918adef70515f87ced19a"}, - {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ecc2920630283e0783c22e2ac94427f8cca29a04cfdf331467d4f661f4072dac"}, - {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:c441c841e82c5ba7a85ad25986014be8d7849c3cfbdb6004541873505929a74e"}, - {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c929916cbdb540d3407c66f19f73387f43e7c12fa318a66f64ac99da601bcdf"}, - {file = "propcache-0.3.0-cp313-cp313t-win32.whl", hash = "sha256:0c3e893c4464ebd751b44ae76c12c5f5c1e4f6cbd6fbf67e3783cd93ad221863"}, - {file = "propcache-0.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:75e872573220d1ee2305b35c9813626e620768248425f58798413e9c39741f46"}, - {file = "propcache-0.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:03c091bb752349402f23ee43bb2bff6bd80ccab7c9df6b88ad4322258d6960fc"}, - {file = "propcache-0.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:46ed02532cb66612d42ae5c3929b5e98ae330ea0f3900bc66ec5f4862069519b"}, - {file = "propcache-0.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11ae6a8a01b8a4dc79093b5d3ca2c8a4436f5ee251a9840d7790dccbd96cb649"}, - {file = "propcache-0.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df03cd88f95b1b99052b52b1bb92173229d7a674df0ab06d2b25765ee8404bce"}, - {file = "propcache-0.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03acd9ff19021bd0567582ac88f821b66883e158274183b9e5586f678984f8fe"}, - {file = "propcache-0.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd54895e4ae7d32f1e3dd91261df46ee7483a735017dc6f987904f194aa5fd14"}, - {file = "propcache-0.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26a67e5c04e3119594d8cfae517f4b9330c395df07ea65eab16f3d559b7068fe"}, - {file = "propcache-0.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee25f1ac091def37c4b59d192bbe3a206298feeb89132a470325bf76ad122a1e"}, - {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:58e6d2a5a7cb3e5f166fd58e71e9a4ff504be9dc61b88167e75f835da5764d07"}, - {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:be90c94570840939fecedf99fa72839aed70b0ced449b415c85e01ae67422c90"}, - {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:49ea05212a529c2caffe411e25a59308b07d6e10bf2505d77da72891f9a05641"}, - {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:119e244ab40f70a98c91906d4c1f4c5f2e68bd0b14e7ab0a06922038fae8a20f"}, - {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:507c5357a8d8b4593b97fb669c50598f4e6cccbbf77e22fa9598aba78292b4d7"}, - {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8526b0941ec5a40220fc4dfde76aed58808e2b309c03e9fa8e2260083ef7157f"}, - {file = "propcache-0.3.0-cp39-cp39-win32.whl", hash = "sha256:7cedd25e5f678f7738da38037435b340694ab34d424938041aa630d8bac42663"}, - {file = "propcache-0.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:bf4298f366ca7e1ad1d21bbb58300a6985015909964077afd37559084590c929"}, - {file = "propcache-0.3.0-py3-none-any.whl", hash = "sha256:67dda3c7325691c2081510e92c561f465ba61b975f481735aefdfc845d2cd043"}, - {file = "propcache-0.3.0.tar.gz", hash = "sha256:a8fd93de4e1d278046345f49e2238cdb298589325849b2645d4a94c53faeffc5"}, +python-versions = ">=3.10" +files = [ + {file = "propcache-0.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b"}, + {file = "propcache-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c"}, + {file = "propcache-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274"}, + {file = "propcache-0.5.2-cp310-cp310-win32.whl", hash = "sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe"}, + {file = "propcache-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d"}, + {file = "propcache-0.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f"}, + {file = "propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0"}, + {file = "propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82"}, + {file = "propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a"}, + {file = "propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031"}, + {file = "propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42"}, + {file = "propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4"}, + {file = "propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d"}, + {file = "propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757"}, + {file = "propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568"}, + {file = "propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191"}, + {file = "propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7"}, + {file = "propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4"}, + {file = "propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0"}, + {file = "propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c"}, + {file = "propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2"}, + {file = "propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821"}, + {file = "propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370"}, + {file = "propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6"}, + {file = "propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe"}, + {file = "propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427"}, +] + +[[package]] +name = "proto-plus" +version = "1.28.1" +description = "Beautiful, Pythonic protocol buffers" +optional = false +python-versions = ">=3.10" +files = [ + {file = "proto_plus-1.28.1-py3-none-any.whl", hash = "sha256:6660f5f1970874bdcfc3088b435188a36a37bd3596668f7d726417c4ae8cfbed"}, + {file = "proto_plus-1.28.1.tar.gz", hash = "sha256:832e68e7fe064cf90ab153b6e5eb935b27891bb89aaeb68b115e9b702f6cb168"}, ] +[package.dependencies] +protobuf = ">=4.25.8,<8.0.0" + +[package.extras] +testing = ["google-api-core (>=1.31.5)"] + [[package]] name = "protobuf" -version = "4.25.2" +version = "7.35.1" description = "" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" files = [ - {file = "protobuf-4.25.2-cp310-abi3-win32.whl", hash = "sha256:b50c949608682b12efb0b2717f53256f03636af5f60ac0c1d900df6213910fd6"}, - {file = "protobuf-4.25.2-cp310-abi3-win_amd64.whl", hash = "sha256:8f62574857ee1de9f770baf04dde4165e30b15ad97ba03ceac65f760ff018ac9"}, - {file = "protobuf-4.25.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:2db9f8fa64fbdcdc93767d3cf81e0f2aef176284071507e3ede160811502fd3d"}, - {file = "protobuf-4.25.2-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:10894a2885b7175d3984f2be8d9850712c57d5e7587a2410720af8be56cdaf62"}, - {file = "protobuf-4.25.2-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:fc381d1dd0516343f1440019cedf08a7405f791cd49eef4ae1ea06520bc1c020"}, - {file = "protobuf-4.25.2-cp38-cp38-win32.whl", hash = "sha256:33a1aeef4b1927431d1be780e87b641e322b88d654203a9e9d93f218ee359e61"}, - {file = "protobuf-4.25.2-cp38-cp38-win_amd64.whl", hash = "sha256:47f3de503fe7c1245f6f03bea7e8d3ec11c6c4a2ea9ef910e3221c8a15516d62"}, - {file = "protobuf-4.25.2-cp39-cp39-win32.whl", hash = "sha256:5e5c933b4c30a988b52e0b7c02641760a5ba046edc5e43d3b94a74c9fc57c1b3"}, - {file = "protobuf-4.25.2-cp39-cp39-win_amd64.whl", hash = "sha256:d66a769b8d687df9024f2985d5137a337f957a0916cf5464d1513eee96a63ff0"}, - {file = "protobuf-4.25.2-py3-none-any.whl", hash = "sha256:a8b7a98d4ce823303145bf3c1a8bdb0f2f4642a414b196f04ad9853ed0c8f830"}, - {file = "protobuf-4.25.2.tar.gz", hash = "sha256:fe599e175cb347efc8ee524bcd4b902d11f7262c0e569ececcb89995c15f0a5e"}, + {file = "protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6"}, + {file = "protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799"}, + {file = "protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4"}, + {file = "protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4"}, + {file = "protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30"}, + {file = "protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87"}, + {file = "protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9"}, + {file = "protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a"}, ] [[package]] name = "psycopg2" -version = "2.9.9" +version = "2.9.12" description = "psycopg2 - Python-PostgreSQL Database Adapter" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "psycopg2-2.9.9-cp310-cp310-win32.whl", hash = "sha256:38a8dcc6856f569068b47de286b472b7c473ac7977243593a288ebce0dc89516"}, - {file = "psycopg2-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:426f9f29bde126913a20a96ff8ce7d73fd8a216cfb323b1f04da402d452853c3"}, - {file = "psycopg2-2.9.9-cp311-cp311-win32.whl", hash = "sha256:ade01303ccf7ae12c356a5e10911c9e1c51136003a9a1d92f7aa9d010fb98372"}, - {file = "psycopg2-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:121081ea2e76729acfb0673ff33755e8703d45e926e416cb59bae3a86c6a4981"}, - {file = "psycopg2-2.9.9-cp312-cp312-win32.whl", hash = "sha256:d735786acc7dd25815e89cc4ad529a43af779db2e25aa7c626de864127e5a024"}, - {file = "psycopg2-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:a7653d00b732afb6fc597e29c50ad28087dcb4fbfb28e86092277a559ae4e693"}, - {file = "psycopg2-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:5e0d98cade4f0e0304d7d6f25bbfbc5bd186e07b38eac65379309c4ca3193efa"}, - {file = "psycopg2-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:7e2dacf8b009a1c1e843b5213a87f7c544b2b042476ed7755be813eaf4e8347a"}, - {file = "psycopg2-2.9.9-cp38-cp38-win32.whl", hash = "sha256:ff432630e510709564c01dafdbe996cb552e0b9f3f065eb89bdce5bd31fabf4c"}, - {file = "psycopg2-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:bac58c024c9922c23550af2a581998624d6e02350f4ae9c5f0bc642c633a2d5e"}, - {file = "psycopg2-2.9.9-cp39-cp39-win32.whl", hash = "sha256:c92811b2d4c9b6ea0285942b2e7cac98a59e166d59c588fe5cfe1eda58e72d59"}, - {file = "psycopg2-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:de80739447af31525feddeb8effd640782cf5998e1a4e9192ebdf829717e3913"}, - {file = "psycopg2-2.9.9.tar.gz", hash = "sha256:d1454bde93fb1e224166811694d600e746430c006fbb031ea06ecc2ea41bf156"}, + {file = "psycopg2-2.9.12-cp310-cp310-win_amd64.whl", hash = "sha256:d5fbe092315fb007c03544704e6d1e678a6c0378139d01cea433dc59edf041b4"}, + {file = "psycopg2-2.9.12-cp311-cp311-win_amd64.whl", hash = "sha256:2532c0cdc6ad18c9c35cd935cc3159712e14f05276a6d29a6435c52d24b840c1"}, + {file = "psycopg2-2.9.12-cp312-cp312-win_amd64.whl", hash = "sha256:83d48e66e18c301d832e93c984a7bcbc0f4ac3bb79e2137e3bc335978c756dc0"}, + {file = "psycopg2-2.9.12-cp313-cp313-win_amd64.whl", hash = "sha256:3d23e684927d37b95cee9a943f6927b04ae2fdcd056fd0e2a30929ee89fee5a9"}, + {file = "psycopg2-2.9.12-cp314-cp314-win_amd64.whl", hash = "sha256:a73d5513bfe929c56555006c7a9cc7ae6e4276aa99dd2b1e2544eb8bb54f8b23"}, + {file = "psycopg2-2.9.12-cp39-cp39-win_amd64.whl", hash = "sha256:09826a6b89714626a662275d03f21639f1c68d183e2dcc9ba134d463a3da753e"}, + {file = "psycopg2-2.9.12.tar.gz", hash = "sha256:1dedb1c7a1d8552c4a6044c6b1c41a52e6a8e2d144af83eccac758076b1b7c15"}, ] [[package]] name = "pyasn1" -version = "0.5.1" +version = "0.6.4" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +python-versions = ">=3.8" files = [ - {file = "pyasn1-0.5.1-py2.py3-none-any.whl", hash = "sha256:4439847c58d40b1d0a573d07e3856e95333f1976294494c325775aeca506eb58"}, - {file = "pyasn1-0.5.1.tar.gz", hash = "sha256:6d391a96e59b23130a5cfa74d6fd7f388dbbe26cc8f1edf39fdddf08d9d6676c"}, + {file = "pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b"}, + {file = "pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81"}, ] [[package]] name = "pyasn1-modules" -version = "0.3.0" +version = "0.4.2" description = "A collection of ASN.1-based protocols modules" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +python-versions = ">=3.8" files = [ - {file = "pyasn1_modules-0.3.0-py2.py3-none-any.whl", hash = "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d"}, - {file = "pyasn1_modules-0.3.0.tar.gz", hash = "sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c"}, + {file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"}, + {file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"}, ] [package.dependencies] -pyasn1 = ">=0.4.6,<0.6.0" +pyasn1 = ">=0.6.1,<0.7.0" [[package]] name = "pycocotools" -version = "2.0.7" +version = "2.0.11" description = "Official APIs for the MS-COCO dataset" optional = false -python-versions = ">=3.5" -files = [ - {file = "pycocotools-2.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a6683a002fcb4500edbcec94bdf48be69f578a9aa5c638db38614df1f45cc935"}, - {file = "pycocotools-2.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d517ec315e53ef8df9f6b0899ebc4c79bd61fd715383861949bb1c3fca2c6d5"}, - {file = "pycocotools-2.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eb5d46900375adaba88eedb5cbc29d8cbcf43e82505d67378df1c3b720a8c5f"}, - {file = "pycocotools-2.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:363a6be808125306ace1a163c0b9ba479ee08eceec1fbd3889a88bd8245f73dc"}, - {file = "pycocotools-2.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:623b941bbafecbfee574aedbf3cb257f7a879f4fdb79394e6d3fb9c76e7ad6cf"}, - {file = "pycocotools-2.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ac4f30bac1503c780072053e6922971392fa3628b2e6967192bfca1f14736e2"}, - {file = "pycocotools-2.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121017ca87e2eec4e9081636d1a79519b50f473959defc5671c2d1ce0eec482e"}, - {file = "pycocotools-2.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:4a8ec6f439638120e11f49120e1ddb6c66e0b1f293d7884207d02703a73d25a1"}, - {file = "pycocotools-2.0.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1a675728e459d72be6e3bb3546672bb37c7daffdc2e5335aa7b834aece2b560"}, - {file = "pycocotools-2.0.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6541340f26bae32e044eedc5d8ccdac5bd0cb64eb2b0a342dac859b696edd0aa"}, - {file = "pycocotools-2.0.7-cp37-cp37m-win_amd64.whl", hash = "sha256:8def3c46349e919999d6d5a1d6b7e587e6891524cc28f8b4a11e463bf0914621"}, - {file = "pycocotools-2.0.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6469089b9b36a1f645dc9ee830f29d261e99b4b3be73cb260688fd8b6d02760c"}, - {file = "pycocotools-2.0.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1dbc429018149dc34e206ea32ee6297ff30b55a8615a3f7f4c6e3842f9df73db"}, - {file = "pycocotools-2.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66864bec8b30d47faa946bb55c8e8d6b7acb9fba0c17ff6aaa37abd78cda962a"}, - {file = "pycocotools-2.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:625388f52e543f6f798f75f1ec125fe519580f22e72ccbd75eee0355ce336e18"}, - {file = "pycocotools-2.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:73dc251ae4a06b7c10747ca7e2d29faabb4f13e5fc43760945966845581e79ae"}, - {file = "pycocotools-2.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e6f7bfa1c5fb206a614bf2382c923d56092219a12dfd0fec3b5f83c13e29e00"}, - {file = "pycocotools-2.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b29086b6ce7b73e4ddaf3045006f5c059f344a2720605cd4474814017ff2af53"}, - {file = "pycocotools-2.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:254506c0eecabb3abbde17640f82103c0c04d53148ae920657664cab9cd649fc"}, - {file = "pycocotools-2.0.7.tar.gz", hash = "sha256:da8b7815196eebf0adabf67fcc459126cbc6498bbc6ab1fd144c371465d86879"}, +python-versions = ">=3.9" +files = [ + {file = "pycocotools-2.0.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:484d33515353186aadba9e2a290d81b107275cdb9565084e31a5568a52a0b120"}, + {file = "pycocotools-2.0.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca9f120f719ec405ad0c74ccfdb8402b0c37bd5f88ab5b6482a0de2efd5a36f4"}, + {file = "pycocotools-2.0.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e40a3a898c6e5340b8d70cf7984868b9bff8c3d80187de9a3b661d504d665978"}, + {file = "pycocotools-2.0.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7cd4cdfd2c676f30838aa0b1047441892fb4f97d70bf3df480bcc7a18a64d7d4"}, + {file = "pycocotools-2.0.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08c79789fd79e801ae4ecfcfeec32b31e36254e7a2b4019af28c104975d5e730"}, + {file = "pycocotools-2.0.11-cp310-cp310-win_amd64.whl", hash = "sha256:f78cbb1a32d061fcad4bdba083de70a39a21c1c3d9235a3f77d8f007541ec5ef"}, + {file = "pycocotools-2.0.11-cp310-cp310-win_arm64.whl", hash = "sha256:e21311ea71f85591680d8992858e2d44a2a156dc3b2bf1c5c901c4a19348177b"}, + {file = "pycocotools-2.0.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:81bdceebb4c64e9265213e2d733808a12f9c18dfb14457323cc6b9af07fa0e61"}, + {file = "pycocotools-2.0.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c05f91ccc658dfe01325267209c4b435da1722c93eeb5749fabc1d087b6882"}, + {file = "pycocotools-2.0.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18ba75ff58cedb33a85ce2c18f1452f1fe20c9dd59925eec5300b2bf6205dbe1"}, + {file = "pycocotools-2.0.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:693417797f0377fd094eb815c0a1e7d1c3c0251b71e3b3779fce3b3cf24793c5"}, + {file = "pycocotools-2.0.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b6a07071c441d0f5e480a8f287106191582e40289d4e242dfe684e0c8a751088"}, + {file = "pycocotools-2.0.11-cp311-cp311-win_amd64.whl", hash = "sha256:8e159232adae3aef6b4e2d37b008bff107b26e9ed3b48e70ea6482302834bd34"}, + {file = "pycocotools-2.0.11-cp311-cp311-win_arm64.whl", hash = "sha256:4fc9889e819452b9c142036e1eabac8a13a8bd552d8beba299a57e0da6bfa1ec"}, + {file = "pycocotools-2.0.11-cp312-abi3-macosx_10_13_universal2.whl", hash = "sha256:a2e9634bc7cadfb01c88e0b98589aaf0bd12983c7927bde93f19c0103e5441f4"}, + {file = "pycocotools-2.0.11-cp312-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fd4121766cc057133534679c0ec3f9023dbd96e9b31cf95c86a069ebdac2b65"}, + {file = "pycocotools-2.0.11-cp312-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a82d1c9ed83f75da0b3f244f2a3cf559351a283307bd9b79a4ee2b93ab3231dd"}, + {file = "pycocotools-2.0.11-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:89e853425018e2c2920ee0f2112cf7c140a1dcf5f4f49abd9c2da112c3e0f4b3"}, + {file = "pycocotools-2.0.11-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:87af87b8d06d5b852a885a319d9362dca3bed9f8bbcc3feb6513acb1f88ea242"}, + {file = "pycocotools-2.0.11-cp312-abi3-win_amd64.whl", hash = "sha256:ffe806ce535f5996445188f9a35643791dc54beabc61bd81e2b03367356d604f"}, + {file = "pycocotools-2.0.11-cp312-abi3-win_arm64.whl", hash = "sha256:c230f5e7b14bd19085217b4f40bba81bf14a182b150b8e9fab1c15d504ade343"}, + {file = "pycocotools-2.0.11-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:fd72b9734e6084b217c1fc3945bfd4ec05bdc75a44e4f0c461a91442bb804973"}, + {file = "pycocotools-2.0.11-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7eb43b79448476b094240450420b7425d06e297880144b8ea6f01e9b4340e43"}, + {file = "pycocotools-2.0.11-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3546b93b39943347c4f5b0694b5824105cbe2174098a416bcad4acd9c21e957"}, + {file = "pycocotools-2.0.11-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:efd1694b2075f2f10c5828f10f6e6c4e44368841fd07dae385c3aa015c8e25f9"}, + {file = "pycocotools-2.0.11-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:368244f30eb8d6cae7003aa2c0831fbdf0153664a32859ec7fbceea52bfb6878"}, + {file = "pycocotools-2.0.11-cp313-cp313t-win_amd64.whl", hash = "sha256:ac8aa17263e6489aa521f9fa91e959dfe0ea3a5519fde2cbf547312cdce7559e"}, + {file = "pycocotools-2.0.11-cp313-cp313t-win_arm64.whl", hash = "sha256:04480330df5013f6edd94891a0ee8294274185f1b5093d1b0f23d51778f0c0e9"}, + {file = "pycocotools-2.0.11-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a6b13baf6bfcf881b6d6ac6e23c776f87a68304cd86e53d1d6b9afa31e363c4e"}, + {file = "pycocotools-2.0.11-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78bae4a9de9d34c4759754a848dfb3306f9ef1c2fcb12164ffbd3d013d008321"}, + {file = "pycocotools-2.0.11-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d896f4310379849dfcfa7893afb0ff21f4f3cdb04ab3f61b05dd98953dd0ad"}, + {file = "pycocotools-2.0.11-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:eebd723503a2eb2c8b285f56ea3be1d9f3875cd7c40d945358a428db94f14015"}, + {file = "pycocotools-2.0.11-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bd7a1e19ef56a828a94bace673372071d334a9232cd32ae3cd48845a04d45c4f"}, + {file = "pycocotools-2.0.11-cp314-cp314t-win_amd64.whl", hash = "sha256:63026e11a56211058d0e84e8263f74cbccd5e786fac18d83fd221ecb9819fcc7"}, + {file = "pycocotools-2.0.11-cp314-cp314t-win_arm64.whl", hash = "sha256:8cedb8ccb97ffe9ed2c8c259234fa69f4f1e8665afe3a02caf93f6ef2952c07f"}, + {file = "pycocotools-2.0.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9dc8b388984c72aa84b1a68933f196ce71ab114c59232d0eab20c97cc1300875"}, + {file = "pycocotools-2.0.11-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2b018497ec198ffc737dd7e6306a2e69999779ca619a9e12950e4506e410c3e"}, + {file = "pycocotools-2.0.11-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e72527fdc00985d29dbe31d17b19cd2d16fbad7b01e974c567b593d5844de710"}, + {file = "pycocotools-2.0.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d6b88557166794a24acd03f50296f2b95adf3e4206b4b8995e6bdca925c9cb72"}, + {file = "pycocotools-2.0.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6a2f4f036f5bfdaf8c9625279051e921721fc9d27f92c97a3f3355a07ba38513"}, + {file = "pycocotools-2.0.11-cp39-cp39-win_amd64.whl", hash = "sha256:eba35d6e06caaea28ce65a4b92e4343ecbb9abf4915f7ef7fca989b80839111c"}, + {file = "pycocotools-2.0.11-cp39-cp39-win_arm64.whl", hash = "sha256:1192de413a23b4b94199197e8f8dbe1277cc24e6e9847bee6a71be3d8e543963"}, + {file = "pycocotools-2.0.11.tar.gz", hash = "sha256:34254d76da85576fcaf5c1f3aa9aae16b8cb15418334ba4283b800796bd1993d"}, ] [package.dependencies] -matplotlib = ">=2.1.0" numpy = "*" +[package.extras] +all = ["matplotlib (>=2.1.0)"] + [[package]] name = "pycparser" -version = "2.21" +version = "3.0" description = "C parser in Python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.10" files = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, + {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, + {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, ] [[package]] name = "pycryptodome" -version = "3.21.0" +version = "3.23.0" description = "Cryptographic library for Python" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -files = [ - {file = "pycryptodome-3.21.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:dad9bf36eda068e89059d1f07408e397856be9511d7113ea4b586642a429a4fd"}, - {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a1752eca64c60852f38bb29e2c86fca30d7672c024128ef5d70cc15868fa10f4"}, - {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3ba4cc304eac4d4d458f508d4955a88ba25026890e8abff9b60404f76a62c55e"}, - {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cb087b8612c8a1a14cf37dd754685be9a8d9869bed2ffaaceb04850a8aeef7e"}, - {file = "pycryptodome-3.21.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:26412b21df30b2861424a6c6d5b1d8ca8107612a4cfa4d0183e71c5d200fb34a"}, - {file = "pycryptodome-3.21.0-cp27-cp27m-win32.whl", hash = "sha256:cc2269ab4bce40b027b49663d61d816903a4bd90ad88cb99ed561aadb3888dd3"}, - {file = "pycryptodome-3.21.0-cp27-cp27m-win_amd64.whl", hash = "sha256:0fa0a05a6a697ccbf2a12cec3d6d2650b50881899b845fac6e87416f8cb7e87d"}, - {file = "pycryptodome-3.21.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6cce52e196a5f1d6797ff7946cdff2038d3b5f0aba4a43cb6bf46b575fd1b5bb"}, - {file = "pycryptodome-3.21.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:a915597ffccabe902e7090e199a7bf7a381c5506a747d5e9d27ba55197a2c568"}, - {file = "pycryptodome-3.21.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4e74c522d630766b03a836c15bff77cb657c5fdf098abf8b1ada2aebc7d0819"}, - {file = "pycryptodome-3.21.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:a3804675283f4764a02db05f5191eb8fec2bb6ca34d466167fc78a5f05bbe6b3"}, - {file = "pycryptodome-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:2480ec2c72438430da9f601ebc12c518c093c13111a5c1644c82cdfc2e50b1e4"}, - {file = "pycryptodome-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:de18954104667f565e2fbb4783b56667f30fb49c4d79b346f52a29cb198d5b6b"}, - {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de4b7263a33947ff440412339cb72b28a5a4c769b5c1ca19e33dd6cd1dcec6e"}, - {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0714206d467fc911042d01ea3a1847c847bc10884cf674c82e12915cfe1649f8"}, - {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d85c1b613121ed3dbaa5a97369b3b757909531a959d229406a75b912dd51dd1"}, - {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8898a66425a57bcf15e25fc19c12490b87bd939800f39a03ea2de2aea5e3611a"}, - {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:932c905b71a56474bff8a9c014030bc3c882cee696b448af920399f730a650c2"}, - {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:18caa8cfbc676eaaf28613637a89980ad2fd96e00c564135bf90bc3f0b34dd93"}, - {file = "pycryptodome-3.21.0-cp36-abi3-win32.whl", hash = "sha256:280b67d20e33bb63171d55b1067f61fbd932e0b1ad976b3a184303a3dad22764"}, - {file = "pycryptodome-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b7aa25fc0baa5b1d95b7633af4f5f1838467f1815442b22487426f94e0d66c53"}, - {file = "pycryptodome-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:2cb635b67011bc147c257e61ce864879ffe6d03342dc74b6045059dfbdedafca"}, - {file = "pycryptodome-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:4c26a2f0dc15f81ea3afa3b0c87b87e501f235d332b7f27e2225ecb80c0b1cdd"}, - {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d5ebe0763c982f069d3877832254f64974139f4f9655058452603ff559c482e8"}, - {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ee86cbde706be13f2dec5a42b52b1c1d1cbb90c8e405c68d0755134735c8dc6"}, - {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fd54003ec3ce4e0f16c484a10bc5d8b9bd77fa662a12b85779a2d2d85d67ee0"}, - {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5dfafca172933506773482b0e18f0cd766fd3920bd03ec85a283df90d8a17bc6"}, - {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:590ef0898a4b0a15485b05210b4a1c9de8806d3ad3d47f74ab1dc07c67a6827f"}, - {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f35e442630bc4bc2e1878482d6f59ea22e280d7121d7adeaedba58c23ab6386b"}, - {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff99f952db3db2fbe98a0b355175f93ec334ba3d01bbde25ad3a5a33abc02b58"}, - {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8acd7d34af70ee63f9a849f957558e49a98f8f1634f86a59d2be62bb8e93f71c"}, - {file = "pycryptodome-3.21.0.tar.gz", hash = "sha256:f7787e0d469bdae763b876174cf2e6c0f7be79808af26b1da96f1a64bcf47297"}, +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "pycryptodome-3.23.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a176b79c49af27d7f6c12e4b178b0824626f40a7b9fed08f712291b6d54bf566"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:573a0b3017e06f2cffd27d92ef22e46aa3be87a2d317a5abf7cc0e84e321bd75"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:63dad881b99ca653302b2c7191998dd677226222a3f2ea79999aa51ce695f720"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-win32.whl", hash = "sha256:b34e8e11d97889df57166eda1e1ddd7676da5fcd4d71a0062a760e75060514b4"}, + {file = "pycryptodome-3.23.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:7ac1080a8da569bde76c0a104589c4f414b8ba296c0b3738cf39a466a9fb1818"}, + {file = "pycryptodome-3.23.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6fe8258e2039eceb74dfec66b3672552b6b7d2c235b2dfecc05d16b8921649a8"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39"}, + {file = "pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27"}, + {file = "pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c"}, + {file = "pycryptodome-3.23.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:350ebc1eba1da729b35ab7627a833a1a355ee4e852d8ba0447fafe7b14504d56"}, + {file = "pycryptodome-3.23.0-pp27-pypy_73-win32.whl", hash = "sha256:93837e379a3e5fd2bb00302a47aee9fdf7940d83595be3915752c74033d17ca7"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:865d83c906b0fc6a59b510deceee656b6bc1c4fa0d82176e2b77e97a420a996a"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d4d56153efc4d81defe8b65fd0821ef8b2d5ddf8ed19df31ba2f00872b8002"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3f2d0aaf8080bda0587d58fc9fe4766e012441e2eed4269a77de6aea981c8be"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64093fc334c1eccfd3933c134c4457c34eaca235eeae49d69449dc4728079339"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ce64e84a962b63a47a592690bdc16a7eaf709d2c2697ababf24a0def566899a6"}, + {file = "pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef"}, ] [[package]] @@ -3525,13 +4003,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pygments" -version = "2.18.0" +version = "2.20.0" description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, - {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, ] [package.extras] @@ -3539,106 +4017,107 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyinstrument" -version = "4.6.2" +version = "4.7.3" description = "Call stack profiler for Python. Shows you why your code is slow!" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pyinstrument-4.6.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7a1b1cd768ea7ea9ab6f5490f7e74431321bcc463e9441dbc2f769617252d9e2"}, - {file = "pyinstrument-4.6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8a386b9d09d167451fb2111eaf86aabf6e094fed42c15f62ec51d6980bce7d96"}, - {file = "pyinstrument-4.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23c3e3ca8553b9aac09bd978c73d21b9032c707ac6d803bae6a20ecc048df4a8"}, - {file = "pyinstrument-4.6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f329f5534ca069420246f5ce57270d975229bcb92a3a3fd6b2ca086527d9764"}, - {file = "pyinstrument-4.6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4dcdcc7ba224a0c5edfbd00b0f530f5aed2b26da5aaa2f9af5519d4aa8c7e41"}, - {file = "pyinstrument-4.6.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73db0c2c99119c65b075feee76e903b4ed82e59440fe8b5724acf5c7cb24721f"}, - {file = "pyinstrument-4.6.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:da58f265326f3cf3975366ccb8b39014f1e69ff8327958a089858d71c633d654"}, - {file = "pyinstrument-4.6.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:feebcf860f955401df30d029ec8de7a0c5515d24ea809736430fd1219686fe14"}, - {file = "pyinstrument-4.6.2-cp310-cp310-win32.whl", hash = "sha256:b2b66ff0b16c8ecf1ec22de001cfff46872b2c163c62429055105564eef50b2e"}, - {file = "pyinstrument-4.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:8d104b7a7899d5fa4c5bf1ceb0c1a070615a72c5dc17bc321b612467ad5c5d88"}, - {file = "pyinstrument-4.6.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:62f6014d2b928b181a52483e7c7b82f2c27e22c577417d1681153e5518f03317"}, - {file = "pyinstrument-4.6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dcb5c8d763c5df55131670ba2a01a8aebd0d490a789904a55eb6a8b8d497f110"}, - {file = "pyinstrument-4.6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ed4e8c6c84e0e6429ba7008a66e435ede2d8cb027794c20923c55669d9c5633"}, - {file = "pyinstrument-4.6.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c0f0e1d8f8c70faa90ff57f78ac0dda774b52ea0bfb2d9f0f41ce6f3e7c869e"}, - {file = "pyinstrument-4.6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b3c44cb037ad0d6e9d9a48c14d856254ada641fbd0ae9de40da045fc2226a2a"}, - {file = "pyinstrument-4.6.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:be9901f17ac2f527c352f2fdca3d717c1d7f2ce8a70bad5a490fc8cc5d2a6007"}, - {file = "pyinstrument-4.6.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8a9791bf8916c1cf439c202fded32de93354b0f57328f303d71950b0027c7811"}, - {file = "pyinstrument-4.6.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d6162615e783c59e36f2d7caf903a7e3ecb6b32d4a4ae8907f2760b2ef395bf6"}, - {file = "pyinstrument-4.6.2-cp311-cp311-win32.whl", hash = "sha256:28af084aa84bbfd3620ebe71d5f9a0deca4451267f363738ca824f733de55056"}, - {file = "pyinstrument-4.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:dd6007d3c2e318e09e582435dd8d111cccf30d342af66886b783208813caf3d7"}, - {file = "pyinstrument-4.6.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e3813c8ecfab9d7d855c5f0f71f11793cf1507f40401aa33575c7fd613577c23"}, - {file = "pyinstrument-4.6.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6c761372945e60fc1396b7a49f30592e8474e70a558f1a87346d27c8c4ce50f7"}, - {file = "pyinstrument-4.6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fba3244e94c117bf4d9b30b8852bbdcd510e7329fdd5c7c8b3799e00a9215a8"}, - {file = "pyinstrument-4.6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:803ac64e526473d64283f504df3b0d5c2c203ea9603cab428641538ffdc753a7"}, - {file = "pyinstrument-4.6.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2e554b1bb0df78f5ce8a92df75b664912ca93aa94208386102af454ec31b647"}, - {file = "pyinstrument-4.6.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7c671057fad22ee3ded897a6a361204ea2538e44c1233cad0e8e30f6d27f33db"}, - {file = "pyinstrument-4.6.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:d02f31fa13a9e8dc702a113878419deba859563a32474c9f68e04619d43d6f01"}, - {file = "pyinstrument-4.6.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b55983a884f083f93f0fc6d12ff8df0acd1e2fb0580d2f4c7bfe6def33a84b58"}, - {file = "pyinstrument-4.6.2-cp312-cp312-win32.whl", hash = "sha256:fdc0a53b27e5d8e47147489c7dab596ddd1756b1e053217ef5bc6718567099ff"}, - {file = "pyinstrument-4.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:dd5c53a0159126b5ce7cbc4994433c9c671e057c85297ff32645166a06ad2c50"}, - {file = "pyinstrument-4.6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b082df0bbf71251a7f4880a12ed28421dba84ea7110bb376e0533067a4eaff40"}, - {file = "pyinstrument-4.6.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90350533396071cb2543affe01e40bf534c35cb0d4b8fa9fdb0f052f9ca2cfe3"}, - {file = "pyinstrument-4.6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67268bb0d579330cff40fd1c90b8510363ca1a0e7204225840614068658dab77"}, - {file = "pyinstrument-4.6.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20e15b4e1d29ba0b7fc81aac50351e0dc0d7e911e93771ebc3f408e864a2c93b"}, - {file = "pyinstrument-4.6.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2e625fc6ffcd4fd420493edd8276179c3f784df207bef4c2192725c1b310534c"}, - {file = "pyinstrument-4.6.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:113d2fc534c9ca7b6b5661d6ada05515bf318f6eb34e8d05860fe49eb7cfe17e"}, - {file = "pyinstrument-4.6.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3098cd72b71a322a72dafeb4ba5c566465e193d2030adad4c09566bd2f89bf4f"}, - {file = "pyinstrument-4.6.2-cp37-cp37m-win32.whl", hash = "sha256:08fdc7f88c989316fa47805234c37a40fafe7b614afd8ae863f0afa9d1707b37"}, - {file = "pyinstrument-4.6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:5ebeba952c0056dcc9b9355328c78c4b5c2a33b4b4276a9157a3ab589f3d1bac"}, - {file = "pyinstrument-4.6.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:34e59e91c88ec9ad5630c0964eca823949005e97736bfa838beb4789e94912a2"}, - {file = "pyinstrument-4.6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cd0320c39e99e3c0a3129d1ed010ac41e5a7eb96fb79900d270080a97962e995"}, - {file = "pyinstrument-4.6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46992e855d630575ec635eeca0068a8ddf423d4fd32ea0875a94e9f8688f0b95"}, - {file = "pyinstrument-4.6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e474c56da636253dfdca7cd1998b240d6b39f7ed34777362db69224fcf053b1"}, - {file = "pyinstrument-4.6.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4b559322f30509ad8f082561792352d0805b3edfa508e492a36041fdc009259"}, - {file = "pyinstrument-4.6.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:06a8578b2943eb1dbbf281e1e59e44246acfefd79e1b06d4950f01b693de12af"}, - {file = "pyinstrument-4.6.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7bd3da31c46f1c1cb7ae89031725f6a1d1015c2041d9c753fe23980f5f9fd86c"}, - {file = "pyinstrument-4.6.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e63f4916001aa9c625976a50779282e0a5b5e9b17c52a50ef4c651e468ed5b88"}, - {file = "pyinstrument-4.6.2-cp38-cp38-win32.whl", hash = "sha256:32ec8db6896b94af790a530e1e0edad4d0f941a0ab8dd9073e5993e7ea46af7d"}, - {file = "pyinstrument-4.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:a59fc4f7db738a094823afe6422509fa5816a7bf74e768ce5a7a2ddd91af40ac"}, - {file = "pyinstrument-4.6.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3a165e0d2deb212d4cf439383982a831682009e1b08733c568cac88c89784e62"}, - {file = "pyinstrument-4.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7ba858b3d6f6e5597c641edcc0e7e464f85aba86d71bc3b3592cb89897bf43f6"}, - {file = "pyinstrument-4.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fd8e547cf3df5f0ec6e4dffbe2e857f6b28eda51b71c3c0b5a2fc0646527835"}, - {file = "pyinstrument-4.6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0de2c1714a37a820033b19cf134ead43299a02662f1379140974a9ab733c5f3a"}, - {file = "pyinstrument-4.6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01fc45dedceec3df81668d702bca6d400d956c8b8494abc206638c167c78dfd9"}, - {file = "pyinstrument-4.6.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5b6e161ef268d43ee6bbfae7fd2cdd0a52c099ddd21001c126ca1805dc906539"}, - {file = "pyinstrument-4.6.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6ba8e368d0421f15ba6366dfd60ec131c1b46505d021477e0f865d26cf35a605"}, - {file = "pyinstrument-4.6.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edca46f04a573ac2fb11a84b937844e6a109f38f80f4b422222fb5be8ecad8cb"}, - {file = "pyinstrument-4.6.2-cp39-cp39-win32.whl", hash = "sha256:baf375953b02fe94d00e716f060e60211ede73f49512b96687335f7071adb153"}, - {file = "pyinstrument-4.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:af1a953bce9fd530040895d01ff3de485e25e1576dccb014f76ba9131376fcad"}, - {file = "pyinstrument-4.6.2.tar.gz", hash = "sha256:0002ee517ed8502bbda6eb2bb1ba8f95a55492fcdf03811ba13d4806e50dd7f6"}, + {file = "pyinstrument-4.7.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6a79912f8a096ccad1b88a527719563f6b2b5dc94057873c2ca840dc6378cfee"}, + {file = "pyinstrument-4.7.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:089f7afb326ee937656ee1767813dc793ad20b3d353d081e16255b63830a4787"}, + {file = "pyinstrument-4.7.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f65107079f68dcaeb58ee032d98075ab7ac49be419c60673406043e0675393b4"}, + {file = "pyinstrument-4.7.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9402e339d802a7f5b1ad716b8411ab98f45e51c4b261e662b8a470c251af0acc"}, + {file = "pyinstrument-4.7.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d1f4e0155f563f66e821210c225af8b64a2283c0feff776c49feba623e7bafd"}, + {file = "pyinstrument-4.7.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c619f3064dae5284b904c4862b35639c35ecd439bb5b4152924f7ccb69edc5e3"}, + {file = "pyinstrument-4.7.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b4d80deaf76cc171b3b707e2babc9a7046610c4e11022167949e60fc2dc62be"}, + {file = "pyinstrument-4.7.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c5fbe9d24154a118a4b86bed5ae228c3d8698216fad65257aca97e790527197a"}, + {file = "pyinstrument-4.7.3-cp310-cp310-win32.whl", hash = "sha256:7405aec2227ed87dc3bc3a8eb82b5dcdec68861d564ee0d429f9a51ca30ccd58"}, + {file = "pyinstrument-4.7.3-cp310-cp310-win_amd64.whl", hash = "sha256:8043b9c1fb0c19a2957098930c3bad43ecdc1cf8e1d3f32a3b9ef74fdd3df028"}, + {file = "pyinstrument-4.7.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:77594adf4713bc3e430e300561a2d837213cf9015414c0e0de6aef0cb9cebd80"}, + {file = "pyinstrument-4.7.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:70afa765c06e4f7605033b85ef82ed946ec8e6ae1835e25f6cbb01205a624197"}, + {file = "pyinstrument-4.7.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b1321514863be18138a6d761696b3f6e8645390dd2f6c8a6d66a453f0d5187c"}, + {file = "pyinstrument-4.7.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de40b44ff2fe78493b944b679cc084e72b2648c37a96fcfbccb9171a4449e509"}, + {file = "pyinstrument-4.7.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a7c481daec4bd77a3dbfbe01a0155e03352dd700f3c3efe4bdbc30821b20e19"}, + {file = "pyinstrument-4.7.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ae2c966c91da630a23dbff5f7e61ad2eee133cfaf1e4acf7e09fcf506cbb6251"}, + {file = "pyinstrument-4.7.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fa2715e3ac3ce2f4b9c4e468a9a4faf43ca645beea002cb47533902576f4f64d"}, + {file = "pyinstrument-4.7.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:61db15f8b59a3a1964041a8df260667fb5dabddd928301e3580cf93d7a05e352"}, + {file = "pyinstrument-4.7.3-cp311-cp311-win32.whl", hash = "sha256:4766bbb2b451460432c97baf00bbda56653429671e8daec344d343f21fb05b8f"}, + {file = "pyinstrument-4.7.3-cp311-cp311-win_amd64.whl", hash = "sha256:b2d2a0e401db6800f63de0539415cdff46b138914d771a46db0b3f673f9827e7"}, + {file = "pyinstrument-4.7.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7c29f7a23e0f704f5f21aeeb47193460601e7359d09156ea043395870494b39a"}, + {file = "pyinstrument-4.7.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:84ceb25f24ceb03dc770b6c142ec4419506d3a04d66d778810cb8da76df25651"}, + {file = "pyinstrument-4.7.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d564d6f6151d3cab28430092cdcbd4aefe0834551af4b4f97e6e57025a348557"}, + {file = "pyinstrument-4.7.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e23ce5fcc30346e576b98ca24bd2a9a68cbc42b90cdb0d8f376fa82cee2fe23"}, + {file = "pyinstrument-4.7.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23d5ad174d2a488c164abee4407f3f3a6e6d5721ab1fab9e0ad9570631704c2"}, + {file = "pyinstrument-4.7.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d87749f68b9cc221628aab989a4a73b16030c27c714ecd83892d716f863d9739"}, + {file = "pyinstrument-4.7.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:897d09c876f18b713498be21430b39428a9254ffec0c6c06796fce0e6a8fe437"}, + {file = "pyinstrument-4.7.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2092910e745cfd0a62dadf041afb38239195244871ee127b1028e7e790602e6b"}, + {file = "pyinstrument-4.7.3-cp312-cp312-win32.whl", hash = "sha256:e9824e11290f6f2772c257cc0bd07f59405759287db6ebcbb06f962a3eba68fb"}, + {file = "pyinstrument-4.7.3-cp312-cp312-win_amd64.whl", hash = "sha256:cf1e67b37e936f647ce731fff5d2f54e102813274d350671dc5961ec8b46b3ff"}, + {file = "pyinstrument-4.7.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6de792dc65dcc75e73b721f4e89aa60a4d2f8617e5a5da060244058018ad0399"}, + {file = "pyinstrument-4.7.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:73da379506a09cdff2fdd23a0b3eb8f020f473d019f604538e0e5045613e33d4"}, + {file = "pyinstrument-4.7.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21e05f53810a6ff5fa261da838935fd1b2ab2bf30a7c053f6c72bcaaa6de0933"}, + {file = "pyinstrument-4.7.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d648596ea04409ca3ca260029041ed7fa046b776205bf9a0b75cda0a4f4d2515"}, + {file = "pyinstrument-4.7.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d98997347047a217ef6b844273d3753e543e0984f2220e9dd284cbef6054c2a"}, + {file = "pyinstrument-4.7.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7f09ebad95af94f5427c20005fc7ba84a0a3deae6324434d7ec3be99d369bf37"}, + {file = "pyinstrument-4.7.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8a66aee3d2cf0cc6b8e57cb189fd9fb16d13b8d538419999596ce4f58b5d4a9a"}, + {file = "pyinstrument-4.7.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eaa45270af0b9d86f1cef705520e9b43f4a1cd18397083f8a594a28f898d078b"}, + {file = "pyinstrument-4.7.3-cp313-cp313-win32.whl", hash = "sha256:6e85b34a9b8ed4df4deaa0afe63bc765ea29003eb5b9b3bc0323f7ad7f7cd0fd"}, + {file = "pyinstrument-4.7.3-cp313-cp313-win_amd64.whl", hash = "sha256:6002ea1018d6d6f9b6f1c66b3e14805213573bd69f79b2e7ad2c507441b3e73e"}, + {file = "pyinstrument-4.7.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b68c5b97690604741bb1f028ec75d2a6298500f415590ae92a766f71b82fc72a"}, + {file = "pyinstrument-4.7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:df9ba133f5a771dd30df1d3b868af75bdb7f12c9ebd5ddd463d09aa6334d96ef"}, + {file = "pyinstrument-4.7.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bfad987207c89b51f80be71f5362cead4ccd62b9f407248b87e91863bba70e4d"}, + {file = "pyinstrument-4.7.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65fd559498902d1560d728238eea53d8dd54cb8f697b816cacce5524f09d8757"}, + {file = "pyinstrument-4.7.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:470a4f6de1a1edf7debe87917b5d12f94fe59975a8a0e91c22ad789b55720073"}, + {file = "pyinstrument-4.7.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f29ed5778b83bf40bd808f120cd2ea11ef94acd2aa5b64398e6d56958b88ab26"}, + {file = "pyinstrument-4.7.3-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:6d642d8c69091fd49286136b7d958f8dbac969a3f6259c7c6d78e8ff207d235e"}, + {file = "pyinstrument-4.7.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:346bc584c542c4c77ca46e8f55eb2d3265ee992839e06d535a22ca65c5b9e767"}, + {file = "pyinstrument-4.7.3-cp38-cp38-win32.whl", hash = "sha256:66af331f9da06df36afbdbd2b7128ae725bb444f24584d2ed1f4c67d1b2759b8"}, + {file = "pyinstrument-4.7.3-cp38-cp38-win_amd64.whl", hash = "sha256:57992c5f73fad7b560e27f864ff9824c6ccc834d48bbeaf4cecf66193cfe28c6"}, + {file = "pyinstrument-4.7.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8b944c939c49af88cec1e20e9c28eec80c478fc2fd53b23ed58702bcb5bcbcf9"}, + {file = "pyinstrument-4.7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:edd85ee9c6aa5be0bf78d48ad2eb5e02fdab1a646875d90fa09cbc61f4c91a01"}, + {file = "pyinstrument-4.7.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e381fc56ba4a77cb45d82eb69689d900a5ee7205a5eb90131234b21ae7a1991"}, + {file = "pyinstrument-4.7.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:98e1b7695c234786e82500394ef50f205713f8702a31aec84fdd0687e0ab8405"}, + {file = "pyinstrument-4.7.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03dd0c51f6ca706be5c27715e9b4527aa82003c2705d3173943c5b4a2b7a47e8"}, + {file = "pyinstrument-4.7.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2b312442f01fbf2582cd7c929703608cb82874b73a0f3250cbeffc4abddae4f5"}, + {file = "pyinstrument-4.7.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e660d9a7f57909574010056dbc80869866623669455516ffc7421988286ddaf3"}, + {file = "pyinstrument-4.7.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:886ccb349aefcbd5be1f33247b3a1af4ad5d34939338d99e94bae064886bf0d8"}, + {file = "pyinstrument-4.7.3-cp39-cp39-win32.whl", hash = "sha256:1ce2828cc29b17720f3c66345ea6f9ff54a3860d0488b59c985377ce2e6a710b"}, + {file = "pyinstrument-4.7.3-cp39-cp39-win_amd64.whl", hash = "sha256:e562e608f878540d19a514774e0f24fccaeac035674cf2b2afacdae9e0e19b29"}, + {file = "pyinstrument-4.7.3.tar.gz", hash = "sha256:3ad61041ff1880d4c99d3384cd267e38a0a6472b5a4dd765992db376bd4394c8"}, ] [package.extras] bin = ["click", "nox"] -docs = ["furo (==2021.6.18b36)", "myst-parser (==0.15.1)", "sphinx (==4.2.0)", "sphinxcontrib-programoutput (==0.17)"] -examples = ["django", "numpy"] -test = ["flaky", "greenlet (>=3.0.0a1)", "ipython", "pytest", "pytest-asyncio (==0.12.0)", "sphinx-autobuild (==2021.3.14)", "trio"] +docs = ["furo (==2024.7.18)", "myst-parser (==3.0.1)", "sphinx (==7.4.7)", "sphinx-autobuild (==2024.4.16)", "sphinxcontrib-programoutput (==0.17)"] +examples = ["django", "litestar", "numpy"] +test = ["cffi (>=v1.17.0rc1)", "flaky", "greenlet (>=3.0.0a1)", "ipython", "pytest", "pytest-asyncio (==0.23.8)", "trio"] types = ["typing-extensions"] [[package]] name = "pyjwt" -version = "2.9.0" +version = "2.13.0" description = "JSON Web Token implementation in Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"}, - {file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"}, + {file = "pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728"}, + {file = "pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423"}, ] +[package.dependencies] +typing_extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + [package.extras] crypto = ["cryptography (>=3.4.0)"] -dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] -docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] -tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] [[package]] name = "pyparsing" -version = "3.1.1" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" +version = "3.3.2" +description = "pyparsing - Classes and methods to define and execute parsing grammars" optional = false -python-versions = ">=3.6.8" +python-versions = ">=3.9" files = [ - {file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"}, - {file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"}, + {file = "pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d"}, + {file = "pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc"}, ] [package.extras] @@ -3668,13 +4147,13 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no [[package]] name = "pytest-mock" -version = "3.14.0" +version = "3.15.1" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, - {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, + {file = "pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d"}, + {file = "pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f"}, ] [package.dependencies] @@ -3685,27 +4164,46 @@ dev = ["pre-commit", "pytest-asyncio", "tox"] [[package]] name = "python-dateutil" -version = "2.8.2" +version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] [package.dependencies] six = ">=1.5" +[[package]] +name = "python-discovery" +version = "1.4.4" +description = "Python interpreter discovery" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python_discovery-1.4.4-py3-none-any.whl", hash = "sha256:abebe9120b43453b68c908acfb1e72a19d1a959ed2cb620ad38fc57d08056dbe"}, + {file = "python_discovery-1.4.4.tar.gz", hash = "sha256:5cad33982d412c1f3ffb8f9ca4ea292c9680bca3942451d30b69c37fce53a4a3"}, +] + +[package.dependencies] +filelock = ">=3.15.4" +platformdirs = ">=4.3.6,<5" + +[package.extras] +docs = ["furo (>=2025.12.19)", "sphinx (>=9.1)", "sphinx-autodoc-typehints (>=3.6.3)", "sphinxcontrib-mermaid (>=2)", "sphinxcontrib-towncrier (>=0.4)", "towncrier (>=25.8)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.5.4)", "pytest (>=8.3.5)", "pytest-mock (>=3.14)", "setuptools (>=75.1)"] + [[package]] name = "python-dotenv" -version = "1.0.1" +version = "1.2.2" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" files = [ - {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, - {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, + {file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"}, + {file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"}, ] [package.extras] @@ -3713,285 +4211,349 @@ cli = ["click (>=5.0)"] [[package]] name = "python-multipart" -version = "0.0.16" +version = "0.0.32" description = "A streaming multipart parser for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" files = [ - {file = "python_multipart-0.0.16-py3-none-any.whl", hash = "sha256:c2759b7b976ef3937214dfb592446b59dfaa5f04682a076f78b117c94776d87a"}, - {file = "python_multipart-0.0.16.tar.gz", hash = "sha256:8dee37b88dab9b59922ca173c35acb627cc12ec74019f5cd4578369c6df36554"}, + {file = "python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23"}, + {file = "python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e"}, ] [[package]] name = "pytz" -version = "2023.4" +version = "2026.2" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ - {file = "pytz-2023.4-py2.py3-none-any.whl", hash = "sha256:f90ef520d95e7c46951105338d918664ebfd6f1d995bd7d153127ce90efafa6a"}, - {file = "pytz-2023.4.tar.gz", hash = "sha256:31d4583c4ed539cd037956140d695e42c033a19e984bfce9964a3f7d59bc2b40"}, + {file = "pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126"}, + {file = "pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a"}, ] [[package]] name = "pyunormalize" -version = "16.0.0" -description = "Unicode normalization forms (NFC, NFKC, NFD, NFKD). A library independent of the Python core Unicode database." +version = "17.0.0" +description = "A library for Unicode normalization (NFC, NFD, NFKC, NFKD) independent of Python's core Unicode database." optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "pyunormalize-16.0.0-py3-none-any.whl", hash = "sha256:c647d95e5d1e2ea9a2f448d1d95d8518348df24eab5c3fd32d2b5c3300a49152"}, - {file = "pyunormalize-16.0.0.tar.gz", hash = "sha256:2e1dfbb4a118154ae26f70710426a52a364b926c9191f764601f5a8cb12761f7"}, + {file = "pyunormalize-17.0.0-py3-none-any.whl", hash = "sha256:f0d93b076f938db2b26d319d04f2b58505d1cd7a80b5b72badbe7d1aa4d2a31c"}, + {file = "pyunormalize-17.0.0.tar.gz", hash = "sha256:0949a3e56817e287febcaf1b0cc4b5adf0bb107628d379335938040947eec792"}, ] [[package]] name = "pywin32" -version = "308" -description = "Python for Window Extensions" +version = "312" +description = "Python for Windows Extensions" optional = false -python-versions = "*" +python-versions = ">=3.9" files = [ - {file = "pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e"}, - {file = "pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e"}, - {file = "pywin32-308-cp310-cp310-win_arm64.whl", hash = "sha256:a5ab5381813b40f264fa3495b98af850098f814a25a63589a8e9eb12560f450c"}, - {file = "pywin32-308-cp311-cp311-win32.whl", hash = "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a"}, - {file = "pywin32-308-cp311-cp311-win_amd64.whl", hash = "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b"}, - {file = "pywin32-308-cp311-cp311-win_arm64.whl", hash = "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6"}, - {file = "pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897"}, - {file = "pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47"}, - {file = "pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091"}, - {file = "pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed"}, - {file = "pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4"}, - {file = "pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd"}, - {file = "pywin32-308-cp37-cp37m-win32.whl", hash = "sha256:1f696ab352a2ddd63bd07430080dd598e6369152ea13a25ebcdd2f503a38f1ff"}, - {file = "pywin32-308-cp37-cp37m-win_amd64.whl", hash = "sha256:13dcb914ed4347019fbec6697a01a0aec61019c1046c2b905410d197856326a6"}, - {file = "pywin32-308-cp38-cp38-win32.whl", hash = "sha256:5794e764ebcabf4ff08c555b31bd348c9025929371763b2183172ff4708152f0"}, - {file = "pywin32-308-cp38-cp38-win_amd64.whl", hash = "sha256:3b92622e29d651c6b783e368ba7d6722b1634b8e70bd376fd7610fe1992e19de"}, - {file = "pywin32-308-cp39-cp39-win32.whl", hash = "sha256:7873ca4dc60ab3287919881a7d4f88baee4a6e639aa6962de25a98ba6b193341"}, - {file = "pywin32-308-cp39-cp39-win_amd64.whl", hash = "sha256:71b3322d949b4cc20776436a9c9ba0eeedcbc9c650daa536df63f0ff111bb920"}, + {file = "pywin32-312-cp310-cp310-win32.whl", hash = "sha256:772235332b5d1024c696f11cea1ae4be7930f0a8b894bb43db14e3f435f1ff7e"}, + {file = "pywin32-312-cp310-cp310-win_amd64.whl", hash = "sha256:5dbc35d2b5320dc07f25fa31269cfb767471002b17de5eb067d03da68c7cb2db"}, + {file = "pywin32-312-cp310-cp310-win_arm64.whl", hash = "sha256:3020656e34f1cf7faeb7bccd2b84653a607c6ff0c55ada85e6487d61716deabd"}, + {file = "pywin32-312-cp311-cp311-win32.whl", hash = "sha256:17948aeadbdb091f0ced6ef0841620794e68327b94ee415571c1203594b7215c"}, + {file = "pywin32-312-cp311-cp311-win_amd64.whl", hash = "sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a"}, + {file = "pywin32-312-cp311-cp311-win_arm64.whl", hash = "sha256:b2200a054ca6d6625c4842fc56a4976a4b47f96b73dbe5538c3f813a80359f47"}, + {file = "pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b"}, + {file = "pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc"}, + {file = "pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950"}, + {file = "pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c"}, + {file = "pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9"}, + {file = "pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831"}, + {file = "pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b"}, + {file = "pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e"}, + {file = "pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa"}, + {file = "pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed"}, + {file = "pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5"}, + {file = "pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9"}, + {file = "pywin32-312-cp39-cp39-win32.whl", hash = "sha256:d620900033cc7531e50727c3c8333091df5dd3ffe6d68cdca38c03f5821408d5"}, + {file = "pywin32-312-cp39-cp39-win_amd64.whl", hash = "sha256:dc90147579a905b8635e1b0ec6514967dcb07e6e0d9c42f1477feef14cac23bb"}, + {file = "pywin32-312-cp39-cp39-win_arm64.whl", hash = "sha256:02ebca0f0242b75292e218065004310d6a477407c09fa449bfe4f6022bc0c0fc"}, ] [[package]] name = "pyyaml" -version = "6.0.1" +version = "6.0.3" description = "YAML parser and emitter for Python" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, + {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, + {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, + {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, + {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, + {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, + {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, + {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, + {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, + {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, + {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, + {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, + {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, + {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, ] [[package]] name = "redis" -version = "5.0.8" +version = "8.0.1" description = "Python client for Redis database and key-value store" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" files = [ - {file = "redis-5.0.8-py3-none-any.whl", hash = "sha256:56134ee08ea909106090934adc36f65c9bcbbaecea5b21ba704ba6fb561f8eb4"}, - {file = "redis-5.0.8.tar.gz", hash = "sha256:0c5b10d387568dfe0698c6fad6615750c24170e548ca2deac10c649d463e9870"}, + {file = "redis-8.0.1-py3-none-any.whl", hash = "sha256:47daa35a058c23468d6437f17a8c76882cb316b838ef763036af99b96cedd743"}, + {file = "redis-8.0.1.tar.gz", hash = "sha256:afc5a7a2f5a084f5b1880dec548dd45be17db7e43c82a30d84f952aefb05cfb0"}, ] [package.dependencies] async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} [package.extras] -hiredis = ["hiredis (>1.0.0)"] -ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] +circuit-breaker = ["pybreaker (>=1.4.0)"] +hiredis = ["hiredis (>=3.2.0)"] +jwt = ["pyjwt (>=2.13.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (>=20.0.1)", "requests (>=2.31.0)"] +otel = ["opentelemetry-api (>=1.39.1)", "opentelemetry-exporter-otlp-proto-http (>=1.39.1)", "opentelemetry-sdk (>=1.39.1)"] +xxhash = ["xxhash (>=3.6.0,<3.7.0)"] [[package]] name = "regex" -version = "2024.11.6" +version = "2026.6.28" description = "Alternative regular expression module, to replace re." optional = false -python-versions = ">=3.8" -files = [ - {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"}, - {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"}, - {file = "regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e"}, - {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde"}, - {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e"}, - {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2"}, - {file = "regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf"}, - {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c"}, - {file = "regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86"}, - {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67"}, - {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d"}, - {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2"}, - {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008"}, - {file = "regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62"}, - {file = "regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e"}, - {file = "regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519"}, - {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638"}, - {file = "regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7"}, - {file = "regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20"}, - {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114"}, - {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3"}, - {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f"}, - {file = "regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0"}, - {file = "regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55"}, - {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89"}, - {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d"}, - {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34"}, - {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d"}, - {file = "regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45"}, - {file = "regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9"}, - {file = "regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60"}, - {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a"}, - {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9"}, - {file = "regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2"}, - {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4"}, - {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577"}, - {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3"}, - {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e"}, - {file = "regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe"}, - {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e"}, - {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29"}, - {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39"}, - {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51"}, - {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad"}, - {file = "regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54"}, - {file = "regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b"}, - {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84"}, - {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4"}, - {file = "regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0"}, - {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0"}, - {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7"}, - {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7"}, - {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c"}, - {file = "regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3"}, - {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07"}, - {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e"}, - {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6"}, - {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4"}, - {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d"}, - {file = "regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff"}, - {file = "regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a"}, - {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3a51ccc315653ba012774efca4f23d1d2a8a8f278a6072e29c7147eee7da446b"}, - {file = "regex-2024.11.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ad182d02e40de7459b73155deb8996bbd8e96852267879396fb274e8700190e3"}, - {file = "regex-2024.11.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba9b72e5643641b7d41fa1f6d5abda2c9a263ae835b917348fc3c928182ad467"}, - {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40291b1b89ca6ad8d3f2b82782cc33807f1406cf68c8d440861da6304d8ffbbd"}, - {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdf58d0e516ee426a48f7b2c03a332a4114420716d55769ff7108c37a09951bf"}, - {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a36fdf2af13c2b14738f6e973aba563623cb77d753bbbd8d414d18bfaa3105dd"}, - {file = "regex-2024.11.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1cee317bfc014c2419a76bcc87f071405e3966da434e03e13beb45f8aced1a6"}, - {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50153825ee016b91549962f970d6a4442fa106832e14c918acd1c8e479916c4f"}, - {file = "regex-2024.11.6-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea1bfda2f7162605f6e8178223576856b3d791109f15ea99a9f95c16a7636fb5"}, - {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:df951c5f4a1b1910f1a99ff42c473ff60f8225baa1cdd3539fe2819d9543e9df"}, - {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:072623554418a9911446278f16ecb398fb3b540147a7828c06e2011fa531e773"}, - {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f654882311409afb1d780b940234208a252322c24a93b442ca714d119e68086c"}, - {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:89d75e7293d2b3e674db7d4d9b1bee7f8f3d1609428e293771d1a962617150cc"}, - {file = "regex-2024.11.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:f65557897fc977a44ab205ea871b690adaef6b9da6afda4790a2484b04293a5f"}, - {file = "regex-2024.11.6-cp38-cp38-win32.whl", hash = "sha256:6f44ec28b1f858c98d3036ad5d7d0bfc568bdd7a74f9c24e25f41ef1ebfd81a4"}, - {file = "regex-2024.11.6-cp38-cp38-win_amd64.whl", hash = "sha256:bb8f74f2f10dbf13a0be8de623ba4f9491faf58c24064f32b65679b021ed0001"}, - {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839"}, - {file = "regex-2024.11.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e"}, - {file = "regex-2024.11.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf"}, - {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b"}, - {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0"}, - {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b"}, - {file = "regex-2024.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef"}, - {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48"}, - {file = "regex-2024.11.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13"}, - {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2"}, - {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95"}, - {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9"}, - {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f"}, - {file = "regex-2024.11.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b"}, - {file = "regex-2024.11.6-cp39-cp39-win32.whl", hash = "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57"}, - {file = "regex-2024.11.6-cp39-cp39-win_amd64.whl", hash = "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983"}, - {file = "regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519"}, +python-versions = ">=3.10" +files = [ + {file = "regex-2026.6.28-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b83932645630965fd860fdb70ebbf964bf3e8007f08851ea424d01f8d35454a8"}, + {file = "regex-2026.6.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e81f1952355042e517dc9861ce65c676e4a098f42402993c40461786d1f794d4"}, + {file = "regex-2026.6.28-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2097591101d70bcc108af64c46f6066bb698ee067fec5f75beac0be317639311"}, + {file = "regex-2026.6.28-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31d7538a614b5842bf53ce329d07b43f97754ca7e6db8d69f347e071bce1c953"}, + {file = "regex-2026.6.28-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5561e47bbe2b75373b695326507743fcdd4d2cc7f5022312024ccf39fa094e0"}, + {file = "regex-2026.6.28-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c10f2c5a55ab3dd8318d8ad5f11b530e2691c0edebebde7713066f484902c3fb"}, + {file = "regex-2026.6.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23f7e0cc60c72486b42a685f1ff4eec90d50d4fb05e4f9c7d5363b03aa02600d"}, + {file = "regex-2026.6.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:76493755f79a88d5ed2c9e63a41d3c05997e0a7ffbe76ed8c4ded8be35b8b14c"}, + {file = "regex-2026.6.28-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ff0f41a00f23ea5054acb61901380c41813d813eee3f80f800995710bcc52ecd"}, + {file = "regex-2026.6.28-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3c60b297292e7e1ef5d02a4759f9e452ee4c8bb95e168d8fd0b5db01bd806f9f"}, + {file = "regex-2026.6.28-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a7cf03c87f7b9cbc25a8894cf9be83818406677b6b391b003ec7c884923387b5"}, + {file = "regex-2026.6.28-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:418208ea0af51cfed4f46eb9b1ea7cfc990ca284f0084ecbd951460fb089421e"}, + {file = "regex-2026.6.28-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:7635fa2cddb917a6bbfac7890602573d2d8c4e470703b0640e6f86a988817ec3"}, + {file = "regex-2026.6.28-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7bb96c13d6cf5880d31bbef84ca701a64d738aa491c2b79975cc33f8ad00a31e"}, + {file = "regex-2026.6.28-cp310-cp310-win32.whl", hash = "sha256:56f05194c4843957dd8b3af87eb0c52d8cf0509e7f18e172d727f5f8ff840646"}, + {file = "regex-2026.6.28-cp310-cp310-win_amd64.whl", hash = "sha256:70710927033af3b54369f17aaba1343b97a23d0b1aa994fa1512b08b1b8c136a"}, + {file = "regex-2026.6.28-cp310-cp310-win_arm64.whl", hash = "sha256:ed7b30185ee3f8b9b053b0be567b4d226016e2afbebc17fde1c6a4580937b688"}, + {file = "regex-2026.6.28-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a71b51dd08b9b62f055fafab3dee8af8bd2ec81b373a44caef18d6c5ca28f43a"}, + {file = "regex-2026.6.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9c26a47770d30a0f85c01e261d2a3ebc342c4af6fd666dbd8c1fe4cbf3adf726"}, + {file = "regex-2026.6.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e5efbc1af38f97e300d43028e5a92e752d924bcfb7f465d8669d5d5a6e78c233"}, + {file = "regex-2026.6.28-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1758df6fdd8c800620a5638958720e8a635e1da49a2f09df2dd63e94a24ec4a"}, + {file = "regex-2026.6.28-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ad73ecf20c1ef5c975639f8bf845a9370fcf7dada7edc1e3b0bca20e2f8202f6"}, + {file = "regex-2026.6.28-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4d80c798b0eec6ea3d45f8816a1e8886c5664615d347d89e8c075b576a1b5a5d"}, + {file = "regex-2026.6.28-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a361feeaf1b6ba1df060f2ff5c5947092edf537a35ce78e76387ac56d3e0f4a4"}, + {file = "regex-2026.6.28-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b92366d9c8bba9642989534073662abdd9b41faf7603a7ae71597833f3b88f0"}, + {file = "regex-2026.6.28-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:11251768cc23f097dd61b18f67966e70f74da822784d17e12a444eb6b29d4288"}, + {file = "regex-2026.6.28-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ad5c67786145ec28a71a267d9f9d92bdc8d70d65541eea852c253f520a01f918"}, + {file = "regex-2026.6.28-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f1da438e739765c3e85175ede05816cbede3caaacb1e0680568bda6119bfdfca"}, + {file = "regex-2026.6.28-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d98b639046e51c5de64d9f77351532105e99ca271cb6f7640e1f903d6ab63032"}, + {file = "regex-2026.6.28-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e164ace4dbab5c6ad4a4ac7c41a2638fe226d0c770a86f2eb041f594bac6ee7"}, + {file = "regex-2026.6.28-cp311-cp311-win32.whl", hash = "sha256:3169a3159e4d99d9ae85ff0ed90ef3b8906cc3152653b6078b842ace6c8f72c3"}, + {file = "regex-2026.6.28-cp311-cp311-win_amd64.whl", hash = "sha256:5977295b0a74e8241df8a4b3b27b12412a831f6fa32ee8b755039592cd768c3d"}, + {file = "regex-2026.6.28-cp311-cp311-win_arm64.whl", hash = "sha256:f5fbaef40c3e9282ccee4b075f5600a0d858aa0c34147732f1baa69c8188a95d"}, + {file = "regex-2026.6.28-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:81cc5793ad33a10444445e8d29d3c73e752c8fb2e120772d70fcb6d41df40fe1"}, + {file = "regex-2026.6.28-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e18225243250a1f7d7e5e5d883f3b96465cd79031acf5c6db902b7025f2125d9"}, + {file = "regex-2026.6.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ecd1638b1c2db1f2d01c182a4b0d3e2e88b0e99910320a745c1727ee3638ddab"}, + {file = "regex-2026.6.28-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4303ebe16b74eeb3fe2715745023266fea92fd44a23f3e7bb2fb48c7a7bbc195"}, + {file = "regex-2026.6.28-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56b856b70b96c381d837f609eee442a1bd320cd2159f5c294b679552fb1a7eaf"}, + {file = "regex-2026.6.28-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f74675ab76ab1d005ffba4dee308e53e89efc22be6e9f9fae5b539a3f81bdff2"}, + {file = "regex-2026.6.28-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90581684565a93f7258af1e5d3f41ef20d7d7c61f2a428183a342bcb65485e38"}, + {file = "regex-2026.6.28-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:28f9e6c28f9b90f6f784595a33240a57e181e61b6ee3dc259b25c61e356d1aa3"}, + {file = "regex-2026.6.28-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:378a71d861fc7c8806b04ac5b133d53c0e774f92f5d9663a539872d3fa2b0417"}, + {file = "regex-2026.6.28-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4cc199874ecd6267a49b111052250825bfe19b5101b23b2ba80f54efa3e0994e"}, + {file = "regex-2026.6.28-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b916a10431494ef4b4d62c6c89cab6426af7873125b8cd6c15811bf5fc58eec8"}, + {file = "regex-2026.6.28-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2e27727fba075f1e4409416d2f537d4c30fc11f012ea507f7bd74d3e19ecb57a"}, + {file = "regex-2026.6.28-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:700fc6a7844bb2c4149292ac79d1df8841a00acd4d45cd32c1ebc7bcc1fd0da8"}, + {file = "regex-2026.6.28-cp312-cp312-win32.whl", hash = "sha256:03376d60b6a11aecb88a79fa2be06b40faa01c6693bc31ef69435cd4818b9463"}, + {file = "regex-2026.6.28-cp312-cp312-win_amd64.whl", hash = "sha256:fbd2ded482bf99e6651992bbfcde460272724d4bbc49ef3d6b46d9312867ec84"}, + {file = "regex-2026.6.28-cp312-cp312-win_arm64.whl", hash = "sha256:37294d3d7ddb64c7e89184b2894e0f8f0a19c514bc59513d71fe692c3a8d5fc6"}, + {file = "regex-2026.6.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b295a83426e0e44e9e60fde99789e181bd26788a1890ae7fe2a24c69bb6246ca"}, + {file = "regex-2026.6.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0c31665c0deb5c111557a1cac8c27bd5629e2f9e7fd5058900a03576c33b601c"}, + {file = "regex-2026.6.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6bf295f2c59de77d1ea7de053607ae4dc9ceb3d57bbb6c7ec51ef4acc4ccff94"}, + {file = "regex-2026.6.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17c077586770f67e05bbffeba07fbee6b2b22244f4d4caf8d94e59d574befe04"}, + {file = "regex-2026.6.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e6cb5a61486f9062397d2e189573b39d38ecfaed698fd9fb6e2756a8ebb8762"}, + {file = "regex-2026.6.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e86e91a2664f44c3a4e363a7d78fb17c27d5046882e30ea5a877f5e89b28d2ba"}, + {file = "regex-2026.6.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4dfd1331c49233998d84fc5f1f4436cf7a435a7655f6cf0f490229bb5c7254e5"}, + {file = "regex-2026.6.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cadea12805a1bce0b091c302b814207be26fb60a9c0e7f9ad2f9e21790a429fe"}, + {file = "regex-2026.6.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f2c1682b67ad5d2376498f2a5a2a8f782fa2e4a06d0465b5e357799806e8a20"}, + {file = "regex-2026.6.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:64e142eb55e84868087da1375d7c36ff97d55010951849f515322a91d5fef1b4"}, + {file = "regex-2026.6.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:abb4daabe7be63273787a62dfd6164dadf8f7a63fbec3d2730e5e5e7126d858c"}, + {file = "regex-2026.6.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec2b2ad00ab8c16a2798cc8db80c53c4d5b8b3a2441f6cbaef06625f5ca25854"}, + {file = "regex-2026.6.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bfc9677982c914d9085b8e1c3b3ae6e88f139fb56531c2416d6c8f338093c22b"}, + {file = "regex-2026.6.28-cp313-cp313-win32.whl", hash = "sha256:bf54bc693fc4e0530e666ba5ec4bcba14dbe8f66b7cfc15c27317d1a6e40b9a5"}, + {file = "regex-2026.6.28-cp313-cp313-win_amd64.whl", hash = "sha256:e128feaf65bf3d9eb91bec92322a8f7e4835e9c798f3e9ea4b69f4def85620e3"}, + {file = "regex-2026.6.28-cp313-cp313-win_arm64.whl", hash = "sha256:695873e0ea8d3815ea9e92e2c68faf039cc450e2c0a62a31afe2049eb11be767"}, + {file = "regex-2026.6.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:189dbf9fc4252d9f1352bf4bd1bef885edb6cc4b7341df202a65f821aaa3891c"}, + {file = "regex-2026.6.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9277a4c6503390aa39cb4483b87ec0384faee0850a23b5cea33d008b5d8d83f1"}, + {file = "regex-2026.6.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17eddca4e8ea9af0b5739314776cdf0172a49731ab61f2e1ea66e066ddd46c97"}, + {file = "regex-2026.6.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4466b8641e00c697aab5a73150150d2b2ea96b131c595691f42031abafd9f4d"}, + {file = "regex-2026.6.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9cfcd4b0bdcf768c498415c170d1ed2a25a99bf0b65fa253bbd02f68ceba6475"}, + {file = "regex-2026.6.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:80c7adf1ef647f6b1e8aa2ca280e517174cd08bdf7a2e412cdfb68bd6a0917cb"}, + {file = "regex-2026.6.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a043f5770e82283a22aed4cefef1a4e0f9dd8fd7184cb6ce0ad2e579e2134a9e"}, + {file = "regex-2026.6.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3bd630a8dba06b55254ea5ee862194edab52ec783100d2ef1cd15a9c512fee27"}, + {file = "regex-2026.6.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b77207e3cee13086f1906a6a2a12b41244c577e8ad9370d4b35ae1d548d354f3"}, + {file = "regex-2026.6.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:6de82c268e5d101ee9e3ffd869924aa9a371e3a21e752cf4fa17b6ce50d219f7"}, + {file = "regex-2026.6.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b15859e3908544fb99cf47341dcf0bfd089147d258c4c4d8a29e5b087f8085cb"}, + {file = "regex-2026.6.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:c91487a917edd48a1ea646fdf60d7936d304f0e686fa7ea8326e47efca51d816"}, + {file = "regex-2026.6.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4ac65f3e3a99fd8f3a4a74e7a6610acd1ce9dfe9b8a03d346a4922380d68aeb"}, + {file = "regex-2026.6.28-cp313-cp313t-win32.whl", hash = "sha256:3f6316f258bc7e6c9c2acbe9954947bbd397a81be3742a637a555f1855d6618d"}, + {file = "regex-2026.6.28-cp313-cp313t-win_amd64.whl", hash = "sha256:1484bdd6fba28422df9b5ebb04055b2e1b680e8e4f08490bb21ff0f3cc50d0ab"}, + {file = "regex-2026.6.28-cp313-cp313t-win_arm64.whl", hash = "sha256:3f15020f0b69cafe57baa067ff65b29acef68ff6b1670a53bef1ca11d708e02d"}, + {file = "regex-2026.6.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:f7c032b0c8a73739ff8ff1aaf30c281fa19c17bf7f1543256c8507390db7807c"}, + {file = "regex-2026.6.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f6710f512c57b84f127a23d0f59560a03b64136eff419ae1be5ab557577fe5e3"}, + {file = "regex-2026.6.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c0013958f427bd82509a186b9ff206d66cb8d60a81fc797a4c717afd18c5b0ba"}, + {file = "regex-2026.6.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f06cdcd6421f8e194ad312ea608020381250df9b8a57661c1b57e9e5273878"}, + {file = "regex-2026.6.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec9689392f7494ff4e3f8e7e8522f9158f11023f337eaaf04a64542fc45bbf26"}, + {file = "regex-2026.6.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aa084684e6d2078bf6139e374d1fc2af5ddc1ac7122759a2db716d68169f6fd0"}, + {file = "regex-2026.6.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40455e6840dc4e96a6fe50f4cedc957de2752c954d91e789812be55d49be199a"}, + {file = "regex-2026.6.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:530b5c223b9ca5dd8370ac502e080aee0e4ded32be987c6564b425fb5523d581"}, + {file = "regex-2026.6.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e0ed273ecd1a89be84466c1749bfe58609cc2a32b5d5e05006c4625ba96411b"}, + {file = "regex-2026.6.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0ab0d5344311fc8e8667078942056c3b9c9b4a4b1cc99f2eb8a5af54554f4acc"}, + {file = "regex-2026.6.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:eacb79625323d9f7e7925366b917f492b8356fad58f5dc4fa12ff8c21d8f4ca9"}, + {file = "regex-2026.6.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:20f4d87702702aa1d572721e146f301660c50eef6fd6cb596e48a22b0ace17db"}, + {file = "regex-2026.6.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e693940a3b9e6d6e4dc2a54ecaa74b74934f77af1ef95f518a74261ef7cc1bc"}, + {file = "regex-2026.6.28-cp314-cp314-win32.whl", hash = "sha256:234a51e20ebc18ab83b2c0600cf28f2e884560a0e00f743878f0b7d8e7c4cf03"}, + {file = "regex-2026.6.28-cp314-cp314-win_amd64.whl", hash = "sha256:7b15c437bc4604f03ceb3f8d37eae2f8930e320e1bc556b259848c639d9eec1a"}, + {file = "regex-2026.6.28-cp314-cp314-win_arm64.whl", hash = "sha256:c6e6f790d01380a74ad564f216c533b86504afb61bf66f2b2e11e7f1a3e287a7"}, + {file = "regex-2026.6.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3527a72adcbe9e3600f1553b497d397c1a371d227580d41d96c3c5964109b65c"}, + {file = "regex-2026.6.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a644f6408692812f5ead82519eed680e08d5d546fddbd9f7d9514e3c73899aa5"}, + {file = "regex-2026.6.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8e2fae6bb883648346f84db270dc9aafc29d8e895f62b88a75ccc83b09519820"}, + {file = "regex-2026.6.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:debe623e09cee97ef9404575e936c610aac9bb08358c5099aaef14644a6871f2"}, + {file = "regex-2026.6.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc579c91fb4605773483a8d940b136bcc5b854fff44fa14a1572a038f46563f1"}, + {file = "regex-2026.6.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7c42be203d84ecf7d487ff23f8a61ef0eb0534fa0fc317a2fce8c065d20618f"}, + {file = "regex-2026.6.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8184b4e2fdaf9cdfe77e38f15a4d9dc149168c9c29eb0ea17c5481d3bb80546"}, + {file = "regex-2026.6.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:697f103104f5872d64078d8eeac59979960be8ee76115a2d3f31096312e2a400"}, + {file = "regex-2026.6.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:714d2b1aa29beef0ddfcdc72ad0771c05326551a8bb0680b0ddf74bfaad87387"}, + {file = "regex-2026.6.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0f09f62e450cc2f113018cc8412aeea3a120a04e1ca7e801a0d441583f9a3b06"}, + {file = "regex-2026.6.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:731ea12d5aeb2577eaef2393d6428b995f76eb35f68a89e03e15a97719d1de19"}, + {file = "regex-2026.6.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:51e952c8783eabd4706d0f63922f219bcfc1bef9b8cb35941c0d1a0396578858"}, + {file = "regex-2026.6.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43248fe4c0ab8fbb223588a0795b11268940072c97bba30ea8f9b49d8cdfde34"}, + {file = "regex-2026.6.28-cp314-cp314t-win32.whl", hash = "sha256:fc1eddc25ad23c0f1344ab280d961ac595ead48292d7c779497975942373f493"}, + {file = "regex-2026.6.28-cp314-cp314t-win_amd64.whl", hash = "sha256:ede8d8e53b6dde0a50f7eca902f0af76d87ab02a55aba7542da68ae3e5dfe83d"}, + {file = "regex-2026.6.28-cp314-cp314t-win_arm64.whl", hash = "sha256:4da6f6a72f8700b97a1a765e837fb7d5750bfd9f13acea7bae498f573e3a70a8"}, + {file = "regex-2026.6.28.tar.gz", hash = "sha256:3cb4b6c5cb3060cc31efdc1fbb27c25fb9b29044afd87e40601a1c4d9db54342"}, ] [[package]] name = "requests" -version = "2.31.0" +version = "2.34.2" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, + {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, + {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, ] [package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" +certifi = ">=2023.5.7" +charset_normalizer = ">=2,<4" idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" +urllib3 = ">=1.26,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] [[package]] name = "rich" -version = "13.9.3" +version = "15.0.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false -python-versions = ">=3.8.0" +python-versions = ">=3.9.0" files = [ - {file = "rich-13.9.3-py3-none-any.whl", hash = "sha256:9836f5096eb2172c9e77df411c1b009bace4193d6a481d534fea75ebba758283"}, - {file = "rich-13.9.3.tar.gz", hash = "sha256:bc1e01b899537598cf02579d2b9f4a415104d3fc439313a7a2c165d76557a08e"}, + {file = "rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb"}, + {file = "rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36"}, ] [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] +[[package]] +name = "rich-toolkit" +version = "0.20.1" +description = "Rich toolkit for building command-line applications" +optional = false +python-versions = ">=3.8" +files = [ + {file = "rich_toolkit-0.20.1-py3-none-any.whl", hash = "sha256:2a6d5f8e15759b9eba5a9ee63da10b275359ead20e5a0fc92bd5b4dbae8ce4bf"}, + {file = "rich_toolkit-0.20.1.tar.gz", hash = "sha256:c7336ae281f435c785acecaedc4b71d4b663dc73d9c8079fea96372527e822a4"}, +] + +[package.dependencies] +click = ">=8.1.7" +rich = ">=13.7.1" +typing-extensions = ">=4.12.2" + [[package]] name = "rlp" version = "4.1.0" @@ -4012,198 +4574,129 @@ docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme rust-backend = ["rusty-rlp (>=0.2.1)"] test = ["hypothesis (>=6.22.0,<6.108.7)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] -[[package]] -name = "rsa" -version = "4.9" -description = "Pure-Python RSA implementation" -optional = false -python-versions = ">=3.6,<4" -files = [ - {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, - {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, -] - -[package.dependencies] -pyasn1 = ">=0.1.3" - [[package]] name = "ruamel-yaml" -version = "0.18.5" +version = "0.19.1" description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "ruamel.yaml-0.18.5-py3-none-any.whl", hash = "sha256:a013ac02f99a69cdd6277d9664689eb1acba07069f912823177c5eced21a6ada"}, - {file = "ruamel.yaml-0.18.5.tar.gz", hash = "sha256:61917e3a35a569c1133a8f772e1226961bf5a1198bea7e23f06a0841dea1ab0e"}, + {file = "ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93"}, + {file = "ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993"}, ] -[package.dependencies] -"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.13\""} - [package.extras] docs = ["mercurial (>5.7)", "ryd"] jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] - -[[package]] -name = "ruamel-yaml-clib" -version = "0.2.8" -description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" -optional = false -python-versions = ">=3.6" -files = [ - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win32.whl", hash = "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win32.whl", hash = "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win32.whl", hash = "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win_amd64.whl", hash = "sha256:1758ce7d8e1a29d23de54a16ae867abd370f01b5a69e1a3ba75223eaa3ca1a1b"}, - {file = "ruamel.yaml.clib-0.2.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win32.whl", hash = "sha256:75e1ed13e1f9de23c5607fe6bd1aeaae21e523b32d83bb33918245361e9cc51b"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win_amd64.whl", hash = "sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win32.whl", hash = "sha256:955eae71ac26c1ab35924203fda6220f84dce57d6d7884f189743e2abe3a9fbe"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win_amd64.whl", hash = "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win32.whl", hash = "sha256:84b554931e932c46f94ab306913ad7e11bba988104c5cff26d90d03f68258cd5"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-win_amd64.whl", hash = "sha256:25ac8c08322002b06fa1d49d1646181f0b2c72f5cbc15a85e80b4c30a544bb15"}, - {file = "ruamel.yaml.clib-0.2.8.tar.gz", hash = "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512"}, -] +libyaml = ["ruamel.yaml.clibz (>=0.3.7)"] +oldlibyaml = ["ruamel.yaml.clib"] [[package]] name = "ruff" -version = "0.6.0" +version = "0.6.9" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.6.0-py3-none-linux_armv6l.whl", hash = "sha256:92dcce923e5df265781e5fc76f9a1edad52201a7aafe56e586b90988d5239013"}, - {file = "ruff-0.6.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:31b90ff9dc79ed476c04e957ba7e2b95c3fceb76148f2079d0d68a908d2cfae7"}, - {file = "ruff-0.6.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6d834a9ec9f8287dd6c3297058b3a265ed6b59233db22593379ee38ebc4b9768"}, - {file = "ruff-0.6.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2089267692696aba342179471831a085043f218706e642564812145df8b8d0d"}, - {file = "ruff-0.6.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa62b423ee4bbd8765f2c1dbe8f6aac203e0583993a91453dc0a449d465c84da"}, - {file = "ruff-0.6.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7344e1a964b16b1137ea361d6516ce4ee61a0403fa94252a1913ecc1311adcae"}, - {file = "ruff-0.6.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:487f3a35c3f33bf82be212ce15dc6278ea854e35573a3f809442f73bec8b2760"}, - {file = "ruff-0.6.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75db409984077a793cf344d499165298a6f65449e905747ac65983b12e3e64b1"}, - {file = "ruff-0.6.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84908bd603533ecf1db456d8fc2665d1f4335d722e84bc871d3bbd2d1116c272"}, - {file = "ruff-0.6.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f1749a0aef3ec41ed91a0e2127a6ae97d2e2853af16dbd4f3c00d7a3af726c5"}, - {file = "ruff-0.6.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:016fea751e2bcfbbd2f8cb19b97b37b3fd33148e4df45b526e87096f4e17354f"}, - {file = "ruff-0.6.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6ae80f141b53b2e36e230017e64f5ea2def18fac14334ffceaae1b780d70c4f7"}, - {file = "ruff-0.6.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eaaaf33ea4b3f63fd264d6a6f4a73fa224bbfda4b438ffea59a5340f4afa2bb5"}, - {file = "ruff-0.6.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7667ddd1fc688150a7ca4137140867584c63309695a30016880caf20831503a0"}, - {file = "ruff-0.6.0-py3-none-win32.whl", hash = "sha256:ae48365aae60d40865a412356f8c6f2c0be1c928591168111eaf07eaefa6bea3"}, - {file = "ruff-0.6.0-py3-none-win_amd64.whl", hash = "sha256:774032b507c96f0c803c8237ce7d2ef3934df208a09c40fa809c2931f957fe5e"}, - {file = "ruff-0.6.0-py3-none-win_arm64.whl", hash = "sha256:a5366e8c3ae6b2dc32821749b532606c42e609a99b0ae1472cf601da931a048c"}, - {file = "ruff-0.6.0.tar.gz", hash = "sha256:272a81830f68f9bd19d49eaf7fa01a5545c5a2e86f32a9935bb0e4bb9a1db5b8"}, + {file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"}, + {file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"}, + {file = "ruff-0.6.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f"}, + {file = "ruff-0.6.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625"}, + {file = "ruff-0.6.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039"}, + {file = "ruff-0.6.9-py3-none-win32.whl", hash = "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d"}, + {file = "ruff-0.6.9-py3-none-win_amd64.whl", hash = "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117"}, + {file = "ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93"}, + {file = "ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2"}, ] [[package]] name = "s3transfer" -version = "0.10.0" +version = "0.19.0" description = "An Amazon S3 Transfer Manager" optional = false -python-versions = ">= 3.8" +python-versions = ">=3.10" files = [ - {file = "s3transfer-0.10.0-py3-none-any.whl", hash = "sha256:3cdb40f5cfa6966e812209d0994f2a4709b561c88e90cf00c2696d2df4e56b2e"}, - {file = "s3transfer-0.10.0.tar.gz", hash = "sha256:d0c8bbf672d5eebbe4e57945e23b972d963f07d82f661cabf678a5c88831595b"}, + {file = "s3transfer-0.19.0-py3-none-any.whl", hash = "sha256:777cc2415536f1debadb5c2ef7779275d0fc0fe0e042411cdd6caebeb2685262"}, + {file = "s3transfer-0.19.0.tar.gz", hash = "sha256:ce436931687addc4c1712d52d40b32f53e88315723f107ffa20ba82b05a0f685"}, ] [package.dependencies] -botocore = ">=1.33.2,<2.0a.0" +botocore = ">=1.37.4,<2.0a.0" [package.extras] -crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] +crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"] [[package]] name = "scipy" -version = "1.12.0" +version = "1.15.3" description = "Fundamental algorithms for scientific computing in Python" optional = false -python-versions = ">=3.9" -files = [ - {file = "scipy-1.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:78e4402e140879387187f7f25d91cc592b3501a2e51dfb320f48dfb73565f10b"}, - {file = "scipy-1.12.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:f5f00ebaf8de24d14b8449981a2842d404152774c1a1d880c901bf454cb8e2a1"}, - {file = "scipy-1.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e53958531a7c695ff66c2e7bb7b79560ffdc562e2051644c5576c39ff8efb563"}, - {file = "scipy-1.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e32847e08da8d895ce09d108a494d9eb78974cf6de23063f93306a3e419960c"}, - {file = "scipy-1.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4c1020cad92772bf44b8e4cdabc1df5d87376cb219742549ef69fc9fd86282dd"}, - {file = "scipy-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:75ea2a144096b5e39402e2ff53a36fecfd3b960d786b7efd3c180e29c39e53f2"}, - {file = "scipy-1.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:408c68423f9de16cb9e602528be4ce0d6312b05001f3de61fe9ec8b1263cad08"}, - {file = "scipy-1.12.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:5adfad5dbf0163397beb4aca679187d24aec085343755fcdbdeb32b3679f254c"}, - {file = "scipy-1.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3003652496f6e7c387b1cf63f4bb720951cfa18907e998ea551e6de51a04467"}, - {file = "scipy-1.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b8066bce124ee5531d12a74b617d9ac0ea59245246410e19bca549656d9a40a"}, - {file = "scipy-1.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8bee4993817e204d761dba10dbab0774ba5a8612e57e81319ea04d84945375ba"}, - {file = "scipy-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a24024d45ce9a675c1fb8494e8e5244efea1c7a09c60beb1eeb80373d0fecc70"}, - {file = "scipy-1.12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e7e76cc48638228212c747ada851ef355c2bb5e7f939e10952bc504c11f4e372"}, - {file = "scipy-1.12.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:f7ce148dffcd64ade37b2df9315541f9adad6efcaa86866ee7dd5db0c8f041c3"}, - {file = "scipy-1.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c39f92041f490422924dfdb782527a4abddf4707616e07b021de33467f917bc"}, - {file = "scipy-1.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7ebda398f86e56178c2fa94cad15bf457a218a54a35c2a7b4490b9f9cb2676c"}, - {file = "scipy-1.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:95e5c750d55cf518c398a8240571b0e0782c2d5a703250872f36eaf737751338"}, - {file = "scipy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e646d8571804a304e1da01040d21577685ce8e2db08ac58e543eaca063453e1c"}, - {file = "scipy-1.12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:913d6e7956c3a671de3b05ccb66b11bc293f56bfdef040583a7221d9e22a2e35"}, - {file = "scipy-1.12.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba1b0c7256ad75401c73e4b3cf09d1f176e9bd4248f0d3112170fb2ec4db067"}, - {file = "scipy-1.12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:730badef9b827b368f351eacae2e82da414e13cf8bd5051b4bdfd720271a5371"}, - {file = "scipy-1.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6546dc2c11a9df6926afcbdd8a3edec28566e4e785b915e849348c6dd9f3f490"}, - {file = "scipy-1.12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:196ebad3a4882081f62a5bf4aeb7326aa34b110e533aab23e4374fcccb0890dc"}, - {file = "scipy-1.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:b360f1b6b2f742781299514e99ff560d1fe9bd1bff2712894b52abe528d1fd1e"}, - {file = "scipy-1.12.0.tar.gz", hash = "sha256:4bf5abab8a36d20193c698b0f1fc282c1d083c94723902c447e5d2f1780936a3"}, +python-versions = ">=3.10" +files = [ + {file = "scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c"}, + {file = "scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253"}, + {file = "scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f"}, + {file = "scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92"}, + {file = "scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82"}, + {file = "scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40"}, + {file = "scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e"}, + {file = "scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c"}, + {file = "scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13"}, + {file = "scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b"}, + {file = "scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba"}, + {file = "scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65"}, + {file = "scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1"}, + {file = "scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889"}, + {file = "scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982"}, + {file = "scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9"}, + {file = "scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594"}, + {file = "scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb"}, + {file = "scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019"}, + {file = "scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6"}, + {file = "scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477"}, + {file = "scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c"}, + {file = "scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45"}, + {file = "scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49"}, + {file = "scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e"}, + {file = "scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539"}, + {file = "scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed"}, + {file = "scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759"}, + {file = "scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62"}, + {file = "scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb"}, + {file = "scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730"}, + {file = "scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825"}, + {file = "scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7"}, + {file = "scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11"}, + {file = "scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126"}, + {file = "scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163"}, + {file = "scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8"}, + {file = "scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5"}, + {file = "scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e"}, + {file = "scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb"}, + {file = "scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723"}, + {file = "scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb"}, + {file = "scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4"}, + {file = "scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5"}, + {file = "scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca"}, + {file = "scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf"}, ] [package.dependencies] -numpy = ">=1.22.4,<1.29.0" - -[package.extras] -dev = ["click", "cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"] -doc = ["jupytext", "matplotlib (>2)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-design (>=0.2.0)"] -test = ["asv", "gmpy2", "hypothesis", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] - -[[package]] -name = "setuptools" -version = "69.0.3" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, - {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, -] +numpy = ">=1.23.5,<2.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] +doc = ["intersphinx_registry", "jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.19.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<8.0.0)", "sphinx-copybutton", "sphinx-design (>=0.4.0)"] +test = ["Cython", "array-api-strict (>=2.0,<2.1.1)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] [[package]] name = "shellingham" @@ -4218,95 +4711,104 @@ files = [ [[package]] name = "six" -version = "1.16.0" +version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] [[package]] name = "sniffio" -version = "1.3.0" +version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" files = [ - {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] [[package]] name = "sqlalchemy" -version = "2.0.25" +version = "2.0.51" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" files = [ - {file = "SQLAlchemy-2.0.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4344d059265cc8b1b1be351bfb88749294b87a8b2bbe21dfbe066c4199541ebd"}, - {file = "SQLAlchemy-2.0.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f9e2e59cbcc6ba1488404aad43de005d05ca56e069477b33ff74e91b6319735"}, - {file = "SQLAlchemy-2.0.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84daa0a2055df9ca0f148a64fdde12ac635e30edbca80e87df9b3aaf419e144a"}, - {file = "SQLAlchemy-2.0.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc8b7dabe8e67c4832891a5d322cec6d44ef02f432b4588390017f5cec186a84"}, - {file = "SQLAlchemy-2.0.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f5693145220517b5f42393e07a6898acdfe820e136c98663b971906120549da5"}, - {file = "SQLAlchemy-2.0.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db854730a25db7c956423bb9fb4bdd1216c839a689bf9cc15fada0a7fb2f4570"}, - {file = "SQLAlchemy-2.0.25-cp310-cp310-win32.whl", hash = "sha256:14a6f68e8fc96e5e8f5647ef6cda6250c780612a573d99e4d881581432ef1669"}, - {file = "SQLAlchemy-2.0.25-cp310-cp310-win_amd64.whl", hash = "sha256:87f6e732bccd7dcf1741c00f1ecf33797383128bd1c90144ac8adc02cbb98643"}, - {file = "SQLAlchemy-2.0.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:342d365988ba88ada8af320d43df4e0b13a694dbd75951f537b2d5e4cb5cd002"}, - {file = "SQLAlchemy-2.0.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f37c0caf14b9e9b9e8f6dbc81bc56db06acb4363eba5a633167781a48ef036ed"}, - {file = "SQLAlchemy-2.0.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa9373708763ef46782d10e950b49d0235bfe58facebd76917d3f5cbf5971aed"}, - {file = "SQLAlchemy-2.0.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24f571990c05f6b36a396218f251f3e0dda916e0c687ef6fdca5072743208f5"}, - {file = "SQLAlchemy-2.0.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75432b5b14dc2fff43c50435e248b45c7cdadef73388e5610852b95280ffd0e9"}, - {file = "SQLAlchemy-2.0.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:884272dcd3ad97f47702965a0e902b540541890f468d24bd1d98bcfe41c3f018"}, - {file = "SQLAlchemy-2.0.25-cp311-cp311-win32.whl", hash = "sha256:e607cdd99cbf9bb80391f54446b86e16eea6ad309361942bf88318bcd452363c"}, - {file = "SQLAlchemy-2.0.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d505815ac340568fd03f719446a589162d55c52f08abd77ba8964fbb7eb5b5f"}, - {file = "SQLAlchemy-2.0.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0dacf67aee53b16f365c589ce72e766efaabd2b145f9de7c917777b575e3659d"}, - {file = "SQLAlchemy-2.0.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b801154027107461ee992ff4b5c09aa7cc6ec91ddfe50d02bca344918c3265c6"}, - {file = "SQLAlchemy-2.0.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59a21853f5daeb50412d459cfb13cb82c089ad4c04ec208cd14dddd99fc23b39"}, - {file = "SQLAlchemy-2.0.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29049e2c299b5ace92cbed0c1610a7a236f3baf4c6b66eb9547c01179f638ec5"}, - {file = "SQLAlchemy-2.0.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b64b183d610b424a160b0d4d880995e935208fc043d0302dd29fee32d1ee3f95"}, - {file = "SQLAlchemy-2.0.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4f7a7d7fcc675d3d85fbf3b3828ecd5990b8d61bd6de3f1b260080b3beccf215"}, - {file = "SQLAlchemy-2.0.25-cp312-cp312-win32.whl", hash = "sha256:cf18ff7fc9941b8fc23437cc3e68ed4ebeff3599eec6ef5eebf305f3d2e9a7c2"}, - {file = "SQLAlchemy-2.0.25-cp312-cp312-win_amd64.whl", hash = "sha256:91f7d9d1c4dd1f4f6e092874c128c11165eafcf7c963128f79e28f8445de82d5"}, - {file = "SQLAlchemy-2.0.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bb209a73b8307f8fe4fe46f6ad5979649be01607f11af1eb94aa9e8a3aaf77f0"}, - {file = "SQLAlchemy-2.0.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:798f717ae7c806d67145f6ae94dc7c342d3222d3b9a311a784f371a4333212c7"}, - {file = "SQLAlchemy-2.0.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fdd402169aa00df3142149940b3bf9ce7dde075928c1886d9a1df63d4b8de62"}, - {file = "SQLAlchemy-2.0.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0d3cab3076af2e4aa5693f89622bef7fa770c6fec967143e4da7508b3dceb9b9"}, - {file = "SQLAlchemy-2.0.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:74b080c897563f81062b74e44f5a72fa44c2b373741a9ade701d5f789a10ba23"}, - {file = "SQLAlchemy-2.0.25-cp37-cp37m-win32.whl", hash = "sha256:87d91043ea0dc65ee583026cb18e1b458d8ec5fc0a93637126b5fc0bc3ea68c4"}, - {file = "SQLAlchemy-2.0.25-cp37-cp37m-win_amd64.whl", hash = "sha256:75f99202324383d613ddd1f7455ac908dca9c2dd729ec8584c9541dd41822a2c"}, - {file = "SQLAlchemy-2.0.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:420362338681eec03f53467804541a854617faed7272fe71a1bfdb07336a381e"}, - {file = "SQLAlchemy-2.0.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c88f0c7dcc5f99bdb34b4fd9b69b93c89f893f454f40219fe923a3a2fd11625"}, - {file = "SQLAlchemy-2.0.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3be4987e3ee9d9a380b66393b77a4cd6d742480c951a1c56a23c335caca4ce3"}, - {file = "SQLAlchemy-2.0.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a159111a0f58fb034c93eeba211b4141137ec4b0a6e75789ab7a3ef3c7e7e3"}, - {file = "SQLAlchemy-2.0.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8b8cb63d3ea63b29074dcd29da4dc6a97ad1349151f2d2949495418fd6e48db9"}, - {file = "SQLAlchemy-2.0.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:736ea78cd06de6c21ecba7416499e7236a22374561493b456a1f7ffbe3f6cdb4"}, - {file = "SQLAlchemy-2.0.25-cp38-cp38-win32.whl", hash = "sha256:10331f129982a19df4284ceac6fe87353ca3ca6b4ca77ff7d697209ae0a5915e"}, - {file = "SQLAlchemy-2.0.25-cp38-cp38-win_amd64.whl", hash = "sha256:c55731c116806836a5d678a70c84cb13f2cedba920212ba7dcad53260997666d"}, - {file = "SQLAlchemy-2.0.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:605b6b059f4b57b277f75ace81cc5bc6335efcbcc4ccb9066695e515dbdb3900"}, - {file = "SQLAlchemy-2.0.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:665f0a3954635b5b777a55111ababf44b4fc12b1f3ba0a435b602b6387ffd7cf"}, - {file = "SQLAlchemy-2.0.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecf6d4cda1f9f6cb0b45803a01ea7f034e2f1aed9475e883410812d9f9e3cfcf"}, - {file = "SQLAlchemy-2.0.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c51db269513917394faec5e5c00d6f83829742ba62e2ac4fa5c98d58be91662f"}, - {file = "SQLAlchemy-2.0.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:790f533fa5c8901a62b6fef5811d48980adeb2f51f1290ade8b5e7ba990ba3de"}, - {file = "SQLAlchemy-2.0.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1b1180cda6df7af84fe72e4530f192231b1f29a7496951db4ff38dac1687202d"}, - {file = "SQLAlchemy-2.0.25-cp39-cp39-win32.whl", hash = "sha256:555651adbb503ac7f4cb35834c5e4ae0819aab2cd24857a123370764dc7d7e24"}, - {file = "SQLAlchemy-2.0.25-cp39-cp39-win_amd64.whl", hash = "sha256:dc55990143cbd853a5d038c05e79284baedf3e299661389654551bd02a6a68d7"}, - {file = "SQLAlchemy-2.0.25-py3-none-any.whl", hash = "sha256:a86b4240e67d4753dc3092d9511886795b3c2852abe599cffe108952f7af7ac3"}, - {file = "SQLAlchemy-2.0.25.tar.gz", hash = "sha256:a2c69a7664fb2d54b8682dd774c3b54f67f84fa123cf84dda2a5f40dcaa04e08"}, + {file = "sqlalchemy-2.0.51-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0"}, + {file = "sqlalchemy-2.0.51-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1af05726b3d0cdba1c55284bf408fd3b792e690fe2399bfb8304565551cda652"}, + {file = "sqlalchemy-2.0.51-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d"}, + {file = "sqlalchemy-2.0.51-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1e47b1199c2e832e325eacabc8d32d2487f58c9358f97e9a00f5eb93c5680d84"}, + {file = "sqlalchemy-2.0.51-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c68568f3facf8f66fa76c60e0ced69b67666ffa9941d1d0a3756fda196049080"}, + {file = "sqlalchemy-2.0.51-cp310-cp310-win32.whl", hash = "sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1"}, + {file = "sqlalchemy-2.0.51-cp310-cp310-win_amd64.whl", hash = "sha256:740cf6f35351b1ac3d82369152acf1d51d37e3dcf85d4dc0a22ca01410eabe2a"}, + {file = "sqlalchemy-2.0.51-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba"}, + {file = "sqlalchemy-2.0.51-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604"}, + {file = "sqlalchemy-2.0.51-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd"}, + {file = "sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260"}, + {file = "sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265"}, + {file = "sqlalchemy-2.0.51-cp311-cp311-win32.whl", hash = "sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86"}, + {file = "sqlalchemy-2.0.51-cp311-cp311-win_amd64.whl", hash = "sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc"}, + {file = "sqlalchemy-2.0.51-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a"}, + {file = "sqlalchemy-2.0.51-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e"}, + {file = "sqlalchemy-2.0.51-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9"}, + {file = "sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389"}, + {file = "sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d"}, + {file = "sqlalchemy-2.0.51-cp312-cp312-win32.whl", hash = "sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5"}, + {file = "sqlalchemy-2.0.51-cp312-cp312-win_amd64.whl", hash = "sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080"}, + {file = "sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07"}, + {file = "sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195"}, + {file = "sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f"}, + {file = "sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400"}, + {file = "sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d"}, + {file = "sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b"}, + {file = "sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5"}, + {file = "sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491"}, + {file = "sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d"}, + {file = "sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54"}, + {file = "sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e"}, + {file = "sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d"}, + {file = "sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8"}, + {file = "sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499"}, + {file = "sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de"}, + {file = "sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7"}, + {file = "sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72"}, + {file = "sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23"}, + {file = "sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522"}, + {file = "sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7"}, + {file = "sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2"}, + {file = "sqlalchemy-2.0.51-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bb1f5062f98b0b3290e72b707747fdd7e0f22d6956b236ba7ca7f5c9971d2da2"}, + {file = "sqlalchemy-2.0.51-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:247acaa29ccef6250dfd6a3eedf8f94ddf23564180a39fe362e32ae9dbdbde46"}, + {file = "sqlalchemy-2.0.51-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c95ef01f53233a305a874a44a63fbfb1d81cd79b49de0f8529b3548cde437e37"}, + {file = "sqlalchemy-2.0.51-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fa268106c8987639a17a18514cfe0cd9bf17420ab887e1e1bf486da8836135b1"}, + {file = "sqlalchemy-2.0.51-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b7f08588854bbb724041d9ae9d980d40040c922382e1d9a2ecb390edc4fd5032"}, + {file = "sqlalchemy-2.0.51-cp38-cp38-win32.whl", hash = "sha256:6b588fd681ddf0c196b8df1ea49a8913514894b2b8f945a9511b4b48871f99c8"}, + {file = "sqlalchemy-2.0.51-cp38-cp38-win_amd64.whl", hash = "sha256:ca216e8af5c05e326efc7e28716ac2381a7cf9791749f5ee1849dccdc99c9b00"}, + {file = "sqlalchemy-2.0.51-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa18ae738b5170e253ad0bb6c4b0f07585081e8a6e50893e4d911d47b39a0904"}, + {file = "sqlalchemy-2.0.51-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:59cab3686b1bc039dd9cded2f8d0c08a246e84e76bd4ab5b4f18c7cdae293825"}, + {file = "sqlalchemy-2.0.51-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:111604e637da87031255ddc26c7d7bc22bc6af6f5d459ccff3af1b4660233a85"}, + {file = "sqlalchemy-2.0.51-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ad30ae663711786303fbcd46a47516302d201ee49a877cb3fac61f672895110a"}, + {file = "sqlalchemy-2.0.51-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b21f0e7efc7a5c509e953784e9d1575ebb8b4318960e7e7d7a93bb803626cf64"}, + {file = "sqlalchemy-2.0.51-cp39-cp39-win32.whl", hash = "sha256:a42ad6afcbaaa777241e347aa2e29155993045a0d6b7db74da61053ffe875fe0"}, + {file = "sqlalchemy-2.0.51-cp39-cp39-win_amd64.whl", hash = "sha256:2a97eaad21c84b4ef8010b11eeba9fe6153eb0b3df3ff8b6abc309df1b978ef7"}, + {file = "sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5"}, + {file = "sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9"}, ] [package.dependencies] -greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} +greenlet = {version = ">=1", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} typing-extensions = ">=4.6.0" [package.extras] -aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] -aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] -aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] -asyncio = ["greenlet (!=0.4.17)"] -asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] -mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (>=1)"] +aioodbc = ["aioodbc", "greenlet (>=1)"] +aiosqlite = ["aiosqlite", "greenlet (>=1)", "typing_extensions (!=3.10.0.1)"] +asyncio = ["greenlet (>=1)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (>=1)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] mssql = ["pyodbc"] mssql-pymssql = ["pymssql"] mssql-pyodbc = ["pyodbc"] @@ -4316,7 +4818,7 @@ mysql-connector = ["mysql-connector-python"] oracle = ["cx_oracle (>=8)"] oracle-oracledb = ["oracledb (>=1.0.1)"] postgresql = ["psycopg2 (>=2.7)"] -postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-asyncpg = ["asyncpg", "greenlet (>=1)"] postgresql-pg8000 = ["pg8000 (>=1.29.1)"] postgresql-psycopg = ["psycopg (>=3.0.7)"] postgresql-psycopg2binary = ["psycopg2-binary"] @@ -4327,13 +4829,13 @@ sqlcipher = ["sqlcipher3_binary"] [[package]] name = "sqlalchemy-utils" -version = "0.41.1" +version = "0.41.2" description = "Various utility functions for SQLAlchemy." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "SQLAlchemy-Utils-0.41.1.tar.gz", hash = "sha256:a2181bff01eeb84479e38571d2c0718eb52042f9afd8c194d0d02877e84b7d74"}, - {file = "SQLAlchemy_Utils-0.41.1-py3-none-any.whl", hash = "sha256:6c96b0768ea3f15c0dc56b363d386138c562752b84f647fb8d31a2223aaab801"}, + {file = "SQLAlchemy-Utils-0.41.2.tar.gz", hash = "sha256:bc599c8c3b3319e53ce6c5c3c471120bd325d0071fb6f38a10e924e3d07b9990"}, + {file = "SQLAlchemy_Utils-0.41.2-py3-none-any.whl", hash = "sha256:85cf3842da2bf060760f955f8467b87983fb2e30f1764fd0e24a48307dc8ec6e"}, ] [package.dependencies] @@ -4348,27 +4850,27 @@ intervals = ["intervals (>=0.7.1)"] password = ["passlib (>=1.6,<2.0)"] pendulum = ["pendulum (>=2.0.5)"] phone = ["phonenumbers (>=5.9.2)"] -test = ["Jinja2 (>=2.3)", "Pygments (>=1.2)", "backports.zoneinfo", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "isort (>=4.2.2)", "pg8000 (>=1.12.4)", "psycopg (>=3.1.8)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (>=2.7.1)", "python-dateutil (>=2.6)", "pytz (>=2014.2)"] -test-all = ["Babel (>=1.3)", "Jinja2 (>=2.3)", "Pygments (>=1.2)", "arrow (>=0.3.4)", "backports.zoneinfo", "colour (>=0.0.4)", "cryptography (>=0.6)", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "furl (>=0.4.1)", "intervals (>=0.7.1)", "isort (>=4.2.2)", "passlib (>=1.6,<2.0)", "pendulum (>=2.0.5)", "pg8000 (>=1.12.4)", "phonenumbers (>=5.9.2)", "psycopg (>=3.1.8)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (>=2.7.1)", "python-dateutil", "python-dateutil (>=2.6)", "pytz (>=2014.2)"] +test = ["Jinja2 (>=2.3)", "Pygments (>=1.2)", "backports.zoneinfo", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "isort (>=4.2.2)", "pg8000 (>=1.12.4)", "psycopg (>=3.1.8)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (==7.4.4)", "python-dateutil (>=2.6)", "pytz (>=2014.2)"] +test-all = ["Babel (>=1.3)", "Jinja2 (>=2.3)", "Pygments (>=1.2)", "arrow (>=0.3.4)", "backports.zoneinfo", "colour (>=0.0.4)", "cryptography (>=0.6)", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "furl (>=0.4.1)", "intervals (>=0.7.1)", "isort (>=4.2.2)", "passlib (>=1.6,<2.0)", "pendulum (>=2.0.5)", "pg8000 (>=1.12.4)", "phonenumbers (>=5.9.2)", "psycopg (>=3.1.8)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (==7.4.4)", "python-dateutil", "python-dateutil (>=2.6)", "pytz (>=2014.2)"] timezone = ["python-dateutil"] url = ["furl (>=0.4.1)"] [[package]] name = "starlette" -version = "0.41.2" +version = "0.46.2" description = "The little ASGI library that shines." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "starlette-0.41.2-py3-none-any.whl", hash = "sha256:fbc189474b4731cf30fcef52f18a8d070e3f3b46c6a04c97579e85e6ffca942d"}, - {file = "starlette-0.41.2.tar.gz", hash = "sha256:9834fd799d1a87fd346deb76158668cfa0b0d56f85caefe8268e2d97c3468b62"}, + {file = "starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35"}, + {file = "starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5"}, ] [package.dependencies] -anyio = ">=3.4.0,<5" +anyio = ">=3.6.2,<5" [package.extras] -full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] +full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] [[package]] name = "strenum" @@ -4388,13 +4890,13 @@ test = ["pylint", "pytest", "pytest-black", "pytest-cov", "pytest-pylint"] [[package]] name = "tensorboardx" -version = "2.6.2.2" +version = "2.6.5" description = "TensorBoardX lets you watch Tensors Flow without Tensorflow" optional = false -python-versions = "*" +python-versions = ">=3.9" files = [ - {file = "tensorboardX-2.6.2.2-py2.py3-none-any.whl", hash = "sha256:160025acbf759ede23fd3526ae9d9bfbfd8b68eb16c38a010ebe326dc6395db8"}, - {file = "tensorboardX-2.6.2.2.tar.gz", hash = "sha256:c6476d7cd0d529b0b72f4acadb1269f9ed8b22f441e87a84f2a3b940bb87b666"}, + {file = "tensorboardx-2.6.5-py3-none-any.whl", hash = "sha256:c10b891d00af306537cb8b58a039b2ba41571f0da06f433a41c4ca8d6abe1373"}, + {file = "tensorboardx-2.6.5.tar.gz", hash = "sha256:ca176db3997ee8c07d2eb77381225956a3fd1c10c91beafab1f17069adc47017"}, ] [package.dependencies] @@ -4402,107 +4904,119 @@ numpy = "*" packaging = "*" protobuf = ">=3.20" -[[package]] -name = "tinydb" -version = "4.8.0" -description = "TinyDB is a tiny, document oriented database optimized for your happiness :)" -optional = false -python-versions = ">=3.7,<4.0" -files = [ - {file = "tinydb-4.8.0-py3-none-any.whl", hash = "sha256:30c06d12383d7c332e404ca6a6103fb2b32cbf25712689648c39d9a6bd34bd3d"}, - {file = "tinydb-4.8.0.tar.gz", hash = "sha256:6dd686a9c5a75dfa9280088fd79a419aefe19cd7f4bd85eba203540ef856d564"}, -] - [[package]] name = "tomli" -version = "2.0.1" +version = "2.4.1" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, + {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"}, + {file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"}, + {file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"}, + {file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"}, + {file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"}, + {file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"}, + {file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"}, + {file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"}, + {file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"}, + {file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"}, + {file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"}, + {file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"}, + {file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"}, + {file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"}, + {file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"}, + {file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"}, + {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, + {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, ] [[package]] name = "toolz" -version = "1.0.0" +version = "1.1.0" description = "List processing tools and functional utilities" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "toolz-1.0.0-py3-none-any.whl", hash = "sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236"}, - {file = "toolz-1.0.0.tar.gz", hash = "sha256:2c86e3d9a04798ac556793bced838816296a2f085017664e4995cb40a1047a02"}, + {file = "toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8"}, + {file = "toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b"}, ] [[package]] name = "tqdm" -version = "4.66.1" +version = "4.68.4" description = "Fast, Extensible Progress Meter" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"}, - {file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"}, + {file = "tqdm-4.68.4-py3-none-any.whl", hash = "sha256:5168118b2368f48c561afda8020fd79195b1bdb0bdf8086b88442c267a315dc2"}, + {file = "tqdm-4.68.4.tar.gz", hash = "sha256:19829c9673638f2a0b8617da4cdcb927e831cd88bcfcb6e78d42a4d1af131520"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] -dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] +discord = ["envwrap", "requests"] notebook = ["ipywidgets (>=6)"] -slack = ["slack-sdk"] -telegram = ["requests"] - -[[package]] -name = "tuspy" -version = "0.2.5" -description = "A Python client for the tus resumable upload protocol -> http://tus.io" -optional = false -python-versions = "*" -files = [ - {file = "tuspy-0.2.5-py3-none-any.whl", hash = "sha256:8ce106dd139ac77a097e8728f15f90d99df7408d9e67ac780ce44aefb9b5dd8d"}, - {file = "tuspy-0.2.5.tar.gz", hash = "sha256:1f238f65d444c0688f57e1dd704d9aa11cf5747eec33669e917627150170b9b8"}, -] - -[package.dependencies] -certifi = ">=2018.1.18" -future = ">=0.16.0" -requests = ">=2.18.4" -six = ">=1.11.0" -tinydb = ">=3.5.0" - -[package.extras] -dev = ["Sphinx (==1.7.1)", "sphinx-autobuild (==0.7.1)", "tox (>=2.3.1)"] -test = ["coverage (>=4.2)", "mock (>=2.0.0)", "pytest (>=3.0.3)", "pytest-cov (>=2.3.1,<2.6)", "responses (>=0.5.1)"] +slack = ["envwrap", "slack-sdk"] +telegram = ["envwrap", "requests"] [[package]] name = "typer" -version = "0.12.5" +version = "0.26.8" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" files = [ - {file = "typer-0.12.5-py3-none-any.whl", hash = "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b"}, - {file = "typer-0.12.5.tar.gz", hash = "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722"}, + {file = "typer-0.26.8-py3-none-any.whl", hash = "sha256:3512ca79ac5c11113414b36e80281b872884477722440691c89d1112e321a49c"}, + {file = "typer-0.26.8.tar.gz", hash = "sha256:c244a6bd558886fe3f8780efb6bdd28bb9aff005a94eedebaa5cb32926fe2f7e"}, ] [package.dependencies] -click = ">=8.0.0" -rich = ">=10.11.0" +annotated-doc = ">=0.0.2" +colorama = {version = "*", markers = "platform_system == \"Windows\""} +rich = ">=13.8.0" shellingham = ">=1.3.0" -typing-extensions = ">=3.7.4.3" [[package]] name = "types-requests" -version = "2.32.0.20241016" +version = "2.33.0.20260518" description = "Typing stubs for requests" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" files = [ - {file = "types-requests-2.32.0.20241016.tar.gz", hash = "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95"}, - {file = "types_requests-2.32.0.20241016-py3-none-any.whl", hash = "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747"}, + {file = "types_requests-2.33.0.20260518-py3-none-any.whl", hash = "sha256:626d697d1adaaff76e2044dc8c5c051d8f21abc157bdfe204a75558076fe0bf0"}, + {file = "types_requests-2.33.0.20260518.tar.gz", hash = "sha256:df7bd3bfe0ca8402dfb841e7d9be714bb5578203283d66d7dc4ef69343449a5e"}, ] [package.dependencies] @@ -4510,146 +5024,158 @@ urllib3 = ">=2" [[package]] name = "typing-extensions" -version = "4.9.0" -description = "Backported and Experimental Type Hints for Python 3.8+" +version = "4.16.0" +description = "Backported and Experimental Type Hints for Python 3.9+" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, - {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, + {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, + {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, ] [[package]] name = "tzdata" -version = "2023.4" +version = "2026.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" files = [ - {file = "tzdata-2023.4-py2.py3-none-any.whl", hash = "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3"}, - {file = "tzdata-2023.4.tar.gz", hash = "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9"}, + {file = "tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7"}, + {file = "tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10"}, ] [[package]] name = "tzlocal" -version = "5.2" +version = "5.4.4" description = "tzinfo object for the local timezone" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" files = [ - {file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"}, - {file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"}, + {file = "tzlocal-5.4.4-py3-none-any.whl", hash = "sha256:aae09f0126a8a86fa736be266eb4a471380d26a0de3bc14844e7821fee3e2a15"}, + {file = "tzlocal-5.4.4.tar.gz", hash = "sha256:8dbb8660838688a7b6ba4fed31d18dedf842afb4d47ca050d6d891c2c15f3be4"}, ] [package.dependencies] tzdata = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] -devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] +devenv = ["zest.releaser"] +testing = ["check_manifest", "pyroma", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "ruff"] [[package]] name = "urllib3" -version = "2.0.7" +version = "2.7.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" files = [ - {file = "urllib3-2.0.7-py3-none-any.whl", hash = "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"}, - {file = "urllib3-2.0.7.tar.gz", hash = "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84"}, + {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, + {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"] +brotli = ["brotli (>=1.2.0)", "brotlicffi (>=1.2.0.0)"] +h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] +zstd = ["backports-zstd (>=1.0.0)"] [[package]] name = "uvicorn" -version = "0.32.1" +version = "0.51.0" description = "The lightning-fast ASGI server." optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" files = [ - {file = "uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e"}, - {file = "uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175"}, + {file = "uvicorn-0.51.0-py3-none-any.whl", hash = "sha256:5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b"}, + {file = "uvicorn-0.51.0.tar.gz", hash = "sha256:f6f4b69b657c312f516dd2d268ab9ae6f254b11e4bac504f37b2ab58b24dd0b0"}, ] [package.dependencies] click = ">=7.0" -colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} h11 = ">=0.8" -httptools = {version = ">=0.6.3", optional = true, markers = "extra == \"standard\""} +httptools = {version = ">=0.8.0", optional = true, markers = "extra == \"standard\""} python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} -uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""} -watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} -websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} +uvloop = {version = ">=0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""} +watchfiles = {version = ">=0.20", optional = true, markers = "extra == \"standard\""} +websockets = {version = ">=13.0", optional = true, markers = "extra == \"standard\""} [package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +standard = ["httptools (>=0.8.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1)", "watchfiles (>=0.20)", "websockets (>=13.0)"] [[package]] name = "uvloop" -version = "0.21.0" +version = "0.22.1" description = "Fast implementation of asyncio event loop on top of libuv" optional = false -python-versions = ">=3.8.0" -files = [ - {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f"}, - {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d"}, - {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26"}, - {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb"}, - {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f"}, - {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c"}, - {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8"}, - {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0"}, - {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e"}, - {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb"}, - {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6"}, - {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d"}, - {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c"}, - {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2"}, - {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d"}, - {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc"}, - {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb"}, - {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f"}, - {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281"}, - {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af"}, - {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6"}, - {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816"}, - {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc"}, - {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553"}, - {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414"}, - {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206"}, - {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe"}, - {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79"}, - {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a"}, - {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc"}, - {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b"}, - {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2"}, - {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0"}, - {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75"}, - {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd"}, - {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff"}, - {file = "uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3"}, +python-versions = ">=3.8.1" +files = [ + {file = "uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c"}, + {file = "uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792"}, + {file = "uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86"}, + {file = "uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd"}, + {file = "uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2"}, + {file = "uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec"}, + {file = "uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9"}, + {file = "uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77"}, + {file = "uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21"}, + {file = "uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702"}, + {file = "uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733"}, + {file = "uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473"}, + {file = "uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42"}, + {file = "uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6"}, + {file = "uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370"}, + {file = "uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4"}, + {file = "uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2"}, + {file = "uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0"}, + {file = "uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705"}, + {file = "uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8"}, + {file = "uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d"}, + {file = "uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e"}, + {file = "uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e"}, + {file = "uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad"}, + {file = "uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142"}, + {file = "uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74"}, + {file = "uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35"}, + {file = "uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25"}, + {file = "uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6"}, + {file = "uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079"}, + {file = "uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289"}, + {file = "uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3"}, + {file = "uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c"}, + {file = "uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21"}, + {file = "uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88"}, + {file = "uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e"}, + {file = "uvloop-0.22.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:80eee091fe128e425177fbd82f8635769e2f32ec9daf6468286ec57ec0313efa"}, + {file = "uvloop-0.22.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:017bd46f9e7b78e81606329d07141d3da446f8798c6baeec124260e22c262772"}, + {file = "uvloop-0.22.1-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3e5c6727a57cb6558592a95019e504f605d1c54eb86463ee9f7a2dbd411c820"}, + {file = "uvloop-0.22.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:57df59d8b48feb0e613d9b1f5e57b7532e97cbaf0d61f7aa9aa32221e84bc4b6"}, + {file = "uvloop-0.22.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:55502bc2c653ed2e9692e8c55cb95b397d33f9f2911e929dc97c4d6b26d04242"}, + {file = "uvloop-0.22.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4a968a72422a097b09042d5fa2c5c590251ad484acf910a651b4b620acd7f193"}, + {file = "uvloop-0.22.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b45649628d816c030dba3c80f8e2689bab1c89518ed10d426036cdc47874dfc4"}, + {file = "uvloop-0.22.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ea721dd3203b809039fcc2983f14608dae82b212288b346e0bfe46ec2fab0b7c"}, + {file = "uvloop-0.22.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ae676de143db2b2f60a9696d7eca5bb9d0dd6cc3ac3dad59a8ae7e95f9e1b54"}, + {file = "uvloop-0.22.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17d4e97258b0172dfa107b89aa1eeba3016f4b1974ce85ca3ef6a66b35cbf659"}, + {file = "uvloop-0.22.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:05e4b5f86e621cf3927631789999e697e58f0d2d32675b67d9ca9eb0bca55743"}, + {file = "uvloop-0.22.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:286322a90bea1f9422a470d5d2ad82d38080be0a29c4dd9b3e6384320a4d11e7"}, + {file = "uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f"}, ] [package.extras] dev = ["Cython (>=3.0,<4.0)", "setuptools (>=60)"] -docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] -test = ["aiohttp (>=3.10.5)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx_rtd_theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["aiohttp (>=3.10.5)", "flake8 (>=6.1,<7.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=25.3.0,<25.4.0)", "pycodestyle (>=2.11.0,<2.12.0)"] [[package]] name = "validators" -version = "0.34.0" +version = "0.35.0" description = "Python Data Validation for Humans™" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "validators-0.34.0-py3-none-any.whl", hash = "sha256:c804b476e3e6d3786fa07a30073a4ef694e617805eb1946ceee3fe5a9b8b1321"}, - {file = "validators-0.34.0.tar.gz", hash = "sha256:647fe407b45af9a74d245b943b18e6a816acf4926974278f6dd617778e1e781f"}, + {file = "validators-0.35.0-py3-none-any.whl", hash = "sha256:e8c947097eae7892cb3d26868d637f79f47b4a0554bc6b80065dfe5aac3705dd"}, + {file = "validators-0.35.0.tar.gz", hash = "sha256:992d6c48a4e77c81f1b4daba10d16c3a9bb0dbb79b3a19ea847ff0928e70497a"}, ] [package.extras] @@ -4657,114 +5183,136 @@ crypto-eth-addresses = ["eth-hash[pycryptodome] (>=0.7.0)"] [[package]] name = "virtualenv" -version = "20.25.0" +version = "21.6.0" description = "Virtual Python Environment builder" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, - {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, + {file = "virtualenv-21.6.0-py3-none-any.whl", hash = "sha256:bce9d097950fef9d81129b333babfb7767072850c2f1acce0ec536708401bfd1"}, + {file = "virtualenv-21.6.0.tar.gz", hash = "sha256:e18a4d750f2b64dea73e72ffde3922f3c52365fabdc8157ebd3da20d031c4734"}, ] [package.dependencies] distlib = ">=0.3.7,<1" -filelock = ">=3.12.2,<4" +filelock = {version = ">=3.24.2,<4", markers = "python_version >= \"3.10\""} platformdirs = ">=3.9.1,<5" - -[package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +python-discovery = ">=1.4.2" +typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} [[package]] name = "watchfiles" -version = "0.24.0" +version = "1.2.0" description = "Simple, modern and high performance file watching and code reload in python." optional = false -python-versions = ">=3.8" -files = [ - {file = "watchfiles-0.24.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:083dc77dbdeef09fa44bb0f4d1df571d2e12d8a8f985dccde71ac3ac9ac067a0"}, - {file = "watchfiles-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e94e98c7cb94cfa6e071d401ea3342767f28eb5a06a58fafdc0d2a4974f4f35c"}, - {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82ae557a8c037c42a6ef26c494d0631cacca040934b101d001100ed93d43f361"}, - {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:acbfa31e315a8f14fe33e3542cbcafc55703b8f5dcbb7c1eecd30f141df50db3"}, - {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b74fdffce9dfcf2dc296dec8743e5b0332d15df19ae464f0e249aa871fc1c571"}, - {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:449f43f49c8ddca87c6b3980c9284cab6bd1f5c9d9a2b00012adaaccd5e7decd"}, - {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4abf4ad269856618f82dee296ac66b0cd1d71450fc3c98532d93798e73399b7a"}, - {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f895d785eb6164678ff4bb5cc60c5996b3ee6df3edb28dcdeba86a13ea0465e"}, - {file = "watchfiles-0.24.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7ae3e208b31be8ce7f4c2c0034f33406dd24fbce3467f77223d10cd86778471c"}, - {file = "watchfiles-0.24.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2efec17819b0046dde35d13fb8ac7a3ad877af41ae4640f4109d9154ed30a188"}, - {file = "watchfiles-0.24.0-cp310-none-win32.whl", hash = "sha256:6bdcfa3cd6fdbdd1a068a52820f46a815401cbc2cb187dd006cb076675e7b735"}, - {file = "watchfiles-0.24.0-cp310-none-win_amd64.whl", hash = "sha256:54ca90a9ae6597ae6dc00e7ed0a040ef723f84ec517d3e7ce13e63e4bc82fa04"}, - {file = "watchfiles-0.24.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:bdcd5538e27f188dd3c804b4a8d5f52a7fc7f87e7fd6b374b8e36a4ca03db428"}, - {file = "watchfiles-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2dadf8a8014fde6addfd3c379e6ed1a981c8f0a48292d662e27cabfe4239c83c"}, - {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6509ed3f467b79d95fc62a98229f79b1a60d1b93f101e1c61d10c95a46a84f43"}, - {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8360f7314a070c30e4c976b183d1d8d1585a4a50c5cb603f431cebcbb4f66327"}, - {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:316449aefacf40147a9efaf3bd7c9bdd35aaba9ac5d708bd1eb5763c9a02bef5"}, - {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73bde715f940bea845a95247ea3e5eb17769ba1010efdc938ffcb967c634fa61"}, - {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3770e260b18e7f4e576edca4c0a639f704088602e0bc921c5c2e721e3acb8d15"}, - {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa0fd7248cf533c259e59dc593a60973a73e881162b1a2f73360547132742823"}, - {file = "watchfiles-0.24.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d7a2e3b7f5703ffbd500dabdefcbc9eafeff4b9444bbdd5d83d79eedf8428fab"}, - {file = "watchfiles-0.24.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d831ee0a50946d24a53821819b2327d5751b0c938b12c0653ea5be7dea9c82ec"}, - {file = "watchfiles-0.24.0-cp311-none-win32.whl", hash = "sha256:49d617df841a63b4445790a254013aea2120357ccacbed00253f9c2b5dc24e2d"}, - {file = "watchfiles-0.24.0-cp311-none-win_amd64.whl", hash = "sha256:d3dcb774e3568477275cc76554b5a565024b8ba3a0322f77c246bc7111c5bb9c"}, - {file = "watchfiles-0.24.0-cp311-none-win_arm64.whl", hash = "sha256:9301c689051a4857d5b10777da23fafb8e8e921bcf3abe6448a058d27fb67633"}, - {file = "watchfiles-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7211b463695d1e995ca3feb38b69227e46dbd03947172585ecb0588f19b0d87a"}, - {file = "watchfiles-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b8693502d1967b00f2fb82fc1e744df128ba22f530e15b763c8d82baee15370"}, - {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdab9555053399318b953a1fe1f586e945bc8d635ce9d05e617fd9fe3a4687d6"}, - {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34e19e56d68b0dad5cff62273107cf5d9fbaf9d75c46277aa5d803b3ef8a9e9b"}, - {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:41face41f036fee09eba33a5b53a73e9a43d5cb2c53dad8e61fa6c9f91b5a51e"}, - {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5148c2f1ea043db13ce9b0c28456e18ecc8f14f41325aa624314095b6aa2e9ea"}, - {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e4bd963a935aaf40b625c2499f3f4f6bbd0c3776f6d3bc7c853d04824ff1c9f"}, - {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c79d7719d027b7a42817c5d96461a99b6a49979c143839fc37aa5748c322f234"}, - {file = "watchfiles-0.24.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:32aa53a9a63b7f01ed32e316e354e81e9da0e6267435c7243bf8ae0f10b428ef"}, - {file = "watchfiles-0.24.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ce72dba6a20e39a0c628258b5c308779b8697f7676c254a845715e2a1039b968"}, - {file = "watchfiles-0.24.0-cp312-none-win32.whl", hash = "sha256:d9018153cf57fc302a2a34cb7564870b859ed9a732d16b41a9b5cb2ebed2d444"}, - {file = "watchfiles-0.24.0-cp312-none-win_amd64.whl", hash = "sha256:551ec3ee2a3ac9cbcf48a4ec76e42c2ef938a7e905a35b42a1267fa4b1645896"}, - {file = "watchfiles-0.24.0-cp312-none-win_arm64.whl", hash = "sha256:b52a65e4ea43c6d149c5f8ddb0bef8d4a1e779b77591a458a893eb416624a418"}, - {file = "watchfiles-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2e3ab79a1771c530233cadfd277fcc762656d50836c77abb2e5e72b88e3a48"}, - {file = "watchfiles-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327763da824817b38ad125dcd97595f942d720d32d879f6c4ddf843e3da3fe90"}, - {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd82010f8ab451dabe36054a1622870166a67cf3fce894f68895db6f74bbdc94"}, - {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d64ba08db72e5dfd5c33be1e1e687d5e4fcce09219e8aee893a4862034081d4e"}, - {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1cf1f6dd7825053f3d98f6d33f6464ebdd9ee95acd74ba2c34e183086900a827"}, - {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43e3e37c15a8b6fe00c1bce2473cfa8eb3484bbeecf3aefbf259227e487a03df"}, - {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88bcd4d0fe1d8ff43675360a72def210ebad3f3f72cabfeac08d825d2639b4ab"}, - {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:999928c6434372fde16c8f27143d3e97201160b48a614071261701615a2a156f"}, - {file = "watchfiles-0.24.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:30bbd525c3262fd9f4b1865cb8d88e21161366561cd7c9e1194819e0a33ea86b"}, - {file = "watchfiles-0.24.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:edf71b01dec9f766fb285b73930f95f730bb0943500ba0566ae234b5c1618c18"}, - {file = "watchfiles-0.24.0-cp313-none-win32.whl", hash = "sha256:f4c96283fca3ee09fb044f02156d9570d156698bc3734252175a38f0e8975f07"}, - {file = "watchfiles-0.24.0-cp313-none-win_amd64.whl", hash = "sha256:a974231b4fdd1bb7f62064a0565a6b107d27d21d9acb50c484d2cdba515b9366"}, - {file = "watchfiles-0.24.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:ee82c98bed9d97cd2f53bdb035e619309a098ea53ce525833e26b93f673bc318"}, - {file = "watchfiles-0.24.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fd92bbaa2ecdb7864b7600dcdb6f2f1db6e0346ed425fbd01085be04c63f0b05"}, - {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f83df90191d67af5a831da3a33dd7628b02a95450e168785586ed51e6d28943c"}, - {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fca9433a45f18b7c779d2bae7beeec4f740d28b788b117a48368d95a3233ed83"}, - {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b995bfa6bf01a9e09b884077a6d37070464b529d8682d7691c2d3b540d357a0c"}, - {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed9aba6e01ff6f2e8285e5aa4154e2970068fe0fc0998c4380d0e6278222269b"}, - {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5171ef898299c657685306d8e1478a45e9303ddcd8ac5fed5bd52ad4ae0b69b"}, - {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4933a508d2f78099162da473841c652ad0de892719043d3f07cc83b33dfd9d91"}, - {file = "watchfiles-0.24.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95cf3b95ea665ab03f5a54765fa41abf0529dbaf372c3b83d91ad2cfa695779b"}, - {file = "watchfiles-0.24.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:01def80eb62bd5db99a798d5e1f5f940ca0a05986dcfae21d833af7a46f7ee22"}, - {file = "watchfiles-0.24.0-cp38-none-win32.whl", hash = "sha256:4d28cea3c976499475f5b7a2fec6b3a36208656963c1a856d328aeae056fc5c1"}, - {file = "watchfiles-0.24.0-cp38-none-win_amd64.whl", hash = "sha256:21ab23fdc1208086d99ad3f69c231ba265628014d4aed31d4e8746bd59e88cd1"}, - {file = "watchfiles-0.24.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b665caeeda58625c3946ad7308fbd88a086ee51ccb706307e5b1fa91556ac886"}, - {file = "watchfiles-0.24.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5c51749f3e4e269231510da426ce4a44beb98db2dce9097225c338f815b05d4f"}, - {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b2509f08761f29a0fdad35f7e1638b8ab1adfa2666d41b794090361fb8b855"}, - {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a60e2bf9dc6afe7f743e7c9b149d1fdd6dbf35153c78fe3a14ae1a9aee3d98b"}, - {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f7d9b87c4c55e3ea8881dfcbf6d61ea6775fffed1fedffaa60bd047d3c08c430"}, - {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:78470906a6be5199524641f538bd2c56bb809cd4bf29a566a75051610bc982c3"}, - {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07cdef0c84c03375f4e24642ef8d8178e533596b229d32d2bbd69e5128ede02a"}, - {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d337193bbf3e45171c8025e291530fb7548a93c45253897cd764a6a71c937ed9"}, - {file = "watchfiles-0.24.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ec39698c45b11d9694a1b635a70946a5bad066b593af863460a8e600f0dff1ca"}, - {file = "watchfiles-0.24.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e28d91ef48eab0afb939fa446d8ebe77e2f7593f5f463fd2bb2b14132f95b6e"}, - {file = "watchfiles-0.24.0-cp39-none-win32.whl", hash = "sha256:7138eff8baa883aeaa074359daabb8b6c1e73ffe69d5accdc907d62e50b1c0da"}, - {file = "watchfiles-0.24.0-cp39-none-win_amd64.whl", hash = "sha256:b3ef2c69c655db63deb96b3c3e587084612f9b1fa983df5e0c3379d41307467f"}, - {file = "watchfiles-0.24.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:632676574429bee8c26be8af52af20e0c718cc7f5f67f3fb658c71928ccd4f7f"}, - {file = "watchfiles-0.24.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a2a9891723a735d3e2540651184be6fd5b96880c08ffe1a98bae5017e65b544b"}, - {file = "watchfiles-0.24.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7fa2bc0efef3e209a8199fd111b8969fe9db9c711acc46636686331eda7dd4"}, - {file = "watchfiles-0.24.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01550ccf1d0aed6ea375ef259706af76ad009ef5b0203a3a4cce0f6024f9b68a"}, - {file = "watchfiles-0.24.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:96619302d4374de5e2345b2b622dc481257a99431277662c30f606f3e22f42be"}, - {file = "watchfiles-0.24.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:85d5f0c7771dcc7a26c7a27145059b6bb0ce06e4e751ed76cdf123d7039b60b5"}, - {file = "watchfiles-0.24.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951088d12d339690a92cef2ec5d3cfd957692834c72ffd570ea76a6790222777"}, - {file = "watchfiles-0.24.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49fb58bcaa343fedc6a9e91f90195b20ccb3135447dc9e4e2570c3a39565853e"}, - {file = "watchfiles-0.24.0.tar.gz", hash = "sha256:afb72325b74fa7a428c009c1b8be4b4d7c2afedafb2982827ef2156646df2fe1"}, +python-versions = ">=3.10" +files = [ + {file = "watchfiles-1.2.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:bb68bf4df85abebe5efddc53cf2075520f243a59868d9b3973278b23e76962a9"}, + {file = "watchfiles-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c16cb06dd17d43b9d185094268459eac92c9538356f050e55b54e82cf700e1d4"}, + {file = "watchfiles-1.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a0feab9af4c021c581f695258c642b3d10c5fd4c676e33a0d8606425d82631"}, + {file = "watchfiles-1.2.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a16ffe19bf5cf9f5edaa1ad1dd830c5a816e8feec430c522302ab55483a4b994"}, + {file = "watchfiles-1.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204f299afcbd65918ab78dbc52626b0ae45e9d8cef403fdbf33ecf9e40eac66e"}, + {file = "watchfiles-1.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11743adfa510bfffebe97659fb280182b5c9b238708f667e866f308c3430dc19"}, + {file = "watchfiles-1.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb72919d93e3a16fc451d3aa3d4b1698423daca1b382d3d959c9ac51297c12a8"}, + {file = "watchfiles-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62f042afde2dde21ec1d2c1a74361e804673df86f51e418a999c9acfe671b07"}, + {file = "watchfiles-1.2.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:027ae72bfdfd254862065d8b3e2a815c6ab9b1853ce41e6648ece84afd34a551"}, + {file = "watchfiles-1.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e1cfd51e97e13ff3bd047c140764d277fc9b95b7cb5da59e46a47d167adab310"}, + {file = "watchfiles-1.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:24b2405c0a46738dd9e1cf7135aa5dbdb9d42d024628651b3b13d5117e99f8df"}, + {file = "watchfiles-1.2.0-cp310-cp310-win32.whl", hash = "sha256:8c520725602756229f045b032a1ff33d7ef0f7404189d62f6c2438cb6d8ef6a1"}, + {file = "watchfiles-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:03b14855c6f35539e2d95c442ae9530a75762f1e26567152b9ed05f96534a74d"}, + {file = "watchfiles-1.2.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201"}, + {file = "watchfiles-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5"}, + {file = "watchfiles-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a"}, + {file = "watchfiles-1.2.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1"}, + {file = "watchfiles-1.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717"}, + {file = "watchfiles-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b"}, + {file = "watchfiles-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5"}, + {file = "watchfiles-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e"}, + {file = "watchfiles-1.2.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165"}, + {file = "watchfiles-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6"}, + {file = "watchfiles-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5"}, + {file = "watchfiles-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8"}, + {file = "watchfiles-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22"}, + {file = "watchfiles-1.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7"}, + {file = "watchfiles-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26"}, + {file = "watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c"}, + {file = "watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc"}, + {file = "watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0"}, + {file = "watchfiles-1.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c"}, + {file = "watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01"}, + {file = "watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8"}, + {file = "watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5"}, + {file = "watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d"}, + {file = "watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c"}, + {file = "watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906"}, + {file = "watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898"}, + {file = "watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379"}, + {file = "watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5"}, + {file = "watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98"}, + {file = "watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44"}, + {file = "watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658"}, + {file = "watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb"}, + {file = "watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f"}, + {file = "watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0"}, + {file = "watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5"}, + {file = "watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71"}, + {file = "watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3"}, + {file = "watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0"}, + {file = "watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427"}, + {file = "watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799"}, + {file = "watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9"}, + {file = "watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077"}, + {file = "watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08"}, + {file = "watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9"}, + {file = "watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4"}, + {file = "watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55"}, + {file = "watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925"}, + {file = "watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4"}, + {file = "watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2"}, + {file = "watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9"}, + {file = "watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa"}, + {file = "watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44"}, + {file = "watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72"}, + {file = "watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4"}, + {file = "watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281"}, + {file = "watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d"}, + {file = "watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e"}, + {file = "watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242"}, + {file = "watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add"}, + {file = "watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f"}, + {file = "watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7"}, + {file = "watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e"}, + {file = "watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06"}, + {file = "watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba"}, + {file = "watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7"}, + {file = "watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103"}, + {file = "watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3"}, + {file = "watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2"}, + {file = "watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28"}, + {file = "watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831"}, + {file = "watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33"}, + {file = "watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4"}, + {file = "watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b"}, + {file = "watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666"}, + {file = "watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925"}, + {file = "watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b"}, + {file = "watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30"}, + {file = "watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5"}, + {file = "watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374"}, + {file = "watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65"}, + {file = "watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69"}, + {file = "watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579"}, + {file = "watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7"}, + {file = "watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2"}, + {file = "watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6"}, + {file = "watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4"}, + {file = "watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488"}, + {file = "watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb"}, + {file = "watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377"}, + {file = "watchfiles-1.2.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2"}, + {file = "watchfiles-1.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db"}, + {file = "watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7"}, + {file = "watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0"}, + {file = "watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838"}, ] [package.dependencies] @@ -4772,19 +5320,19 @@ anyio = ">=3.0.0" [[package]] name = "web3" -version = "7.8.0" +version = "7.16.0" description = "web3: A Python library for interacting with Ethereum" optional = false python-versions = "<4,>=3.8" files = [ - {file = "web3-7.8.0-py3-none-any.whl", hash = "sha256:c8771b3d8772f7104a0462804449beb57d36cef7bd8b411140f95a92fc46b559"}, - {file = "web3-7.8.0.tar.gz", hash = "sha256:712bc9fd6b1ef6e467ee24c25b581e1951cab2cba17f9f548f12587734f2c857"}, + {file = "web3-7.16.0-py3-none-any.whl", hash = "sha256:760b2718c473980d70708c3593d9d28395db4b482f45e38a63a36fa028178f51"}, + {file = "web3-7.16.0.tar.gz", hash = "sha256:b4a75a3fa94fef4d23d502eb3c2244146ef9a1ee0082cf1cb0a91586ba0510c3"}, ] [package.dependencies] aiohttp = ">=3.7.4.post0" eth-abi = ">=5.0.1" -eth-account = ">=0.13.1" +eth-account = ">=0.13.6" eth-hash = {version = ">=0.5.1", extras = ["pycryptodome"]} eth-typing = ">=5.0.0" eth-utils = ">=5.0.0" @@ -4795,93 +5343,90 @@ pywin32 = {version = ">=223", markers = "platform_system == \"Windows\""} requests = ">=2.23.0" types-requests = ">=2.0.0" typing-extensions = ">=4.0.1" -websockets = ">=10.0.0,<14.0.0" +websockets = ">=10.0.0,<16.0.0" [package.extras] -dev = ["build (>=0.9.0)", "bumpversion (>=0.5.3)", "eth-tester[py-evm] (>=0.12.0b1,<0.13.0b1)", "flaky (>=3.7.0)", "hypothesis (>=3.31.2)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "py-geth (>=5.1.0)", "pytest (>=7.0.0)", "pytest-asyncio (>=0.18.1,<0.23)", "pytest-mock (>=1.10)", "pytest-xdist (>=2.4.0)", "setuptools (>=38.6.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=21,<22)", "tox (>=4.0.0)", "tqdm (>4.32)", "twine (>=1.13)", "wheel"] -docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=21,<22)"] -test = ["eth-tester[py-evm] (>=0.12.0b1,<0.13.0b1)", "flaky (>=3.7.0)", "hypothesis (>=3.31.2)", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "py-geth (>=5.1.0)", "pytest (>=7.0.0)", "pytest-asyncio (>=0.18.1,<0.23)", "pytest-mock (>=1.10)", "pytest-xdist (>=2.4.0)", "tox (>=4.0.0)"] -tester = ["eth-tester[py-evm] (>=0.12.0b1,<0.13.0b1)", "py-geth (>=5.1.0)"] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "eth-tester[py-evm] (>=0.13.0b1,<0.14.0b1)", "flaky (>=3.7.0)", "hypothesis (>=3.31.2)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "py-geth (>=6.4.0)", "pytest (>=7.0.0)", "pytest-asyncio (>=0.18.1,<0.23)", "pytest-mock (>=1.10)", "pytest-xdist (>=2.4.0)", "setuptools (>=38.6.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "tqdm (>4.32)", "twine (>=1.13)", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +test = ["eth-tester[py-evm] (>=0.13.0b1,<0.14.0b1)", "flaky (>=3.7.0)", "hypothesis (>=3.31.2)", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "py-geth (>=6.4.0)", "pytest (>=7.0.0)", "pytest-asyncio (>=0.18.1,<0.23)", "pytest-mock (>=1.10)", "pytest-xdist (>=2.4.0)", "tox (>=4.0.0)"] +tester = ["eth-tester[py-evm] (>=0.13.0b1,<0.14.0b1)", "py-geth (>=6.4.0)"] [[package]] name = "websockets" -version = "12.0" +version = "15.0.1" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374"}, - {file = "websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be"}, - {file = "websockets-12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547"}, - {file = "websockets-12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2"}, - {file = "websockets-12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558"}, - {file = "websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480"}, - {file = "websockets-12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c"}, - {file = "websockets-12.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8"}, - {file = "websockets-12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603"}, - {file = "websockets-12.0-cp310-cp310-win32.whl", hash = "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f"}, - {file = "websockets-12.0-cp310-cp310-win_amd64.whl", hash = "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf"}, - {file = "websockets-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4"}, - {file = "websockets-12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f"}, - {file = "websockets-12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3"}, - {file = "websockets-12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c"}, - {file = "websockets-12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45"}, - {file = "websockets-12.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04"}, - {file = "websockets-12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447"}, - {file = "websockets-12.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca"}, - {file = "websockets-12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53"}, - {file = "websockets-12.0-cp311-cp311-win32.whl", hash = "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402"}, - {file = "websockets-12.0-cp311-cp311-win_amd64.whl", hash = "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b"}, - {file = "websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df"}, - {file = "websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc"}, - {file = "websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b"}, - {file = "websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb"}, - {file = "websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92"}, - {file = "websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed"}, - {file = "websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5"}, - {file = "websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2"}, - {file = "websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113"}, - {file = "websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d"}, - {file = "websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f"}, - {file = "websockets-12.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438"}, - {file = "websockets-12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2"}, - {file = "websockets-12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d"}, - {file = "websockets-12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137"}, - {file = "websockets-12.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205"}, - {file = "websockets-12.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def"}, - {file = "websockets-12.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8"}, - {file = "websockets-12.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967"}, - {file = "websockets-12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7"}, - {file = "websockets-12.0-cp38-cp38-win32.whl", hash = "sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62"}, - {file = "websockets-12.0-cp38-cp38-win_amd64.whl", hash = "sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892"}, - {file = "websockets-12.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d"}, - {file = "websockets-12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28"}, - {file = "websockets-12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53"}, - {file = "websockets-12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c"}, - {file = "websockets-12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec"}, - {file = "websockets-12.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9"}, - {file = "websockets-12.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae"}, - {file = "websockets-12.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b"}, - {file = "websockets-12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9"}, - {file = "websockets-12.0-cp39-cp39-win32.whl", hash = "sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6"}, - {file = "websockets-12.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8"}, - {file = "websockets-12.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd"}, - {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870"}, - {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077"}, - {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b"}, - {file = "websockets-12.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30"}, - {file = "websockets-12.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6"}, - {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123"}, - {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931"}, - {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2"}, - {file = "websockets-12.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468"}, - {file = "websockets-12.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b"}, - {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399"}, - {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7"}, - {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611"}, - {file = "websockets-12.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370"}, - {file = "websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e"}, - {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c"}, + {file = "websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256"}, + {file = "websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf"}, + {file = "websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85"}, + {file = "websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"}, + {file = "websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"}, + {file = "websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"}, + {file = "websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"}, + {file = "websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880"}, + {file = "websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411"}, + {file = "websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123"}, + {file = "websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f"}, + {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, ] [[package]] @@ -4897,101 +5442,123 @@ files = [ [[package]] name = "yarl" -version = "1.18.3" +version = "1.24.2" description = "Yet another URL library" optional = false -python-versions = ">=3.9" -files = [ - {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34"}, - {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7"}, - {file = "yarl-1.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed"}, - {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde"}, - {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b"}, - {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5"}, - {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc"}, - {file = "yarl-1.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd"}, - {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990"}, - {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db"}, - {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62"}, - {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760"}, - {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b"}, - {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690"}, - {file = "yarl-1.18.3-cp310-cp310-win32.whl", hash = "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6"}, - {file = "yarl-1.18.3-cp310-cp310-win_amd64.whl", hash = "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8"}, - {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069"}, - {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193"}, - {file = "yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889"}, - {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8"}, - {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca"}, - {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8"}, - {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae"}, - {file = "yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3"}, - {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb"}, - {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e"}, - {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59"}, - {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d"}, - {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e"}, - {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a"}, - {file = "yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1"}, - {file = "yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5"}, - {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50"}, - {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576"}, - {file = "yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640"}, - {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2"}, - {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75"}, - {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512"}, - {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba"}, - {file = "yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb"}, - {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272"}, - {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6"}, - {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e"}, - {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb"}, - {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393"}, - {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285"}, - {file = "yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2"}, - {file = "yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477"}, - {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb"}, - {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa"}, - {file = "yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782"}, - {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0"}, - {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482"}, - {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186"}, - {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58"}, - {file = "yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53"}, - {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2"}, - {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8"}, - {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1"}, - {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a"}, - {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10"}, - {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8"}, - {file = "yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d"}, - {file = "yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c"}, - {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:61e5e68cb65ac8f547f6b5ef933f510134a6bf31bb178be428994b0cb46c2a04"}, - {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe57328fbc1bfd0bd0514470ac692630f3901c0ee39052ae47acd1d90a436719"}, - {file = "yarl-1.18.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a440a2a624683108a1b454705ecd7afc1c3438a08e890a1513d468671d90a04e"}, - {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c7907c8548bcd6ab860e5f513e727c53b4a714f459b084f6580b49fa1b9cee"}, - {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4f6450109834af88cb4cc5ecddfc5380ebb9c228695afc11915a0bf82116789"}, - {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9ca04806f3be0ac6d558fffc2fdf8fcef767e0489d2684a21912cc4ed0cd1b8"}, - {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77a6e85b90a7641d2e07184df5557132a337f136250caafc9ccaa4a2a998ca2c"}, - {file = "yarl-1.18.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6333c5a377c8e2f5fae35e7b8f145c617b02c939d04110c76f29ee3676b5f9a5"}, - {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0b3c92fa08759dbf12b3a59579a4096ba9af8dd344d9a813fc7f5070d86bbab1"}, - {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4ac515b860c36becb81bb84b667466885096b5fc85596948548b667da3bf9f24"}, - {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:045b8482ce9483ada4f3f23b3774f4e1bf4f23a2d5c912ed5170f68efb053318"}, - {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:a4bb030cf46a434ec0225bddbebd4b89e6471814ca851abb8696170adb163985"}, - {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:54d6921f07555713b9300bee9c50fb46e57e2e639027089b1d795ecd9f7fa910"}, - {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1d407181cfa6e70077df3377938c08012d18893f9f20e92f7d2f314a437c30b1"}, - {file = "yarl-1.18.3-cp39-cp39-win32.whl", hash = "sha256:ac36703a585e0929b032fbaab0707b75dc12703766d0b53486eabd5139ebadd5"}, - {file = "yarl-1.18.3-cp39-cp39-win_amd64.whl", hash = "sha256:ba87babd629f8af77f557b61e49e7c7cac36f22f871156b91e10a6e9d4f829e9"}, - {file = "yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b"}, - {file = "yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1"}, +python-versions = ">=3.10" +files = [ + {file = "yarl-1.24.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5249a113065c2b7a958bc699759e359cd61cfc81e3069662208f48f191b7ed12"}, + {file = "yarl-1.24.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7f4425fa244fbf530b006d0c5f79ce920114cfff5b4f5f6056e669f8e160fdc0"}, + {file = "yarl-1.24.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15c0b5e49d3c44e2a0b93e6a49476c5edad0a7686b92c395765a7ea775572a75"}, + {file = "yarl-1.24.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:246d32a53a947c8f0189f5d699cbd4c7036de45d9359e13ba238d1239678c727"}, + {file = "yarl-1.24.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:64480fb3e4d4ed9ed71c48a91a477384fc342a50ca30071d2f8a88d51d9c9413"}, + {file = "yarl-1.24.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:349de4701dc3760b6e876628423a8f147ef4f5599d10aba1e10702075d424ed9"}, + {file = "yarl-1.24.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d162677af8d5d3d6ebab8394b021f4d041ac107a4b705873148a77a49dc9e1b2"}, + {file = "yarl-1.24.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5f5c6ec23a9043f2d139cc072f53dd23168d202a334b9b2fda8de4c3e890d90"}, + {file = "yarl-1.24.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:60de6742447fbbf697f16f070b8a443f1b5fe6ca3826fbef9fe70ecd5328e643"}, + {file = "yarl-1.24.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acf93187c3710e422368eb768aee98db551ec7c85adc250207a95c16548ab7ac"}, + {file = "yarl-1.24.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f4b0352fd41fd34b6651934606268816afd6914d09626f9bcbbf018edb0afb3f"}, + {file = "yarl-1.24.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6b208bb939099b4b297438da4e9b25357f0b1c791888669b963e45b203ea9f36"}, + {file = "yarl-1.24.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4b85b8825e631295ff4bc8943f7471d54c533a9360bbe15ebb38e018b555bb8a"}, + {file = "yarl-1.24.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e26acf20c26cb4fefc631fdb75aca2a6b8fa8b7b5d7f204fb6a8f1e63c706f53"}, + {file = "yarl-1.24.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:819ca24f8eafcfb683c1bd5f44f2f488cea1274eb8944731ffd2e1f10f619342"}, + {file = "yarl-1.24.2-cp310-cp310-win_amd64.whl", hash = "sha256:5cb0f995a901c36be096ccbf4c673591c2faabbe96279598ffaec8c030f85bf4"}, + {file = "yarl-1.24.2-cp310-cp310-win_arm64.whl", hash = "sha256:f408eace7e22a68b467a0562e0d27d322f91fe3eaaa6f466b962c6cfaea9fa39"}, + {file = "yarl-1.24.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:36348bebb147b83818b9d7e673ea4debc75970afc6ffdc7e3975ad05ce5a58c1"}, + {file = "yarl-1.24.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a97e42c8a2233f2f279ecadd9e4a037bcb5d813b78435e8eedd4db5a9e9708c"}, + {file = "yarl-1.24.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8d027d56f1035e339d1001ac33eceab5b2ec8e42e449787bb75e289fb9a5cd1d"}, + {file = "yarl-1.24.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a6377060e7927187a42b7eb202090cbe2b34933a4eeaf90e3bd9e33432e5cae"}, + {file = "yarl-1.24.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:17076578bce0049a5ce57d14ad1bded391b68a3b213e9b81b0097b090244999a"}, + {file = "yarl-1.24.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:50713f1d4d6be6375bb178bb43d140ee1acb8abe589cd723320b7925a275be1e"}, + {file = "yarl-1.24.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:34263e2fa8fb5bb63a0d97706cda38edbad62fddb58c7f12d6acbc092812aa50"}, + {file = "yarl-1.24.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49016d82f032b1bd1e10b01078a7d29ae71bf468eeae0ea22df8bab691e60003"}, + {file = "yarl-1.24.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3f6d2c216318f8f32038ca3f72501ba08536f0fd18a36e858836b121b2deed9f"}, + {file = "yarl-1.24.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:08d3a33218e0c64393e7610284e770409a9c31c429b078bcb24096ed0a783b8f"}, + {file = "yarl-1.24.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5d699376c4ca3cba49bbfae3a05b5b70ded572937171ce1e0b8d87118e2ba294"}, + {file = "yarl-1.24.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a1cab588b4fa14bea2e55ebea27478adfb05372f47573738e1acc4a36c0b05d2"}, + {file = "yarl-1.24.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ec87ccc31bd21db7ad009d8572c127c1000f268517618a4cc09adba3c2a7f21c"}, + {file = "yarl-1.24.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d1dd47a22843b212baa8d74f37796815d43bd046b42a0f41e9da433386c3136b"}, + {file = "yarl-1.24.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7b54b9c67c2b06bd7b9a77253d242124b9c95d2c02def5a1144001ee547dd9d5"}, + {file = "yarl-1.24.2-cp311-cp311-win_amd64.whl", hash = "sha256:f8fdbcff8b2c7c9284e60c196f693588598ddcee31e11c18e14949ce44519d45"}, + {file = "yarl-1.24.2-cp311-cp311-win_arm64.whl", hash = "sha256:b32c37a7a337e90822c45797bf3d79d60875cfcccd3ecc80e9f453d87026c122"}, + {file = "yarl-1.24.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8"}, + {file = "yarl-1.24.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3b075301a2836a0e297b1b658cb6d6135df535d62efefdd60366bd589c2c82f2"}, + {file = "yarl-1.24.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d"}, + {file = "yarl-1.24.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035"}, + {file = "yarl-1.24.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576"}, + {file = "yarl-1.24.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8"}, + {file = "yarl-1.24.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7"}, + {file = "yarl-1.24.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c"}, + {file = "yarl-1.24.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d"}, + {file = "yarl-1.24.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db"}, + {file = "yarl-1.24.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712"}, + {file = "yarl-1.24.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996"}, + {file = "yarl-1.24.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b"}, + {file = "yarl-1.24.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c"}, + {file = "yarl-1.24.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1"}, + {file = "yarl-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dafe10c12ddd4d120d528c4b5599c953bd7b12845347d507b95451195bb6cad"}, + {file = "yarl-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30"}, + {file = "yarl-1.24.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536"}, + {file = "yarl-1.24.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e89418f65eda18f99030386305bd44d7d504e328a7945db1ead514fbe03a0607"}, + {file = "yarl-1.24.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1"}, + {file = "yarl-1.24.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986"}, + {file = "yarl-1.24.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488"}, + {file = "yarl-1.24.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b"}, + {file = "yarl-1.24.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592"}, + {file = "yarl-1.24.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617"}, + {file = "yarl-1.24.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92"}, + {file = "yarl-1.24.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a"}, + {file = "yarl-1.24.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44"}, + {file = "yarl-1.24.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a"}, + {file = "yarl-1.24.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf"}, + {file = "yarl-1.24.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056"}, + {file = "yarl-1.24.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992"}, + {file = "yarl-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:7d37fb7c38f2b6edab0f845c4f85148d4c44204f52bc127021bd2bc9fdbf1656"}, + {file = "yarl-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:1e831894be7c2954240e49791fa4b50c05a0dc881de2552cfe3ffd8631c7f461"}, + {file = "yarl-1.24.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f9312b3c02d9b3d23840f67952913c9c8721d7f1b7db305289faefa878f364c2"}, + {file = "yarl-1.24.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a4f4d6cd615823bfc7fb7e9b5987c3f41666371d870d51058f77e2680fbe9630"}, + {file = "yarl-1.24.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0c3063e5c0a8e8e62fae6c2596fa01da1561e4cd1da6fec5789f5cf99a8aefd8"}, + {file = "yarl-1.24.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fecd17873a096036c1c87ab3486f1aef7f269ada7f23f7f856f93b1cc7744f14"}, + {file = "yarl-1.24.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a46d1ab4ba4d32e6dc80daf8a28ce0bd83d08df52fbc32f3e288663427734535"}, + {file = "yarl-1.24.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73e68edf6dfd5f73f9ca127d84e2a6f9213c65bdffb736bda19524c0564fcd14"}, + {file = "yarl-1.24.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a296ca617f2d25fbceafb962b88750d627e5984e75732c712154d058ae8d79a3"}, + {file = "yarl-1.24.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51b2cf5ec89a8b8470177641ed62a3ba22d74e1e898e06ad53aa77972487208"}, + {file = "yarl-1.24.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:310fc687f7b2044ec54e372c8cbe923bb88f5c37bded0d3079e5791c2fc3cf50"}, + {file = "yarl-1.24.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:297a2fe352ecf858b30a98f87948746ec16f001d279f84aebdbd3bd965e2f1bd"}, + {file = "yarl-1.24.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2a263e76b97bc42bdcd7c5f4953dec1f7cd62a1112fa7f869e57255229390d67"}, + {file = "yarl-1.24.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:822519b64cf0b474f1a0aaef1dc621438ea46bb77c94df97a5b4d213a7d8a8b1"}, + {file = "yarl-1.24.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b6067060d9dc594899ba83e6db6c48c68d1e494a6dab158156ed86977ca7bcb1"}, + {file = "yarl-1.24.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0063adad533e57171b79db3943b229d40dfafeeee579767f96541f106bac5f1b"}, + {file = "yarl-1.24.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ee8e3fb34513e8dc082b586ef4910c98335d43a6fab688cd44d4851bacfce3e8"}, + {file = "yarl-1.24.2-cp314-cp314-win_amd64.whl", hash = "sha256:afb00d7fd8e0f285ca29a44cc50df2d622ff2f7a6d933fa641577b5f9d5f3db0"}, + {file = "yarl-1.24.2-cp314-cp314-win_arm64.whl", hash = "sha256:68cf6eacd6028ef1142bc4b48376b81566385ca6f9e7dde3b0fa91be08ffcb57"}, + {file = "yarl-1.24.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:221ce1dd921ac4f603957f17d7c18c5cc0797fbb52f156941f92e04605d1d67b"}, + {file = "yarl-1.24.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5f3224db28173a00d7afacdee07045cc4673dfab2b15492c7ae10deddbece761"}, + {file = "yarl-1.24.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c557165320d6244ebe3a02431b2a201a20080e02f41f0cfa0ccc47a183765da8"}, + {file = "yarl-1.24.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:904065e6e85b1fa54d0d87438bd58c14c0bad97aad654ad1077fd9d87e8478ed"}, + {file = "yarl-1.24.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cec2a38d70edc10e0e856ceda886af5327a017ccbde8e1de1bd44d300357543"}, + {file = "yarl-1.24.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7484b9361ed222ee1ca5b4337aa4cbdcc4618ce5aff57d9ef1582fd95893fc0"}, + {file = "yarl-1.24.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f9670b89f34db07f81e53aee83e0b938a3412329d51c8f922488be7fcc4024"}, + {file = "yarl-1.24.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abb2759733d63a28b4956500a5dd57140f26486c92b2caedfb964ab7d9b79dbf"}, + {file = "yarl-1.24.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:081c2bf54efe03774d0311172bc04fedf9ca01e644d4cd8c805688e527209bdc"}, + {file = "yarl-1.24.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:86746bef442aa479107fe28132e1277237f9c24c2f00b0b0cf22b3ee0904f2bb"}, + {file = "yarl-1.24.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:2d07d21d0bc4b17558e8de0b02fbfdf1e347d3bb3699edd00bb92e7c57925420"}, + {file = "yarl-1.24.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4fb1ac3fc5fecd8ae7453ea237e4d22b49befa70266dfe1629924245c21a0c7f"}, + {file = "yarl-1.24.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4da31a5512ed1729ca8d8aacde3f7faeb8843cde3165d6bcf7f88f74f17bb8aa"}, + {file = "yarl-1.24.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:533ded4dceb5f1f3da7906244f4e82cf46cfd40d84c69a1faf5ac506aa65ecbe"}, + {file = "yarl-1.24.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7b3a85525f6e7eeabcfdd372862b21ee1915db1b498a04e8bf0e389b607ff0bd"}, + {file = "yarl-1.24.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a7624b1ca46ca5d7b864ef0d2f8efe3091454085ee1855b4e992314529972215"}, + {file = "yarl-1.24.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d"}, + {file = "yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9"}, + {file = "yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8"}, ] [package.dependencies] idna = ">=2.0" multidict = ">=4.0" -propcache = ">=0.2.0" +propcache = ">=0.2.1" [metadata] lock-version = "2.0" python-versions = "^3.10,<3.13" -content-hash = "84f9ce0ad56ffab8a5101eba4d9102f9df7d27d3663fdc08aaeafa49f7f467ab" +content-hash = "e9469d22ecf7b753946d0a9a78b8e597ef90123dde33916b2ec5c0a6571081af" diff --git a/packages/examples/cvat/recording-oracle/poetry.lock b/packages/examples/cvat/recording-oracle/poetry.lock index d467345a9f..b65a3af355 100644 --- a/packages/examples/cvat/recording-oracle/poetry.lock +++ b/packages/examples/cvat/recording-oracle/poetry.lock @@ -238,6 +238,24 @@ tests = ["attrs[tests-no-zope]", "zope-interface"] tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] +[[package]] +name = "audio-qe" +version = "0.1.0" +description = "Audio transcription quality estimation" +optional = false +python-versions = "^3.10, <3.13" +files = [] +develop = true + +[package.dependencies] +numpy = "^1.25.2" +regex = "^2024.11.6" +scipy = "^1.13.1" + +[package.source] +type = "directory" +url = "libs/audio_qe" + [[package]] name = "bitarray" version = "3.1.0" @@ -3846,45 +3864,66 @@ crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] [[package]] name = "scipy" -version = "1.12.0" +version = "1.15.3" description = "Fundamental algorithms for scientific computing in Python" optional = false -python-versions = ">=3.9" -files = [ - {file = "scipy-1.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:78e4402e140879387187f7f25d91cc592b3501a2e51dfb320f48dfb73565f10b"}, - {file = "scipy-1.12.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:f5f00ebaf8de24d14b8449981a2842d404152774c1a1d880c901bf454cb8e2a1"}, - {file = "scipy-1.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e53958531a7c695ff66c2e7bb7b79560ffdc562e2051644c5576c39ff8efb563"}, - {file = "scipy-1.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e32847e08da8d895ce09d108a494d9eb78974cf6de23063f93306a3e419960c"}, - {file = "scipy-1.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4c1020cad92772bf44b8e4cdabc1df5d87376cb219742549ef69fc9fd86282dd"}, - {file = "scipy-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:75ea2a144096b5e39402e2ff53a36fecfd3b960d786b7efd3c180e29c39e53f2"}, - {file = "scipy-1.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:408c68423f9de16cb9e602528be4ce0d6312b05001f3de61fe9ec8b1263cad08"}, - {file = "scipy-1.12.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:5adfad5dbf0163397beb4aca679187d24aec085343755fcdbdeb32b3679f254c"}, - {file = "scipy-1.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3003652496f6e7c387b1cf63f4bb720951cfa18907e998ea551e6de51a04467"}, - {file = "scipy-1.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b8066bce124ee5531d12a74b617d9ac0ea59245246410e19bca549656d9a40a"}, - {file = "scipy-1.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8bee4993817e204d761dba10dbab0774ba5a8612e57e81319ea04d84945375ba"}, - {file = "scipy-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a24024d45ce9a675c1fb8494e8e5244efea1c7a09c60beb1eeb80373d0fecc70"}, - {file = "scipy-1.12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e7e76cc48638228212c747ada851ef355c2bb5e7f939e10952bc504c11f4e372"}, - {file = "scipy-1.12.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:f7ce148dffcd64ade37b2df9315541f9adad6efcaa86866ee7dd5db0c8f041c3"}, - {file = "scipy-1.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c39f92041f490422924dfdb782527a4abddf4707616e07b021de33467f917bc"}, - {file = "scipy-1.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7ebda398f86e56178c2fa94cad15bf457a218a54a35c2a7b4490b9f9cb2676c"}, - {file = "scipy-1.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:95e5c750d55cf518c398a8240571b0e0782c2d5a703250872f36eaf737751338"}, - {file = "scipy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e646d8571804a304e1da01040d21577685ce8e2db08ac58e543eaca063453e1c"}, - {file = "scipy-1.12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:913d6e7956c3a671de3b05ccb66b11bc293f56bfdef040583a7221d9e22a2e35"}, - {file = "scipy-1.12.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba1b0c7256ad75401c73e4b3cf09d1f176e9bd4248f0d3112170fb2ec4db067"}, - {file = "scipy-1.12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:730badef9b827b368f351eacae2e82da414e13cf8bd5051b4bdfd720271a5371"}, - {file = "scipy-1.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6546dc2c11a9df6926afcbdd8a3edec28566e4e785b915e849348c6dd9f3f490"}, - {file = "scipy-1.12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:196ebad3a4882081f62a5bf4aeb7326aa34b110e533aab23e4374fcccb0890dc"}, - {file = "scipy-1.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:b360f1b6b2f742781299514e99ff560d1fe9bd1bff2712894b52abe528d1fd1e"}, - {file = "scipy-1.12.0.tar.gz", hash = "sha256:4bf5abab8a36d20193c698b0f1fc282c1d083c94723902c447e5d2f1780936a3"}, +python-versions = ">=3.10" +files = [ + {file = "scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c"}, + {file = "scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253"}, + {file = "scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f"}, + {file = "scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92"}, + {file = "scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82"}, + {file = "scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40"}, + {file = "scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e"}, + {file = "scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c"}, + {file = "scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13"}, + {file = "scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b"}, + {file = "scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba"}, + {file = "scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65"}, + {file = "scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1"}, + {file = "scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889"}, + {file = "scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982"}, + {file = "scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9"}, + {file = "scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594"}, + {file = "scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb"}, + {file = "scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019"}, + {file = "scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6"}, + {file = "scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477"}, + {file = "scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c"}, + {file = "scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45"}, + {file = "scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49"}, + {file = "scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e"}, + {file = "scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539"}, + {file = "scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed"}, + {file = "scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759"}, + {file = "scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62"}, + {file = "scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb"}, + {file = "scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730"}, + {file = "scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825"}, + {file = "scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7"}, + {file = "scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11"}, + {file = "scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126"}, + {file = "scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163"}, + {file = "scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8"}, + {file = "scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5"}, + {file = "scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e"}, + {file = "scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb"}, + {file = "scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723"}, + {file = "scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb"}, + {file = "scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4"}, + {file = "scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5"}, + {file = "scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca"}, + {file = "scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf"}, ] [package.dependencies] -numpy = ">=1.22.4,<1.29.0" +numpy = ">=1.23.5,<2.5" [package.extras] -dev = ["click", "cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"] -doc = ["jupytext", "matplotlib (>2)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-design (>=0.2.0)"] -test = ["asv", "gmpy2", "hypothesis", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] +dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] +doc = ["intersphinx_registry", "jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.19.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<8.0.0)", "sphinx-copybutton", "sphinx-design (>=0.4.0)"] +test = ["Cython", "array-api-strict (>=2.0,<2.1.1)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] [[package]] name = "setuptools" @@ -4636,4 +4675,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.0" python-versions = "^3.10, <3.13" -content-hash = "7b255d62870b43bd42bf9b686e94e5dbbda9929f4a503d6b5e6e91c8ca2627f4" +content-hash = "7f165837785af6689800e00dfc6170fdaa1687cff3d83ed0c69c69accc8b058b" From fb050b24e70f2d3d7f0faeb834ad6af1fae6065a Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Mon, 13 Jul 2026 17:48:23 +0300 Subject: [PATCH 36/53] Improve test minio file layout --- .../docker-compose.test.head.yml | 2 +- .../exchange-oracle/docker-compose.test.yml | 11 +++++--- .../tests/api/test_exchange_api.py | 28 +++++++++---------- .../manifests/manifest-v1.json} | 0 .../test_track_completed_escrows.py | 8 +++--- .../test_process_job_launcher_webhooks.py | 4 +-- .../integration/services/test_exchange.py | 24 ++++++++-------- .../exchange-oracle/tests/utils/constants.py | 2 +- .../docker-compose.test.head.yml | 4 +-- .../recording-oracle/docker-compose.test.yml | 11 +++++--- .../cloud/manifests/manifest-v1.json} | 0 .../recording-oracle/tests/utils/constants.py | 2 +- 12 files changed, 51 insertions(+), 45 deletions(-) rename packages/examples/cvat/exchange-oracle/tests/assets/{manifests/manifest.json => cloud/manifests/manifest-v1.json} (100%) rename packages/examples/cvat/recording-oracle/tests/{utils/manifest.json => assets/cloud/manifests/manifest-v1.json} (100%) diff --git a/packages/examples/cvat/exchange-oracle/docker-compose.test.head.yml b/packages/examples/cvat/exchange-oracle/docker-compose.test.head.yml index b885070446..7647773326 100644 --- a/packages/examples/cvat/exchange-oracle/docker-compose.test.head.yml +++ b/packages/examples/cvat/exchange-oracle/docker-compose.test.head.yml @@ -15,7 +15,7 @@ services: STORAGE_ENDPOINT_URL: 'host.docker.internal:9000' STORAGE_ACCESS_KEY: 'dev' STORAGE_SECRET_KEY: 'devdevdev' - STORAGE_RESULTS_BUCKET_NAME: 'results' + STORAGE_BUCKET_NAME: 'excor-data' STORAGE_USE_SSL: 'False' STORAGE_PROVIDER: 'aws' ENABLE_CUSTOM_CLOUD_HOST: 'Yes' diff --git a/packages/examples/cvat/exchange-oracle/docker-compose.test.yml b/packages/examples/cvat/exchange-oracle/docker-compose.test.yml index 216ac36132..ba527df3bc 100644 --- a/packages/examples/cvat/exchange-oracle/docker-compose.test.yml +++ b/packages/examples/cvat/exchange-oracle/docker-compose.test.yml @@ -36,10 +36,11 @@ services: MINIO_ROOT_USER: dev MINIO_ROOT_PASSWORD: devdevdev volumes: - - ./tests/utils/manifest.json:/tmp/manifests/manifest.json + # Each subdirectory under the shared assets dir seeds a same-named bucket. + - ./tests/assets/cloud:/tmp/cloud:ro entrypoint: 'sh' command: - -c "mkdir -p /data/manifests && cp /tmp/manifests/manifest.json /data/manifests/manifest.json && minio server /data --console-address ':9001'" + -c "mkdir -p /data && cp -r /tmp/cloud/. /data/ && minio server /data --console-address ':9001'" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 5s @@ -57,9 +58,11 @@ services: entrypoint: > /bin/sh -c " /usr/bin/mc config host add myminio http://minio:9000 dev devdevdev; + /usr/bin/mc mb -p myminio/datasets myminio/manifests myminio/excor-data myminio/recor-data; + /usr/bin/mc anonymous set public myminio/datasets; /usr/bin/mc anonymous set public myminio/manifests; - /usr/bin/mc mb myminio/results; - /usr/bin/mc anonymous set public myminio/results; + /usr/bin/mc anonymous set public myminio/excor-data; + /usr/bin/mc anonymous set public myminio/recor-data; " networks: - test-network diff --git a/packages/examples/cvat/exchange-oracle/tests/api/test_exchange_api.py b/packages/examples/cvat/exchange-oracle/tests/api/test_exchange_api.py index b3c552b888..a56413ddfb 100644 --- a/packages/examples/cvat/exchange-oracle/tests/api/test_exchange_api.py +++ b/packages/examples/cvat/exchange-oracle/tests/api/test_exchange_api.py @@ -175,7 +175,7 @@ def validate_result( session.commit() with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, patch( "src.endpoints.serializers.get_escrow_fund_token_symbol" @@ -240,7 +240,7 @@ def test_can_list_jobs_200_without_escrows_in_hidden_states( session.commit() with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, patch( "src.endpoints.serializers.get_escrow_fund_token_symbol" @@ -298,7 +298,7 @@ def test_can_list_jobs_200_with_only_one_entry_per_escrow_address_if_several_pro session.commit() with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, patch( "src.endpoints.serializers.get_escrow_fund_token_symbol" @@ -338,7 +338,7 @@ def test_can_list_jobs_200_with_fields(client: TestClient, session: Session) -> session.commit() with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, patch( "src.endpoints.serializers.get_escrow_fund_token_symbol" @@ -421,7 +421,7 @@ def test_can_list_jobs_200_with_sorting(client: TestClient, session: Session) -> } == {(obj.__class__.__name__, obj.id): True for obj in cvat_jobs + cvat_tasks + cvat_projects} with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, patch( "src.endpoints.serializers.get_escrow_fund_token_symbol" @@ -541,7 +541,7 @@ def test_can_list_jobs_200_with_filters(client: TestClient, session: Session): post_init_time = utcnow() + timedelta(seconds=1) with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, patch( "src.endpoints.serializers.get_escrow_fund_token_symbol" @@ -632,7 +632,7 @@ def test_can_list_jobs_200_can_show_only_active_jobs_with_free_assignments( session.commit() with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, patch( "src.endpoints.serializers.get_escrow_fund_token_symbol" @@ -681,7 +681,7 @@ def test_can_list_jobs_200_check_values(client: TestClient, session: Session) -> session.commit() with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, patch( "src.endpoints.serializers.get_escrow_fund_token_symbol" @@ -740,7 +740,7 @@ def test_can_list_jobs_200_without_address(client: TestClient, session: Session) session.commit() with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, patch( "src.endpoints.serializers.get_escrow_fund_token_symbol" @@ -875,7 +875,7 @@ def test_can_create_assignment_200(client: TestClient, session: Session) -> None session.commit() with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.endpoints.serializers.get_escrow_manifest") as mock_serializer_get_manifest, patch("src.services.exchange.get_escrow_manifest") as mock_exchange_get_manifest, patch( @@ -980,7 +980,7 @@ def test_cannot_create_assignment_400_when_has_unfinished_assignments( session.commit() with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.services.exchange.get_escrow_manifest") as mock_get_manifest, ): manifest = json.load(data) @@ -1075,7 +1075,7 @@ def test_can_list_assignments_200(client: TestClient, session: Session) -> None: post_init_time = utcnow() + timedelta(seconds=1) with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, patch( "src.endpoints.serializers.get_escrow_fund_token_symbol" @@ -1156,7 +1156,7 @@ def test_can_list_assignments_200_with_sorting(client: TestClient, session: Sess session.commit() with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, patch( "src.endpoints.serializers.get_escrow_fund_token_symbol" @@ -1513,7 +1513,7 @@ def test_can_list_jobs_200_check_updated_at(client: TestClient, session: Session session.commit() with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.endpoints.serializers.get_escrow_manifest") as mock_serializer_get_manifest, patch("src.services.exchange.get_escrow_manifest") as mock_exchange_get_manifest, patch( diff --git a/packages/examples/cvat/exchange-oracle/tests/assets/manifests/manifest.json b/packages/examples/cvat/exchange-oracle/tests/assets/cloud/manifests/manifest-v1.json similarity index 100% rename from packages/examples/cvat/exchange-oracle/tests/assets/manifests/manifest.json rename to packages/examples/cvat/exchange-oracle/tests/assets/cloud/manifests/manifest-v1.json diff --git a/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_escrows.py b/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_escrows.py index 18e7e422b3..ff3e01a75d 100644 --- a/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_escrows.py +++ b/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_escrows.py @@ -572,7 +572,7 @@ def test_can_export_escrow(self): self.session.commit() with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.handlers.job_export.handlers.get_escrow_manifest") as mock_get_manifest, patch("src.handlers.job_export.handlers.validate_escrow"), patch("src.handlers.job_export.downloading.cvat_api") as mock_cvat_api, @@ -700,7 +700,7 @@ def test_can_export_escrow_error_getting_annotations(self): self.session.commit() with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.handlers.job_export.handlers.get_escrow_manifest") as mock_get_manifest, patch("src.handlers.job_export.handlers.validate_escrow"), patch( @@ -823,7 +823,7 @@ def test_can_export_escrow_error_uploading_files(self): self.session.commit() with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.handlers.job_export.handlers.get_escrow_manifest") as mock_get_manifest, patch("src.handlers.job_export.handlers.validate_escrow"), patch("src.handlers.job_export.downloading.cvat_api") as mock_cvat_api, @@ -948,7 +948,7 @@ def test_can_export_multiple_projects_per_escrow(self): self.session.commit() with ( - open("tests/assets/manifests/manifest.json") as manifest_data, + open("tests/assets/cloud/manifests/manifest-v1.json") as manifest_data, patch("src.handlers.job_export.handlers.get_escrow_manifest") as mock_get_manifest, patch("src.handlers.job_export.handlers.validate_escrow"), patch("src.handlers.job_export.downloading.cvat_api") as mock_cvat_api, diff --git a/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_job_launcher_webhooks.py b/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_job_launcher_webhooks.py index 5ac9267b70..75bc04b580 100644 --- a/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_job_launcher_webhooks.py +++ b/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_job_launcher_webhooks.py @@ -62,7 +62,7 @@ def test_process_incoming_job_launcher_webhooks_escrow_created_type(self): self.session.commit() with ( patch("src.chain.escrow.get_escrow") as mock_escrow, - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.handlers.job_creation.handlers.get_escrow_manifest") as mock_get_manifest, patch("src.handlers.job_creation.builders.vision.basic.cvat_api") as mock_cvat_api, patch( @@ -248,7 +248,7 @@ def test_process_incoming_job_launcher_webhooks_escrow_created_type_remove_when_ self.session.commit() with ( patch("src.chain.escrow.get_escrow") as mock_escrow, - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.handlers.job_creation.handlers.get_escrow_manifest") as mock_get_manifest, patch("src.handlers.job_creation.builders.vision.basic.cvat_api") as mock_cvat_api, patch( diff --git a/packages/examples/cvat/exchange-oracle/tests/integration/services/test_exchange.py b/packages/examples/cvat/exchange-oracle/tests/integration/services/test_exchange.py index 6d2b191df7..f86e0511e9 100644 --- a/packages/examples/cvat/exchange-oracle/tests/integration/services/test_exchange.py +++ b/packages/examples/cvat/exchange-oracle/tests/integration/services/test_exchange.py @@ -40,7 +40,7 @@ def test_serialize_job(self): self.session.commit() with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.endpoints.serializers.get_escrow_manifest") as mock_get_manifest, patch( "src.endpoints.serializers.get_escrow_fund_token_symbol" @@ -99,7 +99,7 @@ def test_create_assignment(self): self.session.commit() with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.services.exchange.get_escrow_manifest") as mock_get_manifest, patch("src.services.exchange.cvat_api"), ): @@ -154,7 +154,7 @@ def test_create_assignment_many_jobs_1_completed(self): self.session.commit() with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.services.exchange.get_escrow_manifest") as mock_get_manifest, patch("src.services.exchange.cvat_api"), ): @@ -193,7 +193,7 @@ def test_create_assignment_invalid_project(self): self.session.commit() with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.services.exchange.get_escrow_manifest") as mock_get_manifest, ): manifest = json.load(data) @@ -212,7 +212,7 @@ def test_create_assignment_no_required_qualifications(self): self.session.commit() with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.services.exchange.get_escrow_manifest") as mock_get_manifest, ): manifest = json.load(data) @@ -240,7 +240,7 @@ def test_create_assignment_with_required_qualifications(self): self.session.commit() with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.services.exchange.get_escrow_manifest") as mock_get_manifest, patch("src.services.exchange.cvat_api"), ): @@ -293,7 +293,7 @@ def test_create_assignment_unfinished_assignment(self): self.session.commit() with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.services.exchange.get_escrow_manifest") as mock_get_manifest, patch("src.services.exchange.cvat_api"), ): @@ -331,7 +331,7 @@ def test_create_assignment_has_expired_assignment_and_available_jobs(self): self.session.commit() with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.services.exchange.get_escrow_manifest") as mock_get_manifest, patch("src.services.exchange.cvat_api"), ): @@ -381,7 +381,7 @@ def test_create_assignment_no_available_jobs_completed_assignment(self): self.session.commit() with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.services.exchange.get_escrow_manifest") as mock_get_manifest, patch("src.services.exchange.cvat_api"), ): @@ -426,7 +426,7 @@ def test_create_assignment_no_available_jobs_active_foreign_assignment(self): self.session.commit() with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.services.exchange.get_escrow_manifest") as mock_get_manifest, patch("src.services.exchange.cvat_api"), ): @@ -464,7 +464,7 @@ def test_create_assignment_wont_reassign_job_to_previous_user(self): self.session.commit() with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.services.exchange.get_escrow_manifest") as mock_get_manifest, patch("src.services.exchange.cvat_api"), ): @@ -511,7 +511,7 @@ def test_create_assignment_can_assign_job_to_new_user(self): self.session.commit() with ( - open("tests/assets/manifests/manifest.json") as data, + open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.services.exchange.get_escrow_manifest") as mock_get_manifest, patch("src.services.exchange.cvat_api"), ): diff --git a/packages/examples/cvat/exchange-oracle/tests/utils/constants.py b/packages/examples/cvat/exchange-oracle/tests/utils/constants.py index fb27694525..a8588d1a2b 100644 --- a/packages/examples/cvat/exchange-oracle/tests/utils/constants.py +++ b/packages/examples/cvat/exchange-oracle/tests/utils/constants.py @@ -25,7 +25,7 @@ WALLET_ADDRESS1 = "0x86e83d346041E8806e352681f3F14549C0d2BC69" WALLET_ADDRESS2 = "0x86e83d346041E8806e352681f3F14549C0d2BC70" -DEFAULT_MANIFEST_URL = "http://host.docker.internal:9000/manifests/manifest.json" +DEFAULT_MANIFEST_URL = "http://host.docker.internal:9000/manifests/manifest-v1.json" DEFAULT_HASH = "test" SIGNATURE = ( diff --git a/packages/examples/cvat/recording-oracle/docker-compose.test.head.yml b/packages/examples/cvat/recording-oracle/docker-compose.test.head.yml index 044c537d3b..557bab8576 100644 --- a/packages/examples/cvat/recording-oracle/docker-compose.test.head.yml +++ b/packages/examples/cvat/recording-oracle/docker-compose.test.head.yml @@ -15,13 +15,13 @@ services: STORAGE_ENDPOINT_URL: 'host.docker.internal:9000' STORAGE_ACCESS_KEY: 'dev' STORAGE_SECRET_KEY: 'devdevdev' - STORAGE_RESULTS_BUCKET_NAME: 'results' + STORAGE_RESULTS_BUCKET_NAME: 'recor-data' STORAGE_PROVIDER: 'aws' STORAGE_USE_SSL: False EXCHANGE_ORACLE_STORAGE_ENDPOINT_URL: 'host.docker.internal:9000' EXCHANGE_ORACLE_STORAGE_ACCESS_KEY: 'dev' EXCHANGE_ORACLE_STORAGE_SECRET_KEY: 'devdevdev' - EXCHANGE_ORACLE_STORAGE_RESULTS_BUCKET_NAME: 'results' + EXCHANGE_ORACLE_STORAGE_RESULTS_BUCKET_NAME: 'excor-data' EXCHANGE_ORACLE_STORAGE_USE_SSL: False EXCHANGE_ORACLE_STORAGE_PROVIDER: 'aws' depends_on: diff --git a/packages/examples/cvat/recording-oracle/docker-compose.test.yml b/packages/examples/cvat/recording-oracle/docker-compose.test.yml index 82c78ff286..526fb2b7d0 100644 --- a/packages/examples/cvat/recording-oracle/docker-compose.test.yml +++ b/packages/examples/cvat/recording-oracle/docker-compose.test.yml @@ -18,11 +18,11 @@ services: MINIO_ROOT_USER: dev MINIO_ROOT_PASSWORD: devdevdev volumes: - - ./tests/utils/manifest.json:/tmp/manifests/manifest.json - - ./tests/utils/intermediate-results.json:/tmp/results/intermediate-results.json + # Each subdirectory under the shared assets dir seeds a same-named bucket. + - ./tests/assets/cloud:/tmp/cloud:ro entrypoint: 'sh' command: - -c "mkdir -p /data/manifests && cp /tmp/manifests/manifest.json /data/manifests/manifest.json && mkdir -p /data/results && cp /tmp/results/intermediate-results.json /data/results/intermediate-results.json && minio server /data --console-address ':9001'" + -c "mkdir -p /data && cp -r /tmp/cloud/. /data/ && minio server /data --console-address ':9001'" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 5s @@ -40,8 +40,11 @@ services: entrypoint: > /bin/sh -c " /usr/bin/mc config host add myminio http://minio:9000 dev devdevdev; + /usr/bin/mc mb -p myminio/datasets myminio/manifests myminio/excor-data myminio/recor-data; + /usr/bin/mc anonymous set public myminio/datasets; /usr/bin/mc anonymous set public myminio/manifests; - /usr/bin/mc anonymous set public myminio/results; + /usr/bin/mc anonymous set public myminio/excor-data; + /usr/bin/mc anonymous set public myminio/recor-data; " networks: - test-network diff --git a/packages/examples/cvat/recording-oracle/tests/utils/manifest.json b/packages/examples/cvat/recording-oracle/tests/assets/cloud/manifests/manifest-v1.json similarity index 100% rename from packages/examples/cvat/recording-oracle/tests/utils/manifest.json rename to packages/examples/cvat/recording-oracle/tests/assets/cloud/manifests/manifest-v1.json diff --git a/packages/examples/cvat/recording-oracle/tests/utils/constants.py b/packages/examples/cvat/recording-oracle/tests/utils/constants.py index 90af4374a6..fcf9263452 100644 --- a/packages/examples/cvat/recording-oracle/tests/utils/constants.py +++ b/packages/examples/cvat/recording-oracle/tests/utils/constants.py @@ -19,7 +19,7 @@ EXCHANGE_ORACLE_ADDRESS = "0x90F79bf6EB2c4f870365E785982E1f101E93b906" EXCHANGE_ORACLE_FEE = 10 -DEFAULT_MANIFEST_URL = "http://host.docker.internal:9000/manifests/manifest.json" +DEFAULT_MANIFEST_URL = "http://host.docker.internal:9000/manifests/manifest-v1.json" DEFAULT_HASH = "test" SIGNATURE = ( From e59fb728a32a0f0d94f0a4e790b287f7afe6d2b4 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Mon, 13 Jul 2026 19:11:36 +0300 Subject: [PATCH 37/53] Revert extra change --- .../cvat/exchange-oracle/src/services/exchange.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/examples/cvat/exchange-oracle/src/services/exchange.py b/packages/examples/cvat/exchange-oracle/src/services/exchange.py index 3ece001f65..8e629553f9 100644 --- a/packages/examples/cvat/exchange-oracle/src/services/exchange.py +++ b/packages/examples/cvat/exchange-oracle/src/services/exchange.py @@ -5,7 +5,7 @@ import src.services.cvat as cvat_service from src.chain.escrow import get_escrow_manifest from src.core.manifest import parse_manifest -from src.core.types import JobStatuses, Networks +from src.core.types import JobStatuses, Networks, ProjectStatuses from src.db import SessionLocal from src.db.utils import ForUpdateParams from src.models.cvat import Job @@ -52,6 +52,14 @@ def create_assignment( "The user already has an unfinished assignment in this project" ) + get_or_404( + cvat_service.get_project_by_escrow_address( + session, escrow_address, status_in=[ProjectStatuses.annotation] + ), + escrow_address, + object_type_name="job", + ) + unassigned_job = cvat_service.get_free_job( session, escrow_address=escrow_address, From 59dda0cc8a8b9ac76c8662c9591a22b0751ab991 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Mon, 13 Jul 2026 19:20:39 +0300 Subject: [PATCH 38/53] Update tests --- .../test_track_completed_escrows.py | 15 +++++++++++++++ .../cron/test_process_job_launcher_webhooks.py | 6 +++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_escrows.py b/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_escrows.py index ff3e01a75d..80870ecc89 100644 --- a/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_escrows.py +++ b/packages/examples/cvat/exchange-oracle/tests/integration/cron/state_trackers/test_track_completed_escrows.py @@ -263,7 +263,12 @@ def test_can_request_validation(self): self.session.commit() with ( + open("tests/assets/cloud/manifests/manifest-v1.json") as manifest_data, patch("src.handlers.job_completion.handlers.validate_escrow"), + patch( + "src.handlers.job_completion.handlers.get_escrow_manifest", + return_value=json.load(manifest_data), + ), patch("src.handlers.job_export.results.cloud_service") as mock_cloud_service, ): mock_storage_client = Mock() @@ -373,7 +378,12 @@ def test_request_validation_error_in_file_uploading_increases_attempts(self): self.session.commit() with ( + open("tests/assets/cloud/manifests/manifest-v1.json") as manifest_data, patch("src.handlers.job_completion.handlers.validate_escrow"), + patch( + "src.handlers.job_completion.handlers.get_escrow_manifest", + return_value=json.load(manifest_data), + ), patch("src.handlers.job_export.results.cloud_service") as mock_cloud_service, patch("src.services.cloud.make_client"), ): @@ -466,7 +476,12 @@ def test_can_request_validation_multiple_projects_per_escrow_all_completed(self) self.session.commit() with ( + open("tests/assets/cloud/manifests/manifest-v1.json") as manifest_data, patch("src.handlers.job_completion.handlers.validate_escrow"), + patch( + "src.handlers.job_completion.handlers.get_escrow_manifest", + return_value=json.load(manifest_data), + ), patch("src.handlers.job_export.results.cloud_service") as mock_cloud_service, ): mock_storage_client = Mock() diff --git a/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_job_launcher_webhooks.py b/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_job_launcher_webhooks.py index 75bc04b580..15dd21b1ee 100644 --- a/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_job_launcher_webhooks.py +++ b/packages/examples/cvat/exchange-oracle/tests/integration/cron/test_process_job_launcher_webhooks.py @@ -60,11 +60,15 @@ def test_process_incoming_job_launcher_webhooks_escrow_created_type(self): self.session.add(webhook) self.session.commit() + # The task builder is split across the `basic` and `base` modules, both of which import + # `cvat_api`; share a single mock across both so all CVAT calls are intercepted. + mock_cvat_api = MagicMock() with ( patch("src.chain.escrow.get_escrow") as mock_escrow, open("tests/assets/cloud/manifests/manifest-v1.json") as data, patch("src.handlers.job_creation.handlers.get_escrow_manifest") as mock_get_manifest, - patch("src.handlers.job_creation.builders.vision.basic.cvat_api") as mock_cvat_api, + patch("src.handlers.job_creation.builders.vision.basic.cvat_api", mock_cvat_api), + patch("src.handlers.job_creation.builders.vision.base.cvat_api", mock_cvat_api), patch( "src.handlers.job_creation.builders.vision.basic.cloud_service.make_client" ) as mock_make_cloud_client, From 04a350c3b17b207f284e9c1b35ee7c9eb5034753 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Tue, 14 Jul 2026 18:06:57 +0300 Subject: [PATCH 39/53] Refactor str enums, format code --- .../cvat/exchange-oracle/src/core/config.py | 4 +-- .../src/core/manifest/shared.py | 7 ++--- .../exchange-oracle/src/core/manifest/v2.py | 9 ++---- .../core/tasks/audio_transcription/meta.py | 5 ++-- .../core/tasks/audio_transcription/spec.py | 5 ++-- .../exchange-oracle/src/core/tasks/errors.py | 2 +- .../exchange-oracle/src/core/tasks/types.py | 4 +-- .../cvat/exchange-oracle/src/core/types.py | 30 +++++++++---------- .../exchange-oracle/src/cvat/api_calls.py | 11 ++++--- .../src/endpoints/filtering.py | 9 +++--- .../job_completion/validators/audio.py | 10 ++----- .../builders/audio/transcription.py | 11 ++++--- .../handlers/job_export/exporters/audio.py | 14 ++------- .../exchange-oracle/src/schemas/exchange.py | 6 ++-- .../exchange-oracle/src/services/exchange.py | 3 +- .../exchange-oracle/src/services/webhook.py | 5 ++-- .../cvat/exchange-oracle/src/utils/enums.py | 7 +++++ .../cvat/recording-oracle/poetry.lock | 18 ++++++++++- .../cvat/recording-oracle/pyproject.toml | 1 + .../src/core/manifest/shared.py | 7 ++--- .../recording-oracle/src/core/manifest/v2.py | 9 ++---- .../core/tasks/audio_transcription/meta.py | 5 ++-- .../core/tasks/audio_transcription/spec.py | 5 ++-- .../recording-oracle/src/core/tasks/types.py | 5 ++-- .../cvat/recording-oracle/src/core/types.py | 14 ++++----- .../recording-oracle/src/services/webhook.py | 5 ++-- .../cvat/recording-oracle/src/utils/enums.py | 7 +++++ 27 files changed, 111 insertions(+), 107 deletions(-) diff --git a/packages/examples/cvat/exchange-oracle/src/core/config.py b/packages/examples/cvat/exchange-oracle/src/core/config.py index 10d5bb3a5e..87a024ca00 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/config.py +++ b/packages/examples/cvat/exchange-oracle/src/core/config.py @@ -4,7 +4,6 @@ import inspect import os from collections.abc import Iterable -from enum import Enum from os import getenv from typing import ClassVar, Optional @@ -14,6 +13,7 @@ from web3 import Web3 from web3.providers.rpc import HTTPProvider +from src.utils.enums import StrEnum from src.utils.logging import parse_log_level from src.utils.net import is_ipv4 @@ -321,7 +321,7 @@ class DevelopmentConfig: """ -class Environment(str, Enum): +class Environment(StrEnum): PRODUCTION = "production" DEVELOPMENT = "development" TEST = "test" diff --git a/packages/examples/cvat/exchange-oracle/src/core/manifest/shared.py b/packages/examples/cvat/exchange-oracle/src/core/manifest/shared.py index 089429cc94..1a61fc3774 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/manifest/shared.py +++ b/packages/examples/cvat/exchange-oracle/src/core/manifest/shared.py @@ -1,12 +1,11 @@ -from enum import Enum from typing import Annotated, Any, Literal from pydantic import BaseModel, Field, model_validator -from src.utils.enums import BetterEnumMeta +from src.utils.enums import BetterEnumMeta, StrEnum -class BucketProviders(str, Enum): +class BucketProviders(StrEnum): aws = "AWS" gcs = "GCS" @@ -32,7 +31,7 @@ class GcsBucketUrl(BucketUrlBase, BaseModel): BucketUrl = Annotated[AwsBucketUrl | GcsBucketUrl, Field(discriminator="provider")] -class LabelTypes(str, Enum, metaclass=BetterEnumMeta): +class LabelTypes(StrEnum, metaclass=BetterEnumMeta): plain = "plain" skeleton = "skeleton" diff --git a/packages/examples/cvat/exchange-oracle/src/core/manifest/v2.py b/packages/examples/cvat/exchange-oracle/src/core/manifest/v2.py index 010cdd4c89..4f0e99c6b3 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/manifest/v2.py +++ b/packages/examples/cvat/exchange-oracle/src/core/manifest/v2.py @@ -1,11 +1,10 @@ -from enum import Enum from typing import Any, Literal from pydantic import AnyUrl, BaseModel, ConfigDict, Field, model_validator from src.core.manifest.shared import BucketUrl, LabelInfo, LabelTypes from src.core.tasks import TaskTypes -from src.utils.enums import BetterEnumMeta +from src.utils.enums import BetterEnumMeta, StrEnum class DataInfo(BaseModel): @@ -34,7 +33,7 @@ class DataInfo(BaseModel): """ -class TargetMetrics(str, Enum, metaclass=BetterEnumMeta): +class TargetMetrics(StrEnum, metaclass=BetterEnumMeta): accuracy = "accuracy" wer = "wer" cer = "cer" @@ -161,9 +160,7 @@ def parse_task_specific_params(cls, values: dict[str, Any]) -> dict[str, Any]: raise NotImplementedError details_cls = ( - AudioJobDetails - if job_type == TaskTypes.audio_transcription - else ImageJobDetails + AudioJobDetails if job_type == TaskTypes.audio_transcription else ImageJobDetails ) annotation["details"] = details_cls.model_validate(details) diff --git a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py index e89f27ab98..1f7c45f2c3 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py +++ b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/meta.py @@ -11,12 +11,11 @@ import io import time from datetime import timedelta -from enum import Enum from attrs import Factory, frozen from datumaro.util import dump_json, parse_json -from src.utils.enums import BetterEnumMeta +from src.utils.enums import BetterEnumMeta, StrEnum CVAT_EXPORT_FORMAT = "Generic TSV 1.0" @@ -102,7 +101,7 @@ def parse_gt_tsv(data: bytes) -> list[InputGtRegion]: ] -class RegionKind(str, Enum, metaclass=BetterEnumMeta): +class RegionKind(StrEnum, metaclass=BetterEnumMeta): gt = "gt" # ground-truth honeypot region ds = "ds" # regular dataset region to annotate diff --git a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py index 1b5f12894d..3b0c91f272 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py +++ b/packages/examples/cvat/exchange-oracle/src/core/tasks/audio_transcription/spec.py @@ -1,7 +1,6 @@ from __future__ import annotations from datetime import timedelta -from enum import Enum from typing import TYPE_CHECKING from pydantic import AnyUrl, BaseModel, Field @@ -9,13 +8,13 @@ from src.core.manifest.shared import BucketUrl # noqa: TCH001 # runtime-needed by pydantic from src.core.manifest.v2 import TargetMetrics # noqa: TCH001 # runtime-needed by pydantic from src.core.tasks.errors import UnsupportedManifestError -from src.utils.enums import BetterEnumMeta +from src.utils.enums import BetterEnumMeta, StrEnum if TYPE_CHECKING: from src.core.manifest.v2 import JobManifest -class NormalizerPreset(str, Enum, metaclass=BetterEnumMeta): +class NormalizerPreset(StrEnum, metaclass=BetterEnumMeta): basic = "basic" # universal Unicode + case + whitespace fold # Tier-1 language presets (BASIC plus per-language rules) en = "en" diff --git a/packages/examples/cvat/exchange-oracle/src/core/tasks/errors.py b/packages/examples/cvat/exchange-oracle/src/core/tasks/errors.py index 68ddebabb1..6f002952f7 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/tasks/errors.py +++ b/packages/examples/cvat/exchange-oracle/src/core/tasks/errors.py @@ -1,2 +1,2 @@ class UnsupportedManifestError(Exception): - pass \ No newline at end of file + pass diff --git a/packages/examples/cvat/exchange-oracle/src/core/tasks/types.py b/packages/examples/cvat/exchange-oracle/src/core/tasks/types.py index 6912483e39..0233a8cc69 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/tasks/types.py +++ b/packages/examples/cvat/exchange-oracle/src/core/tasks/types.py @@ -1,9 +1,9 @@ -from enum import Enum +from strenum import StrEnum from src.utils.enums import BetterEnumMeta -class TaskTypes(str, Enum, metaclass=BetterEnumMeta): +class TaskTypes(StrEnum, metaclass=BetterEnumMeta): image_label_binary = "image_label_binary" image_points = "image_points" image_boxes = "image_boxes" diff --git a/packages/examples/cvat/exchange-oracle/src/core/types.py b/packages/examples/cvat/exchange-oracle/src/core/types.py index c138465b89..96a2c92459 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/types.py +++ b/packages/examples/cvat/exchange-oracle/src/core/types.py @@ -1,16 +1,16 @@ -from enum import Enum +from enum import IntEnum from src.core.config import Config -from src.utils.enums import BetterEnumMeta +from src.utils.enums import BetterEnumMeta, StrEnum -class Networks(int, Enum, metaclass=BetterEnumMeta): +class Networks(IntEnum, metaclass=BetterEnumMeta): polygon_mainnet = Config.polygon_mainnet.chain_id polygon_amoy = Config.polygon_amoy.chain_id localhost = Config.localhost.chain_id -class ProjectStatuses(str, Enum, metaclass=BetterEnumMeta): +class ProjectStatuses(StrEnum, metaclass=BetterEnumMeta): creation = "creation" annotation = "annotation" completed = "completed" @@ -20,58 +20,58 @@ class ProjectStatuses(str, Enum, metaclass=BetterEnumMeta): deleted = "deleted" -class TaskStatuses(str, Enum, metaclass=BetterEnumMeta): +class TaskStatuses(StrEnum, metaclass=BetterEnumMeta): annotation = "annotation" completed = "completed" -class JobStatuses(str, Enum, metaclass=BetterEnumMeta): +class JobStatuses(StrEnum, metaclass=BetterEnumMeta): new = "new" in_progress = "in progress" completed = "completed" -class OracleWebhookTypes(str, Enum, metaclass=BetterEnumMeta): +class OracleWebhookTypes(StrEnum, metaclass=BetterEnumMeta): exchange_oracle = "exchange_oracle" job_launcher = "job_launcher" recording_oracle = "recording_oracle" reputation_oracle = "reputation_oracle" -class ExchangeOracleEventTypes(str, Enum, metaclass=BetterEnumMeta): +class ExchangeOracleEventTypes(StrEnum, metaclass=BetterEnumMeta): escrow_failed = "escrow_failed" job_finished = "job_finished" escrow_cleaned = "escrow_cleaned" escrow_recorded = "escrow_recorded" -class JobLauncherEventTypes(str, Enum, metaclass=BetterEnumMeta): +class JobLauncherEventTypes(StrEnum, metaclass=BetterEnumMeta): escrow_created = "escrow_created" escrow_canceled = "escrow_canceled" -class RecordingOracleEventTypes(str, Enum, metaclass=BetterEnumMeta): +class RecordingOracleEventTypes(StrEnum, metaclass=BetterEnumMeta): job_completed = "job_completed" submission_rejected = "submission_rejected" -class ReputationOracleEventTypes(str, Enum, metaclass=BetterEnumMeta): +class ReputationOracleEventTypes(StrEnum, metaclass=BetterEnumMeta): escrow_completed = "escrow_completed" -class OracleWebhookStatuses(str, Enum, metaclass=BetterEnumMeta): +class OracleWebhookStatuses(StrEnum, metaclass=BetterEnumMeta): pending = "pending" completed = "completed" failed = "failed" -class CvatWebhookStatuses(str, Enum, metaclass=BetterEnumMeta): +class CvatWebhookStatuses(StrEnum, metaclass=BetterEnumMeta): pending = "pending" completed = "completed" failed = "failed" -class AssignmentStatuses(str, Enum, metaclass=BetterEnumMeta): +class AssignmentStatuses(StrEnum, metaclass=BetterEnumMeta): """ State changes: @@ -86,7 +86,7 @@ class AssignmentStatuses(str, Enum, metaclass=BetterEnumMeta): canceled = "canceled" -class EscrowValidationStatuses(str, Enum, metaclass=BetterEnumMeta): +class EscrowValidationStatuses(StrEnum, metaclass=BetterEnumMeta): awaiting = "awaiting" in_progress = "in_progress" completed = "completed" diff --git a/packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py b/packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py index 67252af867..bbd8977b22 100644 --- a/packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py +++ b/packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py @@ -6,7 +6,6 @@ from contextlib import contextmanager from contextvars import ContextVar from datetime import datetime, timedelta, timezone -from enum import Enum from http import HTTPStatus from io import BytesIO from pathlib import Path @@ -21,7 +20,7 @@ from httpx import URL from src.core.config import Config -from src.utils.enums import BetterEnumMeta +from src.utils.enums import BetterEnumMeta, StrEnum from src.utils.time import utcnow _NOTSET = object() @@ -34,21 +33,21 @@ class CVATException(Exception): """Indicates that CVAT API returned unexpected response""" -class RequestStatus(str, Enum, metaclass=BetterEnumMeta): +class RequestStatus(StrEnum, metaclass=BetterEnumMeta): QUEUED = "Queued" STARTED = "Started" FINISHED = "Finished" FAILED = "Failed" -class JobStatus(str, Enum, metaclass=BetterEnumMeta): +class JobStatus(StrEnum, metaclass=BetterEnumMeta): new = "new" in_progress = "in progress" rejected = "rejected" completed = "completed" -class LabelType(str, Enum, metaclass=BetterEnumMeta): +class LabelType(StrEnum, metaclass=BetterEnumMeta): tag = "tag" points = "points" rectangle = "rectangle" @@ -56,7 +55,7 @@ class LabelType(str, Enum, metaclass=BetterEnumMeta): interval = "interval" -class WebhookEventType(str, Enum, metaclass=BetterEnumMeta): +class WebhookEventType(StrEnum, metaclass=BetterEnumMeta): update_job = "update:job" ping = "ping" diff --git a/packages/examples/cvat/exchange-oracle/src/endpoints/filtering.py b/packages/examples/cvat/exchange-oracle/src/endpoints/filtering.py index e37e6e3b9f..e3f35b630b 100644 --- a/packages/examples/cvat/exchange-oracle/src/endpoints/filtering.py +++ b/packages/examples/cvat/exchange-oracle/src/endpoints/filtering.py @@ -1,6 +1,5 @@ from __future__ import annotations -from enum import Enum from typing import TYPE_CHECKING, Any, ClassVar, TypeVar import fastapi @@ -11,13 +10,15 @@ from fastapi_filter.contrib.sqlalchemy import Filter as _Filter from pydantic import BaseModel, Field, ValidationInfo, field_validator -from src.utils.enums import BetterEnumMeta +from src.utils.enums import BetterEnumMeta, StrEnum if TYPE_CHECKING: + from enum import Enum + from pydantic.fields import FieldInfo -class OrderingDirection(str, Enum, metaclass=BetterEnumMeta): +class OrderingDirection(StrEnum, metaclass=BetterEnumMeta): asc = "ASC" desc = "DESC" @@ -157,4 +158,4 @@ def _get_field_info(klass: type[BaseModel], field_name: str): return klass.model_fields[field_name] -__all__ = ["Filter", "FilterDepends", "with_prefix", "OrderingDirection"] +__all__ = ["Filter", "FilterDepends", "OrderingDirection", "with_prefix"] diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/validators/audio.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/validators/audio.py index d80701f0e9..0da92d0f1a 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/validators/audio.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_completion/validators/audio.py @@ -25,12 +25,9 @@ def _save_annotation_results(self) -> None: ) self.logger.debug( - f"Downloading annotations for the escrow " - f"(escrow_address={self.escrow_address})" - ) - cvat_job_annotations = download_job_annotations( - self.logger, CVAT_EXPORT_FORMAT, cvat_jobs + f"Downloading annotations for the escrow (escrow_address={self.escrow_address})" ) + cvat_job_annotations = download_job_annotations(self.logger, CVAT_EXPORT_FORMAT, cvat_jobs) result_files: list[FileDescriptor] = [ self._make_annotation_descriptor(job, cvat_job_annotations[job.cvat_id]) @@ -39,8 +36,7 @@ def _save_annotation_results(self) -> None: result_files.append(prepare_annotation_metafile(jobs=cvat_jobs)) self.logger.debug( - f"Recording annotations for the escrow " - f"(escrow_address={self.escrow_address})" + f"Recording annotations for the escrow (escrow_address={self.escrow_address})" ) upload_escrow_results( files=result_files, chain_id=self.chain_id, escrow_address=self.escrow_address diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py index 2af8610d9b..f9761024cd 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py @@ -67,7 +67,7 @@ def _bundle_regions( *, min_duration: timedelta, max_duration: timedelta, - can_bundle: Callable[[_RegionT, _RegionT], bool] = lambda _a, _b: True + can_bundle: Callable[[_RegionT, _RegionT], bool] = lambda _a, _b: True, ) -> list[tuple[timedelta, timedelta, list[_RegionT]]]: """ Bundle consecutive input regions (sorted by start) into groups >= min_duration; @@ -134,10 +134,10 @@ def select_honeypots( can_bundle=( # we can only bundle regions if they belong to the same span lambda a, b: a.span_id == b.span_id - ) + ), ) - rng = np.random.Generator(np.random.MT19937(random_seed)) # pin the rng used for stable results + rng = np.random.Generator(np.random.MT19937(random_seed)) # pin the rng used for stable results idxs = list(range(len(bundles))) rng.shuffle(idxs) n_select = ceil(val_fraction * len(bundles)) @@ -172,7 +172,7 @@ def plan_assignments( """ # 1. shuffle inputs - rng = np.random.Generator(np.random.MT19937(random_seed)) # pin the rng used for stable results + rng = np.random.Generator(np.random.MT19937(random_seed)) # pin the rng used for stable results def sort_key(r: PresentedRegion) -> tuple: return (r.source_filename, r.start) @@ -694,6 +694,7 @@ def _parse_gt_tsv(data: bytes) -> dict[str, list[InputGtRegion]]: # Helpers # --------------------------------------------------------------------------- # + def _validate_gt_regions( regions: list[InputGtRegion], *, @@ -759,6 +760,7 @@ def _bucket_key(prefix: str, filename: str) -> str: """Join a bucket prefix and a filename into a POSIX-style object key.""" return str(PurePosixPath(prefix, filename)) + def _make_cvat_audio_label_configuration( labels: list[str], *, @@ -799,5 +801,6 @@ def _shared_attr_spec(attr) -> dict: ) return label_config + def _get_span_duration(group: list[InputRegion | InputGtRegion]) -> timedelta: return group[-1].stop - group[0].start diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/audio.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/audio.py index 3fa578f445..930497fb23 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/audio.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_export/exporters/audio.py @@ -57,9 +57,7 @@ def export(self) -> None: rows: list[dict] = [] for job in jobs: rows.extend( - self._annotation_rows( - job_annotations[job.cvat_id], assignments_by_clip, attr_names - ) + self._annotation_rows(job_annotations[job.cvat_id], assignments_by_clip, attr_names) ) rows.sort(key=lambda r: (r["filename"], r["_start_s"])) merged = self._render_tsv([*_LEADING_COLUMNS, *attr_names], rows) @@ -132,20 +130,14 @@ def _annotation_rows( @staticmethod def _placed_at(assignment: Clip, clip_time_s: float) -> PlacedRegion | None: return next( - ( - p - for p in assignment.placed - if p.clip_start <= clip_time_s < p.clip_stop - ), + (p for p in assignment.placed if p.clip_start <= clip_time_s < p.clip_stop), None, ) @staticmethod def _render_tsv(columns: list[str], rows: list[dict]) -> bytes: buffer = io.StringIO() - writer = csv.DictWriter( - buffer, fieldnames=columns, delimiter="\t", extrasaction="ignore" - ) + writer = csv.DictWriter(buffer, fieldnames=columns, delimiter="\t", extrasaction="ignore") writer.writeheader() for global_id, row in enumerate(rows): writer.writerow({**row, "id": global_id}) diff --git a/packages/examples/cvat/exchange-oracle/src/schemas/exchange.py b/packages/examples/cvat/exchange-oracle/src/schemas/exchange.py index 32ed3bd870..bd611a36dd 100644 --- a/packages/examples/cvat/exchange-oracle/src/schemas/exchange.py +++ b/packages/examples/cvat/exchange-oracle/src/schemas/exchange.py @@ -1,12 +1,10 @@ from datetime import datetime -from enum import Enum from pydantic import BaseModel, Field -from strenum import StrEnum # added in python 3.11 from src.core.tasks import TaskTypes from src.core.types import Networks -from src.utils.enums import BetterEnumMeta +from src.utils.enums import BetterEnumMeta, StrEnum class JobStatuses(StrEnum, metaclass=BetterEnumMeta): @@ -42,7 +40,7 @@ class AssignmentIdRequest(BaseModel): assignment_id: str -class AssignmentStatuses(str, Enum, metaclass=BetterEnumMeta): +class AssignmentStatuses(StrEnum, metaclass=BetterEnumMeta): active = "active" validation = "validation" completed = "completed" diff --git a/packages/examples/cvat/exchange-oracle/src/services/exchange.py b/packages/examples/cvat/exchange-oracle/src/services/exchange.py index 8e629553f9..c5d18bb10a 100644 --- a/packages/examples/cvat/exchange-oracle/src/services/exchange.py +++ b/packages/examples/cvat/exchange-oracle/src/services/exchange.py @@ -78,8 +78,7 @@ def create_assignment( session, wallet_address=user.wallet_address, cvat_job_id=unassigned_job.cvat_id, - expires_at=utcnow() - + timedelta(seconds=get_assignment_timeout(manifest)), + expires_at=utcnow() + timedelta(seconds=get_assignment_timeout(manifest)), ) cvat_service.update_job_status(session, unassigned_job.id, status=JobStatuses.in_progress) diff --git a/packages/examples/cvat/exchange-oracle/src/services/webhook.py b/packages/examples/cvat/exchange-oracle/src/services/webhook.py index 98e86d8f9e..3988c6ed49 100644 --- a/packages/examples/cvat/exchange-oracle/src/services/webhook.py +++ b/packages/examples/cvat/exchange-oracle/src/services/webhook.py @@ -1,7 +1,6 @@ import datetime import uuid from collections.abc import Sequence -from enum import Enum from attrs import define from sqlalchemy import case, update @@ -14,11 +13,11 @@ from src.db.utils import ForUpdateParams from src.db.utils import maybe_for_update as _maybe_for_update from src.models.webhook import Webhook -from src.utils.enums import BetterEnumMeta +from src.utils.enums import BetterEnumMeta, StrEnum from src.utils.time import utcnow -class OracleWebhookDirectionTags(str, Enum, metaclass=BetterEnumMeta): +class OracleWebhookDirectionTags(StrEnum, metaclass=BetterEnumMeta): incoming = "incoming" outgoing = "outgoing" diff --git a/packages/examples/cvat/exchange-oracle/src/utils/enums.py b/packages/examples/cvat/exchange-oracle/src/utils/enums.py index d4c133b0e5..b069d48158 100644 --- a/packages/examples/cvat/exchange-oracle/src/utils/enums.py +++ b/packages/examples/cvat/exchange-oracle/src/utils/enums.py @@ -1,5 +1,12 @@ from enum import EnumMeta +from strenum import StrEnum # added in python 3.11 + +__all__ = [ + "BetterEnumMeta", + "StrEnum", +] + class BetterEnumMeta(EnumMeta): """ diff --git a/packages/examples/cvat/recording-oracle/poetry.lock b/packages/examples/cvat/recording-oracle/poetry.lock index b65a3af355..ffef5e6aaf 100644 --- a/packages/examples/cvat/recording-oracle/poetry.lock +++ b/packages/examples/cvat/recording-oracle/poetry.lock @@ -4078,6 +4078,22 @@ anyio = ">=3.4.0,<5" [package.extras] full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] +[[package]] +name = "strenum" +version = "0.4.15" +description = "An Enum that inherits from str." +optional = false +python-versions = "*" +files = [ + {file = "StrEnum-0.4.15-py3-none-any.whl", hash = "sha256:a30cda4af7cc6b5bf52c8055bc4bf4b2b6b14a93b574626da33df53cf7740659"}, + {file = "StrEnum-0.4.15.tar.gz", hash = "sha256:878fb5ab705442070e4dd1929bb5e2249511c0bcf2b0eeacf3bcd80875c82eff"}, +] + +[package.extras] +docs = ["myst-parser[linkify]", "sphinx", "sphinx-rtd-theme"] +release = ["twine"] +test = ["pylint", "pytest", "pytest-black", "pytest-cov", "pytest-pylint"] + [[package]] name = "tensorboardx" version = "2.6.2.2" @@ -4675,4 +4691,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.0" python-versions = "^3.10, <3.13" -content-hash = "7f165837785af6689800e00dfc6170fdaa1687cff3d83ed0c69c69accc8b058b" +content-hash = "677d6c5f1b1ced0f0a0365a765be4b611ea9ee532d6bba80bd57c57b80327e72" diff --git a/packages/examples/cvat/recording-oracle/pyproject.toml b/packages/examples/cvat/recording-oracle/pyproject.toml index c07840b9f1..eb7475b986 100644 --- a/packages/examples/cvat/recording-oracle/pyproject.toml +++ b/packages/examples/cvat/recording-oracle/pyproject.toml @@ -25,6 +25,7 @@ starlette = ">=0.40.0" # avoid the vulnerability with multipart/form-data cvat-sdk = "2.37.0" cryptography = "<44.0.0" # human-protocol-sdk -> pgpy dep requires cryptography < 45 human-protocol-sdk = "^7.3.1" +strenum = "^0.4.15" audio-qe = {path = "libs/audio_qe", develop = true} [tool.poetry.group.dev.dependencies] diff --git a/packages/examples/cvat/recording-oracle/src/core/manifest/shared.py b/packages/examples/cvat/recording-oracle/src/core/manifest/shared.py index 089429cc94..1a61fc3774 100644 --- a/packages/examples/cvat/recording-oracle/src/core/manifest/shared.py +++ b/packages/examples/cvat/recording-oracle/src/core/manifest/shared.py @@ -1,12 +1,11 @@ -from enum import Enum from typing import Annotated, Any, Literal from pydantic import BaseModel, Field, model_validator -from src.utils.enums import BetterEnumMeta +from src.utils.enums import BetterEnumMeta, StrEnum -class BucketProviders(str, Enum): +class BucketProviders(StrEnum): aws = "AWS" gcs = "GCS" @@ -32,7 +31,7 @@ class GcsBucketUrl(BucketUrlBase, BaseModel): BucketUrl = Annotated[AwsBucketUrl | GcsBucketUrl, Field(discriminator="provider")] -class LabelTypes(str, Enum, metaclass=BetterEnumMeta): +class LabelTypes(StrEnum, metaclass=BetterEnumMeta): plain = "plain" skeleton = "skeleton" diff --git a/packages/examples/cvat/recording-oracle/src/core/manifest/v2.py b/packages/examples/cvat/recording-oracle/src/core/manifest/v2.py index 010cdd4c89..4f0e99c6b3 100644 --- a/packages/examples/cvat/recording-oracle/src/core/manifest/v2.py +++ b/packages/examples/cvat/recording-oracle/src/core/manifest/v2.py @@ -1,11 +1,10 @@ -from enum import Enum from typing import Any, Literal from pydantic import AnyUrl, BaseModel, ConfigDict, Field, model_validator from src.core.manifest.shared import BucketUrl, LabelInfo, LabelTypes from src.core.tasks import TaskTypes -from src.utils.enums import BetterEnumMeta +from src.utils.enums import BetterEnumMeta, StrEnum class DataInfo(BaseModel): @@ -34,7 +33,7 @@ class DataInfo(BaseModel): """ -class TargetMetrics(str, Enum, metaclass=BetterEnumMeta): +class TargetMetrics(StrEnum, metaclass=BetterEnumMeta): accuracy = "accuracy" wer = "wer" cer = "cer" @@ -161,9 +160,7 @@ def parse_task_specific_params(cls, values: dict[str, Any]) -> dict[str, Any]: raise NotImplementedError details_cls = ( - AudioJobDetails - if job_type == TaskTypes.audio_transcription - else ImageJobDetails + AudioJobDetails if job_type == TaskTypes.audio_transcription else ImageJobDetails ) annotation["details"] = details_cls.model_validate(details) diff --git a/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/meta.py b/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/meta.py index e89f27ab98..1f7c45f2c3 100644 --- a/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/meta.py +++ b/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/meta.py @@ -11,12 +11,11 @@ import io import time from datetime import timedelta -from enum import Enum from attrs import Factory, frozen from datumaro.util import dump_json, parse_json -from src.utils.enums import BetterEnumMeta +from src.utils.enums import BetterEnumMeta, StrEnum CVAT_EXPORT_FORMAT = "Generic TSV 1.0" @@ -102,7 +101,7 @@ def parse_gt_tsv(data: bytes) -> list[InputGtRegion]: ] -class RegionKind(str, Enum, metaclass=BetterEnumMeta): +class RegionKind(StrEnum, metaclass=BetterEnumMeta): gt = "gt" # ground-truth honeypot region ds = "ds" # regular dataset region to annotate diff --git a/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/spec.py b/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/spec.py index 1b5f12894d..3b0c91f272 100644 --- a/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/spec.py +++ b/packages/examples/cvat/recording-oracle/src/core/tasks/audio_transcription/spec.py @@ -1,7 +1,6 @@ from __future__ import annotations from datetime import timedelta -from enum import Enum from typing import TYPE_CHECKING from pydantic import AnyUrl, BaseModel, Field @@ -9,13 +8,13 @@ from src.core.manifest.shared import BucketUrl # noqa: TCH001 # runtime-needed by pydantic from src.core.manifest.v2 import TargetMetrics # noqa: TCH001 # runtime-needed by pydantic from src.core.tasks.errors import UnsupportedManifestError -from src.utils.enums import BetterEnumMeta +from src.utils.enums import BetterEnumMeta, StrEnum if TYPE_CHECKING: from src.core.manifest.v2 import JobManifest -class NormalizerPreset(str, Enum, metaclass=BetterEnumMeta): +class NormalizerPreset(StrEnum, metaclass=BetterEnumMeta): basic = "basic" # universal Unicode + case + whitespace fold # Tier-1 language presets (BASIC plus per-language rules) en = "en" diff --git a/packages/examples/cvat/recording-oracle/src/core/tasks/types.py b/packages/examples/cvat/recording-oracle/src/core/tasks/types.py index 6912483e39..77e577b19e 100644 --- a/packages/examples/cvat/recording-oracle/src/core/tasks/types.py +++ b/packages/examples/cvat/recording-oracle/src/core/tasks/types.py @@ -1,9 +1,8 @@ -from enum import Enum -from src.utils.enums import BetterEnumMeta +from src.utils.enums import BetterEnumMeta, StrEnum -class TaskTypes(str, Enum, metaclass=BetterEnumMeta): +class TaskTypes(StrEnum, metaclass=BetterEnumMeta): image_label_binary = "image_label_binary" image_points = "image_points" image_boxes = "image_boxes" diff --git a/packages/examples/cvat/recording-oracle/src/core/types.py b/packages/examples/cvat/recording-oracle/src/core/types.py index 07f4b3e8aa..8716aa32b7 100644 --- a/packages/examples/cvat/recording-oracle/src/core/types.py +++ b/packages/examples/cvat/recording-oracle/src/core/types.py @@ -1,34 +1,34 @@ -from enum import Enum +from enum import IntEnum from src.core.config import Config -from src.utils.enums import BetterEnumMeta +from src.utils.enums import BetterEnumMeta, StrEnum -class Networks(int, Enum): +class Networks(IntEnum): polygon_mainnet = Config.polygon_mainnet.chain_id polygon_amoy = Config.polygon_amoy.chain_id localhost = Config.localhost.chain_id -class OracleWebhookTypes(str, Enum, metaclass=BetterEnumMeta): +class OracleWebhookTypes(StrEnum, metaclass=BetterEnumMeta): exchange_oracle = "exchange_oracle" recording_oracle = "recording_oracle" reputation_oracle = "reputation_oracle" -class OracleWebhookStatuses(str, Enum, metaclass=BetterEnumMeta): +class OracleWebhookStatuses(StrEnum, metaclass=BetterEnumMeta): pending = "pending" completed = "completed" failed = "failed" -class ExchangeOracleEventTypes(str, Enum, metaclass=BetterEnumMeta): +class ExchangeOracleEventTypes(StrEnum, metaclass=BetterEnumMeta): escrow_failed = "escrow_failed" job_finished = "job_finished" escrow_cleaned = "escrow_cleaned" escrow_recorded = "escrow_recorded" -class RecordingOracleEventTypes(str, Enum, metaclass=BetterEnumMeta): +class RecordingOracleEventTypes(StrEnum, metaclass=BetterEnumMeta): job_completed = "job_completed" submission_rejected = "submission_rejected" diff --git a/packages/examples/cvat/recording-oracle/src/services/webhook.py b/packages/examples/cvat/recording-oracle/src/services/webhook.py index 014c4651cd..1d04744b6d 100644 --- a/packages/examples/cvat/recording-oracle/src/services/webhook.py +++ b/packages/examples/cvat/recording-oracle/src/services/webhook.py @@ -1,7 +1,6 @@ import datetime import uuid from collections.abc import Sequence -from enum import Enum from attrs import define from sqlalchemy import case, update @@ -14,11 +13,11 @@ from src.db.utils import ForUpdateParams from src.db.utils import maybe_for_update as _maybe_for_update from src.models.webhook import Webhook -from src.utils.enums import BetterEnumMeta +from src.utils.enums import BetterEnumMeta, StrEnum from src.utils.time import utcnow -class OracleWebhookDirectionTags(str, Enum, metaclass=BetterEnumMeta): +class OracleWebhookDirectionTags(StrEnum, metaclass=BetterEnumMeta): incoming = "incoming" outgoing = "outgoing" diff --git a/packages/examples/cvat/recording-oracle/src/utils/enums.py b/packages/examples/cvat/recording-oracle/src/utils/enums.py index d4c133b0e5..b069d48158 100644 --- a/packages/examples/cvat/recording-oracle/src/utils/enums.py +++ b/packages/examples/cvat/recording-oracle/src/utils/enums.py @@ -1,5 +1,12 @@ from enum import EnumMeta +from strenum import StrEnum # added in python 3.11 + +__all__ = [ + "BetterEnumMeta", + "StrEnum", +] + class BetterEnumMeta(EnumMeta): """ From 2102b7c94629c358260f5e9eff334b4ca72030e4 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Tue, 14 Jul 2026 18:07:17 +0300 Subject: [PATCH 40/53] fix rebase --- .../cvat/recording-oracle/dockerfiles/test.ci.Dockerfile | 2 ++ .../cvat/recording-oracle/dockerfiles/test.dev.Dockerfile | 2 ++ .../tests/integration/services/test_validation_service.py | 5 ++++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/examples/cvat/recording-oracle/dockerfiles/test.ci.Dockerfile b/packages/examples/cvat/recording-oracle/dockerfiles/test.ci.Dockerfile index 1c39e52f2a..fb45f64890 100644 --- a/packages/examples/cvat/recording-oracle/dockerfiles/test.ci.Dockerfile +++ b/packages/examples/cvat/recording-oracle/dockerfiles/test.ci.Dockerfile @@ -8,6 +8,8 @@ RUN apt-get update -y && \ RUN pip install --no-cache 'poetry==1.8.5' +COPY libs ./libs + COPY pyproject.toml poetry.lock ./ RUN --mount=type=cache,target=/root/.cache \ diff --git a/packages/examples/cvat/recording-oracle/dockerfiles/test.dev.Dockerfile b/packages/examples/cvat/recording-oracle/dockerfiles/test.dev.Dockerfile index fe1143c473..6c8a6017eb 100644 --- a/packages/examples/cvat/recording-oracle/dockerfiles/test.dev.Dockerfile +++ b/packages/examples/cvat/recording-oracle/dockerfiles/test.dev.Dockerfile @@ -11,6 +11,8 @@ RUN apt-get update -y && \ RUN pip install --no-cache 'poetry==1.8.5' +COPY libs ./libs + COPY pyproject.toml poetry.lock ./ RUN --mount=type=cache,target=/root/.cache \ diff --git a/packages/examples/cvat/recording-oracle/tests/integration/services/test_validation_service.py b/packages/examples/cvat/recording-oracle/tests/integration/services/test_validation_service.py index 6531a526f1..cbd185026b 100644 --- a/packages/examples/cvat/recording-oracle/tests/integration/services/test_validation_service.py +++ b/packages/examples/cvat/recording-oracle/tests/integration/services/test_validation_service.py @@ -1176,7 +1176,9 @@ def patched_prepare_merged_dataset(self): with ( # base downloads the exchange-oracle result file; the merger (image) downloads GT mock.patch("src.handlers.completion.task_exporters.base.BucketAccessInfo.parse_obj"), - mock.patch("src.handlers.completion.task_exporters.base.make_cloud_client"), + mock.patch( + "src.handlers.completion.task_exporters.base.make_cloud_client" + ) as mock_base_make_cloud_client, mock.patch("src.handlers.completion.task_exporters.image.BucketAccessInfo.parse_obj"), mock.patch( "src.handlers.completion.task_exporters.image.make_cloud_client" @@ -1190,6 +1192,7 @@ def patched_prepare_merged_dataset(self): ), ): mock_make_cloud_client.return_value.download_file = mock.Mock(return_value=b"") + mock_base_make_cloud_client.return_value.download_file = mock.Mock(return_value=b"") annotation_meta = AnnotationMeta( jobs=[ From a0dde057ecfa8302c6192488623c3edec157e8cc Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Tue, 14 Jul 2026 18:09:03 +0300 Subject: [PATCH 41/53] remove extra files --- .../tests/unit/helpers/__init__.py | 0 .../unit/helpers/predefined_annotations.py | 36 ------------------- 2 files changed, 36 deletions(-) delete mode 100644 packages/examples/cvat/exchange-oracle/tests/unit/helpers/__init__.py delete mode 100644 packages/examples/cvat/exchange-oracle/tests/unit/helpers/predefined_annotations.py diff --git a/packages/examples/cvat/exchange-oracle/tests/unit/helpers/__init__.py b/packages/examples/cvat/exchange-oracle/tests/unit/helpers/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/examples/cvat/exchange-oracle/tests/unit/helpers/predefined_annotations.py b/packages/examples/cvat/exchange-oracle/tests/unit/helpers/predefined_annotations.py deleted file mode 100644 index 657b695e76..0000000000 --- a/packages/examples/cvat/exchange-oracle/tests/unit/helpers/predefined_annotations.py +++ /dev/null @@ -1,36 +0,0 @@ -raw_binary_annotations = { - "image": [ - {"name": "1.jpg", "tag": {"label": "dummy_label"}}, - {"name": "2.jpg", "tag": {"label": "dummy_label"}}, - {"name": "3.jpg", "tag": {"label": "dummy_label"}}, - ] -} -binary_annotations = [ - { - "url": "https://test.storage.googleapis.com/1.jpg", - "answers": [ - { - "tag": "dummy_label", - "assignee": "0x86e83d346041E8806e352681f3F14549C0d2BC61", - } - ], - }, - { - "url": "https://test.storage.googleapis.com/2.jpg", - "answers": [ - { - "tag": "dummy_label", - "assignee": "0x86e83d346041E8806e352681f3F14549C0d2BC61", - } - ], - }, - { - "url": "https://test.storage.googleapis.com/3.jpg", - "answers": [ - { - "tag": "dummy_label", - "assignee": "0x86e83d346041E8806e352681f3F14549C0d2BC61", - } - ], - }, -] From 0254d27324a9d09e8a7dd9a3c7b1355f3309651f Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Tue, 14 Jul 2026 19:07:14 +0300 Subject: [PATCH 42/53] Fix minio version conflict with boto3, https://github.com/minio/minio/issues/20845 --- packages/examples/cvat/exchange-oracle/docker-compose.test.yml | 2 +- packages/examples/cvat/recording-oracle/docker-compose.test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/examples/cvat/exchange-oracle/docker-compose.test.yml b/packages/examples/cvat/exchange-oracle/docker-compose.test.yml index ba527df3bc..1943955b12 100644 --- a/packages/examples/cvat/exchange-oracle/docker-compose.test.yml +++ b/packages/examples/cvat/exchange-oracle/docker-compose.test.yml @@ -28,7 +28,7 @@ services: minio: container_name: minio - image: minio/minio:RELEASE.2022-05-26T05-48-41Z + image: quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z ports: - 9001:9001 - 9000:9000 diff --git a/packages/examples/cvat/recording-oracle/docker-compose.test.yml b/packages/examples/cvat/recording-oracle/docker-compose.test.yml index 526fb2b7d0..c490cfcaa9 100644 --- a/packages/examples/cvat/recording-oracle/docker-compose.test.yml +++ b/packages/examples/cvat/recording-oracle/docker-compose.test.yml @@ -13,7 +13,7 @@ services: minio: container_name: minio - image: minio/minio:RELEASE.2022-05-26T05-48-41Z + image: minio/minio:RELEASE.2025-09-07T16-13-09Z environment: MINIO_ROOT_USER: dev MINIO_ROOT_PASSWORD: devdevdev From 27ff8b05dab7b6b7ca80a25083e22154f177b643 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Tue, 14 Jul 2026 19:23:40 +0300 Subject: [PATCH 43/53] Fix tests, add audio task creation tests --- .../exchange-oracle/src/core/manifest/v2.py | 3 + .../cloud/manifests/audio_manifest.json | 28 ++ .../tests/unit/test_audio_task_creation.py | 291 ++++++++++++++++++ .../recording-oracle/src/core/manifest/v2.py | 3 + 4 files changed, 325 insertions(+) create mode 100644 packages/examples/cvat/exchange-oracle/tests/assets/cloud/manifests/audio_manifest.json create mode 100644 packages/examples/cvat/exchange-oracle/tests/unit/test_audio_task_creation.py diff --git a/packages/examples/cvat/exchange-oracle/src/core/manifest/v2.py b/packages/examples/cvat/exchange-oracle/src/core/manifest/v2.py index 4f0e99c6b3..228986d72f 100644 --- a/packages/examples/cvat/exchange-oracle/src/core/manifest/v2.py +++ b/packages/examples/cvat/exchange-oracle/src/core/manifest/v2.py @@ -1,3 +1,4 @@ +from copy import deepcopy from typing import Any, Literal from pydantic import AnyUrl, BaseModel, ConfigDict, Field, model_validator @@ -139,6 +140,8 @@ def parse_task_specific_params(cls, values: dict[str, Any]) -> dict[str, Any]: if not isinstance(values, dict): raise NotImplementedError + values = deepcopy(values) # copy before mutating + job_type = values.get("job_type") annotation = values.get("annotation") diff --git a/packages/examples/cvat/exchange-oracle/tests/assets/cloud/manifests/audio_manifest.json b/packages/examples/cvat/exchange-oracle/tests/assets/cloud/manifests/audio_manifest.json new file mode 100644 index 0000000000..9818e2d1a7 --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/tests/assets/cloud/manifests/audio_manifest.json @@ -0,0 +1,28 @@ +{ + "version": 2, + "job_type": "audio_transcription", + "data": { + "media_url": "placeholder", + "regions_url": "placeholder", + "gt_url": "placeholder" + }, + "annotation": { + "description": "Audio transcription task", + "user_guide_url": "https://localhost/audio-transcription-guide.md", + "labels": [{ "name": "speech" }], + "shared_attributes": [], + "validation": { + "target_score": 0.5, + "target_metric": "wer" + }, + "qualifications": [], + "details": { + "max_time": 1800, + "normalizer": "basic", + "max_segment_duration": 120, + "min_gt_span_duration": 20, + "min_composition": { "gt": 2, "ds": 2 }, + "validation_overhead": 20 + } + } +} diff --git a/packages/examples/cvat/exchange-oracle/tests/unit/test_audio_task_creation.py b/packages/examples/cvat/exchange-oracle/tests/unit/test_audio_task_creation.py new file mode 100644 index 0000000000..e61b83b4b6 --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/tests/unit/test_audio_task_creation.py @@ -0,0 +1,291 @@ +import json +import subprocess +from pathlib import Path +from unittest.mock import Mock, patch + +import pytest + +import src.services.cvat as db_service +from src.core.config import Config +from src.core.manifest import parse_manifest +from src.core.storage import compose_data_bucket_prefix +from src.core.tasks import TaskTypes +from src.core.tasks.audio_transcription.meta import ( + RegionKind, + TaskMetaLayout, + TaskMetaSerializer, + parse_gt_tsv, +) +from src.core.types import Networks, TaskStatuses +from src.handlers.job_creation import handlers +from src.handlers.job_creation.builders.audio.transcription import _parse_regions_tsv +from src.services.cloud.utils import BucketAccessInfo, make_client + +from tests.utils.constants import ESCROW_ADDRESS +from tests.utils.setup_cvat import get_session + +CHAIN_ID = Networks.localhost.value + +MANIFEST_PATH = "tests/assets/cloud/manifests/audio_manifest.json" + + +def _ts(seconds: float) -> str: + """Render seconds as an ``H:MM:SS.ffffff`` timestamp (the region/GT TSV format).""" + h = int(seconds // 3600) + m = int((seconds % 3600) // 60) + s = seconds - h * 3600 - m * 60 + return f"{h}:{m:02d}:{s:09.6f}" + + +def _generate_wav(path: Path, *, duration_s: int) -> None: + """Synthesize a mono sine-tone WAV on the fly (ffmpeg is present in the test image).""" + subprocess.run( + [ + "ffmpeg", + "-y", + "-loglevel", + "error", + "-f", + "lavfi", + "-i", + f"sine=frequency=440:duration={duration_s}", + "-ar", + "48000", + "-ac", + "1", + str(path), + ], + check=True, + ) + + +def _make_bucket_url(path: str, *, bucket_name: str) -> dict: + cfg = Config.storage_config + + return { + "provider": cfg.provider.upper(), + "host_url": f"{cfg.get_scheme()}{cfg.endpoint_url}", + "bucket_name": bucket_name, + "path": path, + "access_key": cfg.access_key, + "secret_key": cfg.secret_key, + } + + +@pytest.fixture +def fxt_audio_transcription_input(tmp_path: Path): + """Factory: generate the media recordings + region/GT TSVs, upload them to the input bucket, and + return a manifest whose data URLs embed the minio connection details. The region/GT layout is + caller-provided so tests exercising different assignment counts can reuse this.""" + + input_bucket = "datasets" + input_prefix = "audio_test" + + media_duration_s = 120 + ds_len = 10.0 + ds_rois = { + "audio1.wav": [30.0, 45.0, 60.0, 75.0], + "audio2.wav": [30.0, 45.0, 60.0], + "audio3.wav": [10.0, 25.0, 40.0, 55.0], + "audio4.wav": [10.0, 25.0, 40.0], + "audio5.wav": [10.0, 25.0], + } + ds_rois_tsv = "filename\tstart\tstop\tlabel\n" + "".join( + f"{filename}\t{_ts(start)}\t{_ts(start + ds_len)}\tspeech\n" + for filename, starts in ds_rois.items() + for start in starts + ) + media_files = list(ds_rois) + + # 3 GT spans (the honeypot source) with 1, 2 and 3 regions respectively. Each span covers + # >= min_gt_span_duration=20s (first region start .. last region stop), and all sit after their + # recording's DS regions so GT and DS stay disjoint. + gt_rois = [ + ("s1", "audio1.wav", [(90.0, 112.0)]), + ("s2", "audio2.wav", [(90.0, 98.0), (106.0, 112.0)]), + ("s3", "audio3.wav", [(80.0, 86.0), (92.0, 98.0), (104.0, 110.0)]), + ] + gt_tsv = "filename\tstart\tstop\tlabel\ttext\tspan_id\n" + "".join( + f"{filename}\t{_ts(start)}\t{_ts(stop)}\tspeech\tgt {span_id} {roi_idx} text\t{span_id}\n" + for roi_idx, (span_id, filename, regions) in enumerate(gt_rois) + for start, stop in regions + ) + + with open(MANIFEST_PATH) as f: + manifest = json.load(f) + + manifest["data"] = { + "media_url": _make_bucket_url(f"{input_prefix}/media", bucket_name=input_bucket), + "regions_url": _make_bucket_url(f"{input_prefix}/regions.tsv", bucket_name=input_bucket), + "gt_url": _make_bucket_url(f"{input_prefix}/gt.tsv", bucket_name=input_bucket), + } + + try: + client = make_client(BucketAccessInfo.parse_obj(parse_manifest(manifest).data.media_url)) + for filename in media_files: + wav_path = tmp_path / filename + _generate_wav(wav_path, duration_s=media_duration_s) + client.create_file(f"{input_prefix}/media/{filename}", wav_path.read_bytes()) + + client.create_file(f"{input_prefix}/regions.tsv", ds_rois_tsv.encode()) + client.create_file(f"{input_prefix}/gt.tsv", gt_tsv.encode()) + + yield manifest, ds_rois_tsv, gt_tsv + finally: + client.remove_files(prefix=input_prefix) + + +def _make_cvat_api_mock() -> Mock: + """A cvat_api stand-in returning integer ids so the DB inserts succeed, and a fresh + task id per create_task call so the unique cvat_id constraint holds.""" + cvat_api = Mock() + cvat_api.create_project.return_value = Mock(id=1) + cvat_api.create_cvat_webhook.return_value = Mock(id=10) + cvat_api.create_cloudstorage.return_value = Mock(id=100) + + task_ids = iter(range(1000, 2000)) + cvat_api.create_task.side_effect = lambda *_a, **_kw: Mock( + id=next(task_ids), status=TaskStatuses.annotation.value + ) + return cvat_api + + +def cleanup_oracle_bucket(prefix: str = "/"): + storage_client = make_client(BucketAccessInfo.parse_obj(Config.storage_config)) + storage_client.remove_files(prefix=prefix) + + +@pytest.fixture +def post_cleanup_oracle_bucket(): + yield + cleanup_oracle_bucket() + + +@pytest.fixture +def pre_cleanup_oracle_bucket(): + cleanup_oracle_bucket() + + +@pytest.mark.usefixtures("pre_cleanup_oracle_bucket") +@pytest.mark.usefixtures("post_cleanup_oracle_bucket") +def test_create_audio_transcription_task(fxt_audio_transcription_input): + manifest, ds_rois_tsv, gt_tsv = fxt_audio_transcription_input + + cvat_api = _make_cvat_api_mock() + with ( + patch.object(handlers, "get_escrow_manifest", return_value=manifest), + patch("src.handlers.job_creation.builders.audio.transcription.cvat_api", cvat_api), + ): + handlers.create_task(ESCROW_ADDRESS, CHAIN_ID) + + # Check CVAT API calls + cvat_api.create_project.assert_called_once() + cvat_api.create_cvat_webhook.assert_called_once() + cvat_api.create_cloudstorage.assert_called_once() + + assignment_count = cvat_api.create_task.call_count + assert assignment_count == 4 + assert cvat_api.put_task_data.call_count == assignment_count + + # Check DB records + with get_session() as session: + project = db_service.get_project_by_escrow_address(session, ESCROW_ADDRESS) + assert project is not None + assert project.cvat_id == 1 + assert project.cvat_webhook_id == 10 + assert project.cvat_cloudstorage_id == 100 + assert project.job_type == TaskTypes.audio_transcription.value + assert project.chain_id == CHAIN_ID + + tasks = db_service.get_tasks_by_cvat_project_id(session, project.cvat_id) + assert len(tasks) == assignment_count + + # create_task only registers the escrow creation; it is finished later by the cron once + # CVAT confirms the jobs, so it is still active (finished_at is None) at this point. + escrow_creation = db_service.get_escrow_creation_by_escrow_address( + session, ESCROW_ADDRESS, CHAIN_ID, active=True + ) + assert escrow_creation.total_jobs == assignment_count + + # Check oracle bucket + storage_client = make_client(BucketAccessInfo.parse_obj(Config.storage_config)) + storage_prefix = compose_data_bucket_prefix(ESCROW_ADDRESS, CHAIN_ID) + storage_keys = set(storage_client.list_files(prefix=storage_prefix, trim_prefix=True)) + + layout = TaskMetaLayout() + clips = [k for k in storage_keys if k.startswith(f"{layout.CLIPS_DIR}/") and k.endswith(".wav")] + assert len(clips) == assignment_count + + for meta_file in ( + layout.REGIONS_TSV_FILENAME, + layout.GT_FILENAME, + layout.REGIONS_FILENAME, + layout.CLIPS_FILENAME, + layout.TASK_CLIPS_FILENAME, + ): + assert meta_file in storage_keys, f"missing {meta_file} in {sorted(storage_keys)}" + + # task_clips maps each created CVAT task id to its assignment clip id + task_clips = json.loads( + storage_client.download_file(f"{storage_prefix}/{layout.TASK_CLIPS_FILENAME}") + ) + assert len(task_clips) == assignment_count + + # Verify the input regions & GT were consumed as expected + + # The raw region/GT TSVs are copied verbatim to the oracle bucket for provenance. + assert ( + storage_client.download_file(f"{storage_prefix}/{layout.REGIONS_TSV_FILENAME}").decode() + == ds_rois_tsv + ) + assert storage_client.download_file(f"{storage_prefix}/{layout.GT_FILENAME}").decode() == gt_tsv + + # Expected input ROI ids, reconstructed from the fixture TSVs with the production parsers. + regions_by_file = _parse_regions_tsv(ds_rois_tsv.encode()) + input_media = set(regions_by_file) + expected_ds_ids = sorted(r.id for regions in regions_by_file.values() for r in regions) + + gt_rois = parse_gt_tsv(gt_tsv.encode()) + expected_gt_ids = {r.id for r in gt_rois} + span_by_gt_id = {r.id: r.span_id for r in gt_rois} + + serializer = TaskMetaSerializer() + presented = serializer.parse_regions( + storage_client.download_file(f"{storage_prefix}/{layout.REGIONS_FILENAME}") + ) + ds_regions = [r for r in presented if r.kind == RegionKind.ds] + gt_regions = [r for r in presented if r.kind == RegionKind.gt] + + # Presented regions reference only the input media + assert {r.source_filename for r in presented} <= input_media + + # Every input DS ROI is presented for annotation exactly once + presented_ds_ids = [region_id for r in ds_regions for region_id in r.input_ids] + assert sorted(presented_ds_ids) == expected_ds_ids + + # val_fraction=1.0 -> all GT regions become honeypots, each used once, bundled within one span + presented_gt_ids = [region_id for r in gt_regions for region_id in r.input_ids] + assert sorted(presented_gt_ids) == sorted(expected_gt_ids) + for r in gt_regions: + assert len({span_by_gt_id[region_id] for region_id in r.input_ids}) == 1 + + # Verify the assignment composition + clips = serializer.parse_clips( + storage_client.download_file(f"{storage_prefix}/{layout.CLIPS_FILENAME}") + ) + assert len(clips) == assignment_count + + placed_ds_ids: list[str] = [] + placed_gt_ids: list[str] = [] + for clip in clips: + kinds = {p.kind for p in clip.placed} + assert RegionKind.ds in kinds, "assignment has no DS work" + assert RegionKind.gt in kinds, "assignment has no GT honeypot" + for p in clip.placed: + (placed_ds_ids if p.kind == RegionKind.ds else placed_gt_ids).extend(p.input_ids) + + # Each DS ROI is annotated exactly once across all assignments. + assert sorted(placed_ds_ids) == expected_ds_ids + # Honeypots are drawn from the presented GT pool (and may repeat across assignments). + assert placed_gt_ids + assert set(placed_gt_ids) <= expected_gt_ids diff --git a/packages/examples/cvat/recording-oracle/src/core/manifest/v2.py b/packages/examples/cvat/recording-oracle/src/core/manifest/v2.py index 4f0e99c6b3..228986d72f 100644 --- a/packages/examples/cvat/recording-oracle/src/core/manifest/v2.py +++ b/packages/examples/cvat/recording-oracle/src/core/manifest/v2.py @@ -1,3 +1,4 @@ +from copy import deepcopy from typing import Any, Literal from pydantic import AnyUrl, BaseModel, ConfigDict, Field, model_validator @@ -139,6 +140,8 @@ def parse_task_specific_params(cls, values: dict[str, Any]) -> dict[str, Any]: if not isinstance(values, dict): raise NotImplementedError + values = deepcopy(values) # copy before mutating + job_type = values.get("job_type") annotation = values.get("annotation") From c0d87773a6ba15a7b351a2bb52c9e2811404bf1c Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Tue, 14 Jul 2026 20:46:04 +0300 Subject: [PATCH 44/53] cleanup --- .../recording-oracle/docker-compose.test.yml | 3 + .../recording-oracle/src/core/tasks/types.py | 1 - .../completion/task_exporters/audio.py | 4 +- .../validation/quality_checkers/audio.py | 5 +- .../services/test_validation_service.py | 60 +++- .../recording-oracle/tests/unit/conftest.py | 7 - .../tests/utils/intermediate-results.json | 29 -- .../utils/recording_oracle_dummy_input.json | 321 ------------------ 8 files changed, 51 insertions(+), 379 deletions(-) delete mode 100644 packages/examples/cvat/recording-oracle/tests/unit/conftest.py delete mode 100644 packages/examples/cvat/recording-oracle/tests/utils/intermediate-results.json delete mode 100644 packages/examples/cvat/recording-oracle/tests/utils/recording_oracle_dummy_input.json diff --git a/packages/examples/cvat/recording-oracle/docker-compose.test.yml b/packages/examples/cvat/recording-oracle/docker-compose.test.yml index c490cfcaa9..98aa584c28 100644 --- a/packages/examples/cvat/recording-oracle/docker-compose.test.yml +++ b/packages/examples/cvat/recording-oracle/docker-compose.test.yml @@ -14,6 +14,9 @@ services: minio: container_name: minio image: minio/minio:RELEASE.2025-09-07T16-13-09Z + ports: + - 9001:9001 + - 9000:9000 environment: MINIO_ROOT_USER: dev MINIO_ROOT_PASSWORD: devdevdev diff --git a/packages/examples/cvat/recording-oracle/src/core/tasks/types.py b/packages/examples/cvat/recording-oracle/src/core/tasks/types.py index 77e577b19e..0c65e58bcb 100644 --- a/packages/examples/cvat/recording-oracle/src/core/tasks/types.py +++ b/packages/examples/cvat/recording-oracle/src/core/tasks/types.py @@ -1,4 +1,3 @@ - from src.utils.enums import BetterEnumMeta, StrEnum diff --git a/packages/examples/cvat/recording-oracle/src/handlers/completion/task_exporters/audio.py b/packages/examples/cvat/recording-oracle/src/handlers/completion/task_exporters/audio.py index b16c51e137..9e6b01e1ee 100644 --- a/packages/examples/cvat/recording-oracle/src/handlers/completion/task_exporters/audio.py +++ b/packages/examples/cvat/recording-oracle/src/handlers/completion/task_exporters/audio.py @@ -66,9 +66,7 @@ def _build_merged_tsv(self) -> bytes: rows.sort(key=lambda r: (r["filename"], parse_time(r["start"]).total_seconds())) buffer = io.StringIO() - writer = csv.DictWriter( - buffer, fieldnames=columns, delimiter="\t", extrasaction="ignore" - ) + writer = csv.DictWriter(buffer, fieldnames=columns, delimiter="\t", extrasaction="ignore") writer.writeheader() for global_id, row in enumerate(rows): writer.writerow({**{c: "" for c in columns}, **row, "id": global_id}) diff --git a/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/audio.py b/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/audio.py index 27320ed9a2..fdfb29e093 100644 --- a/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/audio.py +++ b/packages/examples/cvat/recording-oracle/src/handlers/validation/quality_checkers/audio.py @@ -71,7 +71,7 @@ class AudioTaskQualityChecker(TaskQualityChecker): "granularity": Granularity.CHARACTER, "align": AlignMode.CHAR, "metric": Metric.EQUALITY, - } + }, } def _validate_jobs(self) -> None: @@ -81,8 +81,7 @@ def _validate_jobs(self) -> None: if metric not in self.ALLOWED_METRICS: raise NotImplementedError( "Audio transcription validation only supports {} metrics, got '{}'".format( - ', '.join(v.value for v in self.ALLOWED_METRICS), - metric.value + ", ".join(v.value for v in self.ALLOWED_METRICS), metric.value ) ) diff --git a/packages/examples/cvat/recording-oracle/tests/integration/services/test_validation_service.py b/packages/examples/cvat/recording-oracle/tests/integration/services/test_validation_service.py index cbd185026b..dc85dfe0ad 100644 --- a/packages/examples/cvat/recording-oracle/tests/integration/services/test_validation_service.py +++ b/packages/examples/cvat/recording-oracle/tests/integration/services/test_validation_service.py @@ -129,7 +129,9 @@ def test_can_handle_lowered_quality_requirements_in_manifest(self, session: Sess logger = mock.Mock(Logger) common_lock_es.enter_context( - mock.patch("src.handlers.completion.task_exporters.image.BucketAccessInfo.parse_obj") + mock.patch( + "src.handlers.completion.task_exporters.image.BucketAccessInfo.parse_obj" + ) ) common_lock_es.enter_context( @@ -156,7 +158,9 @@ def test_can_handle_lowered_quality_requirements_in_manifest(self, session: Sess ) mock_get_task_data_meta = common_lock_es.enter_context( - mock.patch("src.handlers.validation.quality_checkers.image.cvat_api.get_task_data_meta") + mock.patch( + "src.handlers.validation.quality_checkers.image.cvat_api.get_task_data_meta" + ) ) mock_get_task_data_meta.return_value = mock.Mock( cvat_api.models.IDataMetaRead, @@ -164,7 +168,9 @@ def test_can_handle_lowered_quality_requirements_in_manifest(self, session: Sess ) mock_get_task_labels = common_lock_es.enter_context( - mock.patch("src.handlers.validation.quality_checkers.image.cvat_api.get_task_labels") + mock.patch( + "src.handlers.validation.quality_checkers.image.cvat_api.get_task_labels" + ) ) mock_get_task_labels.return_value = [label.name for label in manifest.annotation.labels] @@ -333,7 +339,9 @@ def test_can_change_bad_honeypots_in_jobs(self, session: Session, seed: int): logger = mock.Mock(Logger) common_lock_es.enter_context( - mock.patch("src.handlers.completion.task_exporters.image.BucketAccessInfo.parse_obj") + mock.patch( + "src.handlers.completion.task_exporters.image.BucketAccessInfo.parse_obj" + ) ) mock_make_cloud_client = common_lock_es.enter_context( @@ -357,7 +365,9 @@ def test_can_change_bad_honeypots_in_jobs(self, session: Session, seed: int): ) mock_get_task_data_meta = common_lock_es.enter_context( - mock.patch("src.handlers.validation.quality_checkers.image.cvat_api.get_task_data_meta") + mock.patch( + "src.handlers.validation.quality_checkers.image.cvat_api.get_task_data_meta" + ) ) mock_get_task_data_meta.return_value = mock.Mock( cvat_api.models.IDataMetaRead, @@ -365,7 +375,9 @@ def test_can_change_bad_honeypots_in_jobs(self, session: Session, seed: int): ) mock_get_task_labels = common_lock_es.enter_context( - mock.patch("src.handlers.validation.quality_checkers.image.cvat_api.get_task_labels") + mock.patch( + "src.handlers.validation.quality_checkers.image.cvat_api.get_task_labels" + ) ) mock_get_task_labels.return_value = [label.name for label in manifest.annotation.labels] @@ -568,7 +580,9 @@ def test_can_stop_on_slow_annotation_after_warmup_iterations(self, session: Sess logger = mock.Mock(Logger) common_lock_es.enter_context( - mock.patch("src.handlers.completion.task_exporters.image.BucketAccessInfo.parse_obj") + mock.patch( + "src.handlers.completion.task_exporters.image.BucketAccessInfo.parse_obj" + ) ) mock_make_cloud_client = common_lock_es.enter_context( @@ -591,7 +605,9 @@ def test_can_stop_on_slow_annotation_after_warmup_iterations(self, session: Sess ) mock_get_task_data_meta = common_lock_es.enter_context( - mock.patch("src.handlers.validation.quality_checkers.image.cvat_api.get_task_data_meta") + mock.patch( + "src.handlers.validation.quality_checkers.image.cvat_api.get_task_data_meta" + ) ) mock_get_task_data_meta.return_value = mock.Mock( cvat_api.models.IDataMetaRead, @@ -599,7 +615,9 @@ def test_can_stop_on_slow_annotation_after_warmup_iterations(self, session: Sess ) mock_get_task_labels = common_lock_es.enter_context( - mock.patch("src.handlers.validation.quality_checkers.image.cvat_api.get_task_labels") + mock.patch( + "src.handlers.validation.quality_checkers.image.cvat_api.get_task_labels" + ) ) mock_get_task_labels.return_value = [label.name for label in manifest.annotation.labels] @@ -685,7 +703,9 @@ def test_can_exclude_bad_gt_for_each_label_separately(self, session: Session): logger = mock.Mock(Logger) common_lock_es.enter_context( - mock.patch("src.handlers.completion.task_exporters.image.BucketAccessInfo.parse_obj") + mock.patch( + "src.handlers.completion.task_exporters.image.BucketAccessInfo.parse_obj" + ) ) mock_make_cloud_client = common_lock_es.enter_context( @@ -710,7 +730,9 @@ def test_can_exclude_bad_gt_for_each_label_separately(self, session: Session): # All tasks have the same frames mock_get_task_data_meta = common_lock_es.enter_context( - mock.patch("src.handlers.validation.quality_checkers.image.cvat_api.get_task_data_meta") + mock.patch( + "src.handlers.validation.quality_checkers.image.cvat_api.get_task_data_meta" + ) ) mock_get_task_data_meta.return_value = mock.Mock( cvat_api.models.IDataMetaRead, @@ -866,7 +888,9 @@ def test_can_complete_if_not_enough_gt_left_in_task_with_several_jobs( logger = logging.getLogger() common_lock_es.enter_context( - mock.patch("src.handlers.completion.task_exporters.image.BucketAccessInfo.parse_obj") + mock.patch( + "src.handlers.completion.task_exporters.image.BucketAccessInfo.parse_obj" + ) ) mock_make_cloud_client = common_lock_es.enter_context( @@ -891,7 +915,9 @@ def test_can_complete_if_not_enough_gt_left_in_task_with_several_jobs( # All tasks have the same frames mock_get_task_data_meta = common_lock_es.enter_context( - mock.patch("src.handlers.validation.quality_checkers.image.cvat_api.get_task_data_meta") + mock.patch( + "src.handlers.validation.quality_checkers.image.cvat_api.get_task_data_meta" + ) ) mock_get_task_data_meta.return_value = mock.Mock( cvat_api.models.IDataMetaRead, @@ -1020,7 +1046,9 @@ def test_can_complete_if_not_enough_gt_left_in_task_with_one_job( logger = logging.getLogger() common_lock_es.enter_context( - mock.patch("src.handlers.completion.task_exporters.image.BucketAccessInfo.parse_obj") + mock.patch( + "src.handlers.completion.task_exporters.image.BucketAccessInfo.parse_obj" + ) ) mock_make_cloud_client = common_lock_es.enter_context( @@ -1045,7 +1073,9 @@ def test_can_complete_if_not_enough_gt_left_in_task_with_one_job( # All tasks have the same frames mock_get_task_data_meta = common_lock_es.enter_context( - mock.patch("src.handlers.validation.quality_checkers.image.cvat_api.get_task_data_meta") + mock.patch( + "src.handlers.validation.quality_checkers.image.cvat_api.get_task_data_meta" + ) ) mock_get_task_data_meta.return_value = mock.Mock( cvat_api.models.IDataMetaRead, diff --git a/packages/examples/cvat/recording-oracle/tests/unit/conftest.py b/packages/examples/cvat/recording-oracle/tests/unit/conftest.py deleted file mode 100644 index e83292c355..0000000000 --- a/packages/examples/cvat/recording-oracle/tests/unit/conftest.py +++ /dev/null @@ -1,7 +0,0 @@ -import pytest - - -@pytest.fixture(autouse=True) -def db(): - # Override the root autouse DB fixture: unit tests here need no database. - yield diff --git a/packages/examples/cvat/recording-oracle/tests/utils/intermediate-results.json b/packages/examples/cvat/recording-oracle/tests/utils/intermediate-results.json deleted file mode 100644 index c0bf2a08c2..0000000000 --- a/packages/examples/cvat/recording-oracle/tests/utils/intermediate-results.json +++ /dev/null @@ -1,29 +0,0 @@ -[ - { - "answers": [ - { - "assignee": "0x1234567890123456789012345678901234567890", - "tag": "dummy_label" - } - ], - "url": "https://test.storage.googleapis.com/1.jpg" - }, - { - "answers": [ - { - "assignee": "0x1234567890123456789012345678901234567890", - "tag": "dummy_label" - } - ], - "url": "https://test.storage.googleapis.com/2.jpg" - }, - { - "answers": [ - { - "assignee": "0x1234567890123456789012345678901234567890", - "tag": "dummy_label" - } - ], - "url": "https://test.storage.googleapis.com/3.jpg" - } -] diff --git a/packages/examples/cvat/recording-oracle/tests/utils/recording_oracle_dummy_input.json b/packages/examples/cvat/recording-oracle/tests/utils/recording_oracle_dummy_input.json deleted file mode 100644 index 872bc46ac6..0000000000 --- a/packages/examples/cvat/recording-oracle/tests/utils/recording_oracle_dummy_input.json +++ /dev/null @@ -1,321 +0,0 @@ -[ - { - "answers": [ - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b36", - "tag": "" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b37", - "tag": "" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b38", - "tag": "" - } - ], - "url": "https://test.storage.googleapis.com/1.png" - }, - { - "answers": [ - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b36", - "tag": "dummy_label" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b37", - "tag": "" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b38", - "tag": "" - } - ], - "url": "https://test.storage.googleapis.com/2.png" - }, - { - "answers": [ - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b36", - "tag": "dummy_label" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b37", - "tag": "" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b38", - "tag": "dummy_label" - } - ], - "url": "https://test.storage.googleapis.com/3.png" - }, - { - "answers": [ - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b36", - "tag": "" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b37", - "tag": "" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b38", - "tag": "" - } - ], - "url": "https://test.storage.googleapis.com/4.png" - }, - { - "answers": [ - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b36", - "tag": "dummy_label" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b37", - "tag": "dummy_label" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b38", - "tag": "dummy_label" - } - ], - "url": "https://test.storage.googleapis.com/5.png" - }, - { - "answers": [ - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b36", - "tag": "dummy_label" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b37", - "tag": "dummy_label" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b38", - "tag": "dummy_label" - } - ], - "url": "https://test.storage.googleapis.com/6.png" - }, - { - "answers": [ - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b36", - "tag": "dummy_label" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b37", - "tag": "dummy_label" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b38", - "tag": "dummy_label" - } - ], - "url": "https://test.storage.googleapis.com/7.png" - }, - { - "answers": [ - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b36", - "tag": "" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b37", - "tag": "" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b38", - "tag": "" - } - ], - "url": "https://test.storage.googleapis.com/8.png" - }, - { - "answers": [ - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b36", - "tag": "" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b37", - "tag": "" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b38", - "tag": "" - } - ], - "url": "https://test.storage.googleapis.com/9.png" - }, - { - "answers": [ - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b36", - "tag": "" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b37", - "tag": "" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b38", - "tag": "" - } - ], - "url": "https://test.storage.googleapis.com/10.png" - }, - { - "answers": [ - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b36", - "tag": "" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b37", - "tag": "dummy_label" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b38", - "tag": "" - } - ], - "url": "https://test.storage.googleapis.com/11.png" - }, - { - "answers": [ - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b36", - "tag": "dummy_label" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b37", - "tag": "dummy_label" - } - ], - "url": "https://test.storage.googleapis.com/12.png" - }, - { - "answers": [ - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b36", - "tag": "dummy_label" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b37", - "tag": "dummy_label" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b38", - "tag": "dummy_label" - } - ], - "url": "https://test.storage.googleapis.com/13.png" - }, - { - "answers": [ - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b36", - "tag": "dummy_label" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b37", - "tag": "dummy_label" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b38", - "tag": "dummy_label" - } - ], - "url": "https://test.storage.googleapis.com/14.png" - }, - { - "answers": [ - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b36", - "tag": "" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b37", - "tag": "" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b38", - "tag": "" - } - ], - "url": "https://test.storage.googleapis.com/15.png" - }, - { - "answers": [ - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b36", - "tag": "" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b37", - "tag": "" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b38", - "tag": "" - } - ], - "url": "https://test.storage.googleapis.com/16.png" - }, - { - "answers": [ - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b36", - "tag": "" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b37", - "tag": "" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b38", - "tag": "" - } - ], - "url": "https://test.storage.googleapis.com/17.png" - }, - { - "answers": [ - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b36", - "tag": "" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b37", - "tag": "dummy_label" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b38", - "tag": "" - } - ], - "url": "https://test.storage.googleapis.com/18.png" - }, - { - "answers": [ - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b36", - "tag": "" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b37", - "tag": "" - }, - { - "assignee": "0x0755D4d722a4a201c1C5A4B5E614D913e7747b38", - "tag": "dummy_label" - } - ], - "url": "https://test.storage.googleapis.com/19.png" - } -] \ No newline at end of file From 4aba5b1fc22a39b8f71b3e77040067ed09afcb46 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Tue, 14 Jul 2026 21:01:18 +0300 Subject: [PATCH 45/53] test(cvat-audio): add audio task-creation and validation tests Exchange oracle: unit test for audio-transcription task creation, asserting that input regions/GT are consumed as expected and each assignment mixes DS work with GT honeypots. Recording oracle: validation tests (pass + fail) driving the full process_intermediate_results flow against a golden fixture captured from the real EO builder. A perfect submission scores WER 0.0 (success); an empty one scores 1.0 and is rejected (failure). Supporting changes: - tests/utils/audio_transcription.py: shared helpers, kept byte-identical across both oracles. - tests/assets/utils/gen_audio_validation_fixture.py: maintenance script that runs the real EO builder and vendors its output as the RO golden fixture; runnable in the test container and importable without the test env for docs. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../examples/cvat/exchange-oracle/README.md | 34 ++++ .../cvat/exchange-oracle/pyproject.toml | 2 + .../cloud/manifests/audio_manifest.json | 8 +- .../utils/gen_audio_validation_fixture.py | 152 ++++++++++++++++ .../tests/unit/test_audio_task_creation.py | 49 ++---- .../tests/utils/audio_transcription.py | 108 ++++++++++++ .../cloud/audio_validation/assignments.json | 1 + .../assets/cloud/audio_validation/gt.tsv | 7 + .../cloud/audio_validation/manifest.json | 28 +++ .../cloud/audio_validation/task_clips.json | 1 + .../tests/unit/test_audio_validation.py | 162 ++++++++++++++++++ .../tests/utils/audio_transcription.py | 108 ++++++++++++ 12 files changed, 617 insertions(+), 43 deletions(-) create mode 100644 packages/examples/cvat/exchange-oracle/tests/assets/utils/gen_audio_validation_fixture.py create mode 100644 packages/examples/cvat/exchange-oracle/tests/utils/audio_transcription.py create mode 100644 packages/examples/cvat/recording-oracle/tests/assets/cloud/audio_validation/assignments.json create mode 100644 packages/examples/cvat/recording-oracle/tests/assets/cloud/audio_validation/gt.tsv create mode 100644 packages/examples/cvat/recording-oracle/tests/assets/cloud/audio_validation/manifest.json create mode 100644 packages/examples/cvat/recording-oracle/tests/assets/cloud/audio_validation/task_clips.json create mode 100644 packages/examples/cvat/recording-oracle/tests/unit/test_audio_validation.py create mode 100644 packages/examples/cvat/recording-oracle/tests/utils/audio_transcription.py diff --git a/packages/examples/cvat/exchange-oracle/README.md b/packages/examples/cvat/exchange-oracle/README.md index d0165e2a63..171880a651 100644 --- a/packages/examples/cvat/exchange-oracle/README.md +++ b/packages/examples/cvat/exchange-oracle/README.md @@ -115,3 +115,37 @@ docker compose -p "test" \ ``` The dev setup mounts the local directory to speed the things up. + +#### Regenerating the audio-validation fixture + +The recording oracle's audio-transcription validation test runs against a golden fixture — real +builder output (`gt.tsv`, `assignments.json`, `task_clips.json`, `manifest.json`) vendored under +`recording-oracle/tests/assets/cloud/audio_validation/`. Regenerate it whenever the builder output +layout or the shared audio task setup (`tests/utils/audio_transcription.py`) changes. + +The wrapper script `tests/assets/utils/gen_audio_validation_fixture.py` runs the real EO builder +once (against a mocked CVAT) and writes the fixture. Easiest is to run it inside the test-suite +container, which already has the env, minio and postgres — it overrides the service's `pytest` +command, applies migrations, and writes to a mounted output dir (the recording-oracle tree isn't +visible in the container, so mount it and pass its path): + +```sh +docker compose -p "test" \ + -f docker-compose.test.yml \ + -f docker-compose.test.head.yml \ + -f docker-compose.test.head.dev.yml \ + run --rm \ + -v "$(pwd)/../recording-oracle/tests/assets/cloud/audio_validation:/out" \ + test sh -c "alembic upgrade head && PYTHONPATH=. \ + python tests/assets/utils/gen_audio_validation_fixture.py /out" +``` + +To run it directly instead (against your own reachable minio + postgres, with the test-service env +exported and migrations applied): + +```sh +PYTHONPATH=. python tests/assets/utils/gen_audio_validation_fixture.py [OUTPUT_DIR] +``` + +`OUTPUT_DIR` defaults to the recording-oracle assets tree; pass a path to write elsewhere (e.g. a +scratch dir to diff before committing). diff --git a/packages/examples/cvat/exchange-oracle/pyproject.toml b/packages/examples/cvat/exchange-oracle/pyproject.toml index f1b82ac5f4..5dc9135894 100644 --- a/packages/examples/cvat/exchange-oracle/pyproject.toml +++ b/packages/examples/cvat/exchange-oracle/pyproject.toml @@ -142,6 +142,8 @@ ignore = [ ] # alembic is not a package in a traditional sense, so putting __init__.py there doesn't make sense "alembic/*" = ["INP001"] +# Standalone maintenance scripts: not importable packages (INP001) and print to stdout (T201). +"tests/assets/utils/*" = ["INP001", "T201"] "__init__.py" = ["F401"] [tool.ruff.lint.pep8-naming] diff --git a/packages/examples/cvat/exchange-oracle/tests/assets/cloud/manifests/audio_manifest.json b/packages/examples/cvat/exchange-oracle/tests/assets/cloud/manifests/audio_manifest.json index 9818e2d1a7..1b72a7f8cb 100644 --- a/packages/examples/cvat/exchange-oracle/tests/assets/cloud/manifests/audio_manifest.json +++ b/packages/examples/cvat/exchange-oracle/tests/assets/cloud/manifests/audio_manifest.json @@ -2,13 +2,13 @@ "version": 2, "job_type": "audio_transcription", "data": { - "media_url": "placeholder", - "regions_url": "placeholder", - "gt_url": "placeholder" + "media_url": "https://invalid/", + "regions_url": "https://invalid/", + "gt_url": "https://invalid/" }, "annotation": { "description": "Audio transcription task", - "user_guide_url": "https://localhost/audio-transcription-guide.md", + "user_guide_url": "https://invalid/audio-transcription-guide.md", "labels": [{ "name": "speech" }], "shared_attributes": [], "validation": { diff --git a/packages/examples/cvat/exchange-oracle/tests/assets/utils/gen_audio_validation_fixture.py b/packages/examples/cvat/exchange-oracle/tests/assets/utils/gen_audio_validation_fixture.py new file mode 100644 index 0000000000..79c0cc4b13 --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/tests/assets/utils/gen_audio_validation_fixture.py @@ -0,0 +1,152 @@ +"""Regenerate the recording-oracle audio-validation fixture. + +Recording Oracle validates audio-transcription assignments against the metadata the exchange oracle +produces during task creation. + +Run this script whenever the builder output layout or the shared audio task setup +(``tests.utils.audio_transcription``) changes. + +Easiest is to run it inside the test suite container. It reuses the test service, and writes the +fixture to a mounted output dir. Call from the exchange-oracle dir: + + docker compose -p test \ + -f docker-compose.test.yml \\ -f docker-compose.test.head.yml \\ -f + docker-compose.test.head.dev.yml \\ run --rm \\ -v + "$(pwd)/../recording-oracle/tests/assets/cloud/audio_validation:/out" \\ test sh -c "alembic + upgrade head && PYTHONPATH=. \ + python tests/assets/utils/gen_audio_validation_fixture.py /out" + +If you want to run it directly, launch the test services, export the same env vars the test service +sets (see docker-compose.test.head.yml), apply migrations (``alembic upgrade head``), then: + + PYTHONPATH=. python tests/assets/utils/gen_audio_validation_fixture.py [OUTPUT_DIR] +""" + +from __future__ import annotations + +import tempfile +from pathlib import Path + +INPUT_BUCKET = "datasets" +INPUT_PREFIX = "audio_validation_fixture" + +# The manifest the fixture is built from, vendored alongside it for the RO test. +MANIFEST_FILENAME = "manifest.json" + +# Default output: the RO assets tree, resolved relative to this file +# (exchange-oracle/tests/assets/utils/ -> recording-oracle/). Override with a positional arg. +DEFAULT_OUTPUT_DIR = ( + Path(__file__).resolve().parents[4] + / "recording-oracle" + / "tests" + / "assets" + / "cloud" + / "audio_validation" +) + + +def main(output_dir: Path = DEFAULT_OUTPUT_DIR) -> None: + # Imported lazily: src.core.config reads storage env vars at import time, so keeping these out + # of module scope lets --help / the docstring be read without the test-suite env defined. + import json + from unittest.mock import patch + + from src.core.config import Config + from src.core.manifest import parse_manifest + from src.core.storage import compose_data_bucket_filename, compose_data_bucket_prefix + from src.core.tasks.audio_transcription.meta import TaskMetaLayout + from src.core.types import Networks + from src.handlers.job_creation import handlers + from src.services.cloud.utils import BucketAccessInfo, make_client + + from tests.unit.test_audio_task_creation import ( + MANIFEST_PATH, + _generate_wav, + _make_bucket_url, + _make_cvat_api_mock, + ) + from tests.utils.audio_transcription import ( + MEDIA_DURATION_S, + MEDIA_FILES, + build_gt_tsv, + build_regions_tsv, + ) + from tests.utils.constants import ESCROW_ADDRESS + + chain_id = Networks.localhost.value + + # The builder-produced metadata the RO validation consumes. + fixture_files = ( + TaskMetaLayout.GT_FILENAME, + TaskMetaLayout.CLIPS_FILENAME, + TaskMetaLayout.TASK_CLIPS_FILENAME, + ) + + def data_url(name: str): + return _make_bucket_url(f"{INPUT_PREFIX}/{name}", bucket_name=INPUT_BUCKET) + + def build_manifest(): + with open(MANIFEST_PATH) as f: + manifest = json.load(f) + manifest["data"] = { + "media_url": data_url("media"), + "regions_url": data_url("regions.tsv"), + "gt_url": data_url("gt.tsv"), + } + return manifest + + def upload_input(tmp_dir: Path): + manifest = build_manifest() + client = make_client(BucketAccessInfo.parse_obj(parse_manifest(manifest).data.media_url)) + for filename in MEDIA_FILES: + wav_path = tmp_dir / filename + _generate_wav(wav_path, duration_s=MEDIA_DURATION_S) + client.create_file(f"{INPUT_PREFIX}/media/{filename}", wav_path.read_bytes()) + client.create_file(f"{INPUT_PREFIX}/regions.tsv", build_regions_tsv().encode()) + client.create_file(f"{INPUT_PREFIX}/gt.tsv", build_gt_tsv().encode()) + return manifest, client + + output_dir.mkdir(parents=True, exist_ok=True) + + with tempfile.TemporaryDirectory() as tmp: + manifest, input_client = upload_input(Path(tmp)) + + cvat_api = _make_cvat_api_mock() + with ( + patch.object(handlers, "get_escrow_manifest", return_value=manifest), + patch("src.handlers.job_creation.builders.audio.transcription.cvat_api", cvat_api), + ): + handlers.create_task(ESCROW_ADDRESS, chain_id) + + oracle_client = make_client(BucketAccessInfo.parse_obj(Config.storage_config)) + try: + for filename in fixture_files: + data = oracle_client.download_file( + compose_data_bucket_filename(ESCROW_ADDRESS, chain_id, filename) + ) + (output_dir / filename).write_bytes(data) + print(f"wrote {output_dir / filename} ({len(data)} bytes)") + + # Vendor the manifest the fixture was built from, so the RO validation runs against the + # exact same task config (metric, normalizer, tolerances) that produced it. + manifest_out = output_dir / MANIFEST_FILENAME + manifest_out.write_bytes(Path(MANIFEST_PATH).read_bytes()) + print(f"wrote {manifest_out} ({manifest_out.stat().st_size} bytes)") + finally: + # Best-effort cleanup of the buckets this run touched. + input_client.remove_files(prefix=INPUT_PREFIX) + oracle_client.remove_files(prefix=compose_data_bucket_prefix(ESCROW_ADDRESS, chain_id)) + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "output_dir", + nargs="?", + type=Path, + default=DEFAULT_OUTPUT_DIR, + help="directory to write the fixture into (default: the recording-oracle assets tree)", + ) + main(parser.parse_args().output_dir) diff --git a/packages/examples/cvat/exchange-oracle/tests/unit/test_audio_task_creation.py b/packages/examples/cvat/exchange-oracle/tests/unit/test_audio_task_creation.py index e61b83b4b6..1ca543d56c 100644 --- a/packages/examples/cvat/exchange-oracle/tests/unit/test_audio_task_creation.py +++ b/packages/examples/cvat/exchange-oracle/tests/unit/test_audio_task_creation.py @@ -21,6 +21,12 @@ from src.handlers.job_creation.builders.audio.transcription import _parse_regions_tsv from src.services.cloud.utils import BucketAccessInfo, make_client +from tests.utils.audio_transcription import ( + MEDIA_DURATION_S, + MEDIA_FILES, + build_gt_tsv, + build_regions_tsv, +) from tests.utils.constants import ESCROW_ADDRESS from tests.utils.setup_cvat import get_session @@ -29,14 +35,6 @@ MANIFEST_PATH = "tests/assets/cloud/manifests/audio_manifest.json" -def _ts(seconds: float) -> str: - """Render seconds as an ``H:MM:SS.ffffff`` timestamp (the region/GT TSV format).""" - h = int(seconds // 3600) - m = int((seconds % 3600) // 60) - s = seconds - h * 3600 - m * 60 - return f"{h}:{m:02d}:{s:09.6f}" - - def _generate_wav(path: Path, *, duration_s: int) -> None: """Synthesize a mono sine-tone WAV on the fly (ffmpeg is present in the test image).""" subprocess.run( @@ -81,35 +79,8 @@ def fxt_audio_transcription_input(tmp_path: Path): input_bucket = "datasets" input_prefix = "audio_test" - media_duration_s = 120 - ds_len = 10.0 - ds_rois = { - "audio1.wav": [30.0, 45.0, 60.0, 75.0], - "audio2.wav": [30.0, 45.0, 60.0], - "audio3.wav": [10.0, 25.0, 40.0, 55.0], - "audio4.wav": [10.0, 25.0, 40.0], - "audio5.wav": [10.0, 25.0], - } - ds_rois_tsv = "filename\tstart\tstop\tlabel\n" + "".join( - f"{filename}\t{_ts(start)}\t{_ts(start + ds_len)}\tspeech\n" - for filename, starts in ds_rois.items() - for start in starts - ) - media_files = list(ds_rois) - - # 3 GT spans (the honeypot source) with 1, 2 and 3 regions respectively. Each span covers - # >= min_gt_span_duration=20s (first region start .. last region stop), and all sit after their - # recording's DS regions so GT and DS stay disjoint. - gt_rois = [ - ("s1", "audio1.wav", [(90.0, 112.0)]), - ("s2", "audio2.wav", [(90.0, 98.0), (106.0, 112.0)]), - ("s3", "audio3.wav", [(80.0, 86.0), (92.0, 98.0), (104.0, 110.0)]), - ] - gt_tsv = "filename\tstart\tstop\tlabel\ttext\tspan_id\n" + "".join( - f"{filename}\t{_ts(start)}\t{_ts(stop)}\tspeech\tgt {span_id} {roi_idx} text\t{span_id}\n" - for roi_idx, (span_id, filename, regions) in enumerate(gt_rois) - for start, stop in regions - ) + ds_rois_tsv = build_regions_tsv() + gt_tsv = build_gt_tsv() with open(MANIFEST_PATH) as f: manifest = json.load(f) @@ -122,9 +93,9 @@ def fxt_audio_transcription_input(tmp_path: Path): try: client = make_client(BucketAccessInfo.parse_obj(parse_manifest(manifest).data.media_url)) - for filename in media_files: + for filename in MEDIA_FILES: wav_path = tmp_path / filename - _generate_wav(wav_path, duration_s=media_duration_s) + _generate_wav(wav_path, duration_s=MEDIA_DURATION_S) client.create_file(f"{input_prefix}/media/{filename}", wav_path.read_bytes()) client.create_file(f"{input_prefix}/regions.tsv", ds_rois_tsv.encode()) diff --git a/packages/examples/cvat/exchange-oracle/tests/utils/audio_transcription.py b/packages/examples/cvat/exchange-oracle/tests/utils/audio_transcription.py new file mode 100644 index 0000000000..b799c9b01f --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/tests/utils/audio_transcription.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +import math +from typing import TYPE_CHECKING + +from src.core.tasks.audio_transcription.meta import ( + RegionKind, + parse_gt_tsv, +) + +if TYPE_CHECKING: + from src.core.tasks.audio_transcription.meta import Clip, InputGtRegion + +# Canonical audio task input, shared by the task-creation and validation tests so both exercise +# the same layout. DS_ROIS: per-file region start times (each ROI is DS_LEN seconds long). +DS_LEN = 10.0 +DS_ROIS = { + "audio1.wav": [30.0, 45.0, 60.0, 75.0], + "audio2.wav": [30.0, 45.0, 60.0], + "audio3.wav": [10.0, 25.0, 40.0, 55.0], + "audio4.wav": [10.0, 25.0, 40.0], + "audio5.wav": [10.0, 25.0], +} +# (span_id, filename, [(start, stop), ...]) -- 3 GT spans with 1, 2 and 3 cues. Each span covers +# >= min_gt_span_duration and sits after its recording's DS regions, so GT and DS stay disjoint. +GT_ROIS = [ + ("s1", "audio1.wav", [(90.0, 112.0)]), + ("s2", "audio2.wav", [(90.0, 98.0), (106.0, 112.0)]), + ("s3", "audio3.wav", [(80.0, 86.0), (92.0, 98.0), (104.0, 110.0)]), +] +TRANSCRIPTION_ATTR = "transcription" + +# Media, regions and GT share one layout: the recordings must contain every DS/GT cue, so the +# file set and duration are derived from DS_ROIS/GT_ROIS rather than pinned independently. +MEDIA_MARGIN_S = 8.0 + + +def _region_stops() -> list[float]: + ds_stops = [start + DS_LEN for starts in DS_ROIS.values() for start in starts] + gt_stops = [stop for _span_id, _filename, regions in GT_ROIS for _start, stop in regions] + return ds_stops + gt_stops + + +MEDIA_FILES = sorted({*DS_ROIS, *(filename for _span_id, filename, _regions in GT_ROIS)}) +MEDIA_DURATION_S = math.ceil(max(_region_stops()) + MEDIA_MARGIN_S) + + +def ts(seconds: float) -> str: + """Render seconds as an ``H:MM:SS.ffffff`` timestamp (the region/GT/annotation TSV format).""" + h = int(seconds // 3600) + m = int((seconds % 3600) // 60) + s = seconds - h * 3600 - m * 60 + return f"{h}:{m:02d}:{s:09.6f}" + + +def build_regions_tsv(ds_rois: dict[str, list[float]] = DS_ROIS, ds_len: float = DS_LEN) -> str: + return "filename\tstart\tstop\tlabel\n" + "".join( + f"{filename}\t{ts(start)}\t{ts(start + ds_len)}\tspeech\n" + for filename, starts in ds_rois.items() + for start in starts + ) + + +def build_gt_tsv(gt_rois: list = GT_ROIS) -> str: + return "filename\tstart\tstop\tlabel\ttext\tspan_id\n" + "".join( + f"{filename}\t{ts(start)}\t{ts(stop)}\tspeech\tgt {span_id} {roi_idx} text\t{span_id}\n" + for roi_idx, (span_id, filename, regions) in enumerate(gt_rois) + for start, stop in regions + ) + + +def gt_regions_by_id(gt_tsv: str) -> dict[str, InputGtRegion]: + return {region.id: region for region in parse_gt_tsv(gt_tsv.encode())} + + +def build_perfect_annotation( + clip: Clip, + gt: dict[str, InputGtRegion], + *, + transcription_attr: str = TRANSCRIPTION_ATTR, +) -> str: + """Produce an annotator submission TSV that perfectly transcribes every honeypot in ``clip``. + + Each GT cue's source-time window is mapped back to clip time via its placement offset and + emitted with the GT text, so validation aligns it exactly (WER/CER == 0). + """ + rows: list[tuple[float, float, str]] = [] + for placed in clip.placed: + if placed.kind != RegionKind.gt: + continue + offset = placed.source_start - placed.clip_start # clip_time = source_time - offset + for region_id in placed.input_ids: + cue = gt[region_id] + rows.append( + (cue.start.total_seconds() - offset, cue.stop.total_seconds() - offset, cue.text) + ) + rows.sort() + body = "".join(f"{ts(start)}\t{ts(stop)}\tspeech\t{text}\n" for start, stop, text in rows) + return build_empty_annotation(transcription_attr) + body + + +def build_empty_annotation(transcription_attr: str = TRANSCRIPTION_ATTR) -> str: + """An annotator submission with no transcribed intervals. + + Every presented honeypot is then unmatched, so validation scores each as a full deletion + (error rate 1.0) -- i.e. a failing assignment. + """ + return f"start\tstop\tlabel\t{transcription_attr}\n" diff --git a/packages/examples/cvat/recording-oracle/tests/assets/cloud/audio_validation/assignments.json b/packages/examples/cvat/recording-oracle/tests/assets/cloud/audio_validation/assignments.json new file mode 100644 index 0000000000..0e7598abfb --- /dev/null +++ b/packages/examples/cvat/recording-oracle/tests/assets/cloud/audio_validation/assignments.json @@ -0,0 +1 @@ +[{"id":"21b0aeed1730488989aeeaf704cbcc35","clip_filename":"21b0aeed1730488989aeeaf704cbcc35.wav","clip_dur":55.5,"placed":[{"index":0,"source_filename":"audio2.wav","source_start":106.0,"source_stop":112.0,"clip_start":0.0,"clip_stop":6.0,"kind":"gt","input_ids":["audio2.wav:2"]},{"index":1,"source_filename":"audio3.wav","source_start":92.0,"source_stop":98.0,"clip_start":6.7,"clip_stop":12.7,"kind":"gt","input_ids":["audio3.wav:4"]},{"index":2,"source_filename":"audio4.wav","source_start":25.0,"source_stop":35.0,"clip_start":13.399999999999999,"clip_stop":23.4,"kind":"ds","input_ids":["audio4.wav:12"]},{"index":3,"source_filename":"audio1.wav","source_start":45.0,"source_stop":55.0,"clip_start":24.099999999999998,"clip_stop":34.099999999999994,"kind":"ds","input_ids":["audio1.wav:1"]},{"index":4,"source_filename":"audio4.wav","source_start":10.0,"source_stop":20.0,"clip_start":34.8,"clip_stop":44.8,"kind":"ds","input_ids":["audio4.wav:11"]},{"index":5,"source_filename":"audio5.wav","source_start":10.0,"source_stop":20.0,"clip_start":45.5,"clip_stop":55.5,"kind":"ds","input_ids":["audio5.wav:14"]}]},{"id":"384334a3a0c74a8d8b973a6cee6d7c40","clip_filename":"384334a3a0c74a8d8b973a6cee6d7c40.wav","clip_dur":55.50000000000001,"placed":[{"index":0,"source_filename":"audio3.wav","source_start":25.0,"source_stop":35.0,"clip_start":0.0,"clip_stop":10.0,"kind":"ds","input_ids":["audio3.wav:8"]},{"index":1,"source_filename":"audio1.wav","source_start":75.0,"source_stop":85.0,"clip_start":10.7,"clip_stop":20.7,"kind":"ds","input_ids":["audio1.wav:3"]},{"index":2,"source_filename":"audio1.wav","source_start":30.0,"source_stop":40.0,"clip_start":21.4,"clip_stop":31.4,"kind":"ds","input_ids":["audio1.wav:0"]},{"index":3,"source_filename":"audio4.wav","source_start":40.0,"source_stop":50.0,"clip_start":32.1,"clip_stop":42.1,"kind":"ds","input_ids":["audio4.wav:13"]},{"index":4,"source_filename":"audio3.wav","source_start":80.0,"source_stop":86.0,"clip_start":42.800000000000004,"clip_stop":48.800000000000004,"kind":"gt","input_ids":["audio3.wav:3"]},{"index":5,"source_filename":"audio3.wav","source_start":104.0,"source_stop":110.0,"clip_start":49.50000000000001,"clip_stop":55.50000000000001,"kind":"gt","input_ids":["audio3.wav:5"]}]},{"id":"eff82f2c73e3498b9368003af01373ba","clip_filename":"eff82f2c73e3498b9368003af01373ba.wav","clip_dur":73.50000000000001,"placed":[{"index":0,"source_filename":"audio3.wav","source_start":10.0,"source_stop":20.0,"clip_start":0.0,"clip_stop":10.0,"kind":"ds","input_ids":["audio3.wav:7"]},{"index":1,"source_filename":"audio1.wav","source_start":90.0,"source_stop":112.0,"clip_start":10.7,"clip_stop":32.7,"kind":"gt","input_ids":["audio1.wav:0"]},{"index":2,"source_filename":"audio2.wav","source_start":90.0,"source_stop":98.0,"clip_start":33.400000000000006,"clip_stop":41.400000000000006,"kind":"gt","input_ids":["audio2.wav:1"]},{"index":3,"source_filename":"audio2.wav","source_start":60.0,"source_stop":70.0,"clip_start":42.10000000000001,"clip_stop":52.10000000000001,"kind":"ds","input_ids":["audio2.wav:6"]},{"index":4,"source_filename":"audio3.wav","source_start":55.0,"source_stop":65.0,"clip_start":52.80000000000001,"clip_stop":62.80000000000001,"kind":"ds","input_ids":["audio3.wav:10"]},{"index":5,"source_filename":"audio2.wav","source_start":45.0,"source_stop":55.0,"clip_start":63.500000000000014,"clip_stop":73.50000000000001,"kind":"ds","input_ids":["audio2.wav:5"]}]},{"id":"c4d6975a85a846e4ba75ea9a7b2643c2","clip_filename":"c4d6975a85a846e4ba75ea9a7b2643c2.wav","clip_dur":57.5,"placed":[{"index":0,"source_filename":"audio2.wav","source_start":90.0,"source_stop":98.0,"clip_start":0.0,"clip_stop":8.0,"kind":"gt","input_ids":["audio2.wav:1"]},{"index":1,"source_filename":"audio5.wav","source_start":25.0,"source_stop":35.0,"clip_start":8.7,"clip_stop":18.7,"kind":"ds","input_ids":["audio5.wav:15"]},{"index":2,"source_filename":"audio3.wav","source_start":40.0,"source_stop":50.0,"clip_start":19.4,"clip_stop":29.4,"kind":"ds","input_ids":["audio3.wav:9"]},{"index":3,"source_filename":"audio1.wav","source_start":60.0,"source_stop":70.0,"clip_start":30.099999999999998,"clip_stop":40.099999999999994,"kind":"ds","input_ids":["audio1.wav:2"]},{"index":4,"source_filename":"audio2.wav","source_start":30.0,"source_stop":40.0,"clip_start":40.8,"clip_stop":50.8,"kind":"ds","input_ids":["audio2.wav:4"]},{"index":5,"source_filename":"audio3.wav","source_start":92.0,"source_stop":98.0,"clip_start":51.5,"clip_stop":57.5,"kind":"gt","input_ids":["audio3.wav:4"]}]}] \ No newline at end of file diff --git a/packages/examples/cvat/recording-oracle/tests/assets/cloud/audio_validation/gt.tsv b/packages/examples/cvat/recording-oracle/tests/assets/cloud/audio_validation/gt.tsv new file mode 100644 index 0000000000..1e76d815fc --- /dev/null +++ b/packages/examples/cvat/recording-oracle/tests/assets/cloud/audio_validation/gt.tsv @@ -0,0 +1,7 @@ +filename start stop label text span_id +audio1.wav 0:01:30.000000 0:01:52.000000 speech gt s1 0 text s1 +audio2.wav 0:01:30.000000 0:01:38.000000 speech gt s2 1 text s2 +audio2.wav 0:01:46.000000 0:01:52.000000 speech gt s2 1 text s2 +audio3.wav 0:01:20.000000 0:01:26.000000 speech gt s3 2 text s3 +audio3.wav 0:01:32.000000 0:01:38.000000 speech gt s3 2 text s3 +audio3.wav 0:01:44.000000 0:01:50.000000 speech gt s3 2 text s3 diff --git a/packages/examples/cvat/recording-oracle/tests/assets/cloud/audio_validation/manifest.json b/packages/examples/cvat/recording-oracle/tests/assets/cloud/audio_validation/manifest.json new file mode 100644 index 0000000000..1b72a7f8cb --- /dev/null +++ b/packages/examples/cvat/recording-oracle/tests/assets/cloud/audio_validation/manifest.json @@ -0,0 +1,28 @@ +{ + "version": 2, + "job_type": "audio_transcription", + "data": { + "media_url": "https://invalid/", + "regions_url": "https://invalid/", + "gt_url": "https://invalid/" + }, + "annotation": { + "description": "Audio transcription task", + "user_guide_url": "https://invalid/audio-transcription-guide.md", + "labels": [{ "name": "speech" }], + "shared_attributes": [], + "validation": { + "target_score": 0.5, + "target_metric": "wer" + }, + "qualifications": [], + "details": { + "max_time": 1800, + "normalizer": "basic", + "max_segment_duration": 120, + "min_gt_span_duration": 20, + "min_composition": { "gt": 2, "ds": 2 }, + "validation_overhead": 20 + } + } +} diff --git a/packages/examples/cvat/recording-oracle/tests/assets/cloud/audio_validation/task_clips.json b/packages/examples/cvat/recording-oracle/tests/assets/cloud/audio_validation/task_clips.json new file mode 100644 index 0000000000..f4d211ab50 --- /dev/null +++ b/packages/examples/cvat/recording-oracle/tests/assets/cloud/audio_validation/task_clips.json @@ -0,0 +1 @@ +{"1000":"21b0aeed1730488989aeeaf704cbcc35","1001":"384334a3a0c74a8d8b973a6cee6d7c40","1002":"eff82f2c73e3498b9368003af01373ba","1003":"c4d6975a85a846e4ba75ea9a7b2643c2"} \ No newline at end of file diff --git a/packages/examples/cvat/recording-oracle/tests/unit/test_audio_validation.py b/packages/examples/cvat/recording-oracle/tests/unit/test_audio_validation.py new file mode 100644 index 0000000000..a26303c221 --- /dev/null +++ b/packages/examples/cvat/recording-oracle/tests/unit/test_audio_validation.py @@ -0,0 +1,162 @@ +import json +import logging +from collections.abc import Callable +from pathlib import Path +from types import SimpleNamespace + +import pytest +from sqlalchemy.orm import Session + +from src.core.annotation_meta import AnnotationMeta, JobMeta +from src.core.config import Config +from src.core.manifest import parse_manifest +from src.core.storage import ( + compose_data_bucket_filename, + compose_data_bucket_prefix, + compose_results_bucket_filename, + compose_results_bucket_prefix, +) +from src.core.tasks.audio_transcription.meta import ( + Clip, + TaskMetaLayout, + TaskMetaSerializer, + TaskResultsLayout, +) +from src.core.types import Networks +from src.core.validation_results import ValidationFailure, ValidationSuccess +from src.handlers.validation import process_intermediate_results +from src.services.cloud import make_client as make_cloud_client +from src.services.cloud.utils import BucketAccessInfo + +from tests.utils.audio_transcription import ( + TRANSCRIPTION_ATTR, + build_empty_annotation, + build_perfect_annotation, + gt_regions_by_id, +) +from tests.utils.constants import ESCROW_ADDRESS, WALLET_ADDRESS1 + +CHAIN_ID = Networks.localhost.value + +# Real exchange-oracle builder output. Regenerate by +# exchange-oracle/tests/assets/utils/gen_audio_validation_fixture.py +FIXTURE_DIR = Path(__file__).resolve().parents[1] / "assets" / "cloud" / "audio_validation" + +MANIFEST_FILENAME = "manifest.json" + + +def _load_manifest(): + return parse_manifest(json.loads((FIXTURE_DIR / MANIFEST_FILENAME).read_text())) + + +def _cleanup_eo_bucket(): + client = make_cloud_client(BucketAccessInfo.parse_obj(Config.exchange_oracle_storage_config)) + client.remove_files(prefix=compose_data_bucket_prefix(ESCROW_ADDRESS, CHAIN_ID)) + client.remove_files(prefix=compose_results_bucket_prefix(ESCROW_ADDRESS, CHAIN_ID)) + + +@pytest.fixture +def pre_cleanup_eo_bucket(): + _cleanup_eo_bucket() + + +@pytest.fixture +def post_cleanup_eo_bucket(): + yield + _cleanup_eo_bucket() + + +@pytest.fixture +def fxt_published_task(pre_cleanup_eo_bucket): + """Publish the golden builder metadata to the exchange-oracle bucket the validator reads.""" + + client = make_cloud_client(BucketAccessInfo.parse_obj(Config.exchange_oracle_storage_config)) + + gt_tsv = (FIXTURE_DIR / TaskMetaLayout.GT_FILENAME).read_text() + clips_data = (FIXTURE_DIR / TaskMetaLayout.CLIPS_FILENAME).read_bytes() + task_clips_data = (FIXTURE_DIR / TaskMetaLayout.TASK_CLIPS_FILENAME).read_bytes() + + for filename, data in ( + (TaskMetaLayout.GT_FILENAME, gt_tsv.encode()), + (TaskMetaLayout.CLIPS_FILENAME, clips_data), + (TaskMetaLayout.TASK_CLIPS_FILENAME, task_clips_data), + ): + client.create_file(compose_data_bucket_filename(ESCROW_ADDRESS, CHAIN_ID, filename), data) + + serializer = TaskMetaSerializer() + return SimpleNamespace( + client=client, + clips_by_id={clip.id: clip for clip in serializer.parse_clips(clips_data)}, + task_clips=serializer.parse_task_clips(task_clips_data), + gt=gt_regions_by_id(gt_tsv), + ) + + +@pytest.mark.usefixtures("post_cleanup_eo_bucket") +class AudioValidationTest: + def _validate(self, session: Session, published_task, build_annotation: Callable[[Clip], str]): + """Upload one annotator submission per CVAT task and run the intermediate-results flow. + + ``build_annotation`` maps a clip to its submission TSV. Returns the validation result and + the job metas (their ``job_id`` keys the result). + """ + + jobs = [] + for job_id, (task_id, clip_id) in enumerate( + sorted(published_task.task_clips.items()), start=1 + ): + assignment_id = f"assignment-{job_id}" + published_task.client.create_file( + compose_results_bucket_filename( + ESCROW_ADDRESS, + CHAIN_ID, + TaskResultsLayout.assignment_annotation_filename(job_id, assignment_id), + ), + build_annotation(published_task.clips_by_id[clip_id]).encode(), + ) + jobs.append( + JobMeta( + job_id=job_id, + task_id=task_id, + annotator_wallet_address=WALLET_ADDRESS1, + assignment_id=assignment_id, + start_frame=0, + stop_frame=0, + ) + ) + + result = process_intermediate_results( + session=session, + escrow_address=ESCROW_ADDRESS, + chain_id=CHAIN_ID, + meta=AnnotationMeta(jobs=jobs), + manifest=_load_manifest(), + logger=logging.getLogger(__name__), + ) + + return result, jobs + + def test_can_pass_validation(self, session: Session, fxt_published_task): + result, jobs = self._validate( + session, + fxt_published_task, + lambda clip: build_perfect_annotation( + clip, fxt_published_task.gt, transcription_attr=TRANSCRIPTION_ATTR + ), + ) + + # No rejections -> the escrow completes with a 0.0 (perfect) score per job. + assert isinstance(result, ValidationSuccess) + assert result.job_results == {job.job_id: 0.0 for job in jobs} + + def test_can_fail_validation(self, session: Session, fxt_published_task): + result, jobs = self._validate( + session, + fxt_published_task, + lambda _clip: build_empty_annotation(TRANSCRIPTION_ATTR), + ) + + # Every honeypot is a full deletion -> rate 1.0 > the 0.5 target, so every job is rejected. + assert isinstance(result, ValidationFailure) + assert set(result.rejected_jobs) == {job.job_id for job in jobs} + assert result.job_results == {job.job_id: 1.0 for job in jobs} diff --git a/packages/examples/cvat/recording-oracle/tests/utils/audio_transcription.py b/packages/examples/cvat/recording-oracle/tests/utils/audio_transcription.py new file mode 100644 index 0000000000..b799c9b01f --- /dev/null +++ b/packages/examples/cvat/recording-oracle/tests/utils/audio_transcription.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +import math +from typing import TYPE_CHECKING + +from src.core.tasks.audio_transcription.meta import ( + RegionKind, + parse_gt_tsv, +) + +if TYPE_CHECKING: + from src.core.tasks.audio_transcription.meta import Clip, InputGtRegion + +# Canonical audio task input, shared by the task-creation and validation tests so both exercise +# the same layout. DS_ROIS: per-file region start times (each ROI is DS_LEN seconds long). +DS_LEN = 10.0 +DS_ROIS = { + "audio1.wav": [30.0, 45.0, 60.0, 75.0], + "audio2.wav": [30.0, 45.0, 60.0], + "audio3.wav": [10.0, 25.0, 40.0, 55.0], + "audio4.wav": [10.0, 25.0, 40.0], + "audio5.wav": [10.0, 25.0], +} +# (span_id, filename, [(start, stop), ...]) -- 3 GT spans with 1, 2 and 3 cues. Each span covers +# >= min_gt_span_duration and sits after its recording's DS regions, so GT and DS stay disjoint. +GT_ROIS = [ + ("s1", "audio1.wav", [(90.0, 112.0)]), + ("s2", "audio2.wav", [(90.0, 98.0), (106.0, 112.0)]), + ("s3", "audio3.wav", [(80.0, 86.0), (92.0, 98.0), (104.0, 110.0)]), +] +TRANSCRIPTION_ATTR = "transcription" + +# Media, regions and GT share one layout: the recordings must contain every DS/GT cue, so the +# file set and duration are derived from DS_ROIS/GT_ROIS rather than pinned independently. +MEDIA_MARGIN_S = 8.0 + + +def _region_stops() -> list[float]: + ds_stops = [start + DS_LEN for starts in DS_ROIS.values() for start in starts] + gt_stops = [stop for _span_id, _filename, regions in GT_ROIS for _start, stop in regions] + return ds_stops + gt_stops + + +MEDIA_FILES = sorted({*DS_ROIS, *(filename for _span_id, filename, _regions in GT_ROIS)}) +MEDIA_DURATION_S = math.ceil(max(_region_stops()) + MEDIA_MARGIN_S) + + +def ts(seconds: float) -> str: + """Render seconds as an ``H:MM:SS.ffffff`` timestamp (the region/GT/annotation TSV format).""" + h = int(seconds // 3600) + m = int((seconds % 3600) // 60) + s = seconds - h * 3600 - m * 60 + return f"{h}:{m:02d}:{s:09.6f}" + + +def build_regions_tsv(ds_rois: dict[str, list[float]] = DS_ROIS, ds_len: float = DS_LEN) -> str: + return "filename\tstart\tstop\tlabel\n" + "".join( + f"{filename}\t{ts(start)}\t{ts(start + ds_len)}\tspeech\n" + for filename, starts in ds_rois.items() + for start in starts + ) + + +def build_gt_tsv(gt_rois: list = GT_ROIS) -> str: + return "filename\tstart\tstop\tlabel\ttext\tspan_id\n" + "".join( + f"{filename}\t{ts(start)}\t{ts(stop)}\tspeech\tgt {span_id} {roi_idx} text\t{span_id}\n" + for roi_idx, (span_id, filename, regions) in enumerate(gt_rois) + for start, stop in regions + ) + + +def gt_regions_by_id(gt_tsv: str) -> dict[str, InputGtRegion]: + return {region.id: region for region in parse_gt_tsv(gt_tsv.encode())} + + +def build_perfect_annotation( + clip: Clip, + gt: dict[str, InputGtRegion], + *, + transcription_attr: str = TRANSCRIPTION_ATTR, +) -> str: + """Produce an annotator submission TSV that perfectly transcribes every honeypot in ``clip``. + + Each GT cue's source-time window is mapped back to clip time via its placement offset and + emitted with the GT text, so validation aligns it exactly (WER/CER == 0). + """ + rows: list[tuple[float, float, str]] = [] + for placed in clip.placed: + if placed.kind != RegionKind.gt: + continue + offset = placed.source_start - placed.clip_start # clip_time = source_time - offset + for region_id in placed.input_ids: + cue = gt[region_id] + rows.append( + (cue.start.total_seconds() - offset, cue.stop.total_seconds() - offset, cue.text) + ) + rows.sort() + body = "".join(f"{ts(start)}\t{ts(stop)}\tspeech\t{text}\n" for start, stop, text in rows) + return build_empty_annotation(transcription_attr) + body + + +def build_empty_annotation(transcription_attr: str = TRANSCRIPTION_ATTR) -> str: + """An annotator submission with no transcribed intervals. + + Every presented honeypot is then unmatched, so validation scores each as a full deletion + (error rate 1.0) -- i.e. a failing assignment. + """ + return f"start\tstop\tlabel\t{transcription_attr}\n" From b0f49e5339e486002b9f90549a48e8123d73169a Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Tue, 14 Jul 2026 21:11:18 +0300 Subject: [PATCH 46/53] Change the recommended compose project to avoid dev image clobber --- packages/examples/cvat/exchange-oracle/README.md | 12 ++++++------ .../cvat/exchange-oracle/docker-compose.dev.yml | 8 ++++---- .../assets/utils/gen_audio_validation_fixture.py | 2 +- packages/examples/cvat/recording-oracle/README.md | 10 +++++----- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/examples/cvat/exchange-oracle/README.md b/packages/examples/cvat/exchange-oracle/README.md index 171880a651..2af661be5d 100644 --- a/packages/examples/cvat/exchange-oracle/README.md +++ b/packages/examples/cvat/exchange-oracle/README.md @@ -76,11 +76,11 @@ Available at `/docs` route A single command to build, run, and tear down the test suite: ```sh -docker compose -p "test" \ +docker compose -p eo-test \ -f docker-compose.test.yml \ -f docker-compose.test.head.yml \ up --build test --attach test --exit-code-from test; \ - docker compose -p "test" \ + docker compose -p eo-test \ -f docker-compose.test.yml \ -f docker-compose.test.head.yml \ down @@ -95,19 +95,19 @@ commands allow running just the services, build, tear down, and run the test sui ```sh # run services -docker compose -p "test" \ +docker compose -p eo-test \ -f docker-compose.test.yml \ up -d --build # run the tests -docker compose -p "test" \ +docker compose -p eo-test \ -f docker-compose.test.yml \ -f docker-compose.test.head.yml \ -f docker-compose.test.head.dev.yml \ up --build test --attach test --exit-code-from test # tear down -docker compose -p "test" \ +docker compose -p eo-test \ -f docker-compose.test.yml \ -f docker-compose.test.head.yml \ -f docker-compose.test.head.dev.yml \ @@ -130,7 +130,7 @@ command, applies migrations, and writes to a mounted output dir (the recording-o visible in the container, so mount it and pass its path): ```sh -docker compose -p "test" \ +docker compose -p eo-test \ -f docker-compose.test.yml \ -f docker-compose.test.head.yml \ -f docker-compose.test.head.dev.yml \ diff --git a/packages/examples/cvat/exchange-oracle/docker-compose.dev.yml b/packages/examples/cvat/exchange-oracle/docker-compose.dev.yml index 065297be48..8c7d63e0b8 100644 --- a/packages/examples/cvat/exchange-oracle/docker-compose.dev.yml +++ b/packages/examples/cvat/exchange-oracle/docker-compose.dev.yml @@ -32,7 +32,7 @@ services: ports: - 6380:6379 networks: - - test-network + - oracle-network minio: container_name: minio @@ -54,7 +54,7 @@ services: timeout: 5s retries: 3 networks: - - test-network + - oracle-network - cvat-human-bridge minio-mc: @@ -78,7 +78,7 @@ services: /usr/bin/mc anonymous set public myminio/launcher; " networks: - - test-network + - oracle-network volumes: postgres: @@ -86,7 +86,7 @@ volumes: minio: networks: - test-network: + oracle-network: cvat-human-bridge: name: cvat-human-bridge external: true diff --git a/packages/examples/cvat/exchange-oracle/tests/assets/utils/gen_audio_validation_fixture.py b/packages/examples/cvat/exchange-oracle/tests/assets/utils/gen_audio_validation_fixture.py index 79c0cc4b13..1602908a24 100644 --- a/packages/examples/cvat/exchange-oracle/tests/assets/utils/gen_audio_validation_fixture.py +++ b/packages/examples/cvat/exchange-oracle/tests/assets/utils/gen_audio_validation_fixture.py @@ -9,7 +9,7 @@ Easiest is to run it inside the test suite container. It reuses the test service, and writes the fixture to a mounted output dir. Call from the exchange-oracle dir: - docker compose -p test \ + docker compose -p eo-test \ -f docker-compose.test.yml \\ -f docker-compose.test.head.yml \\ -f docker-compose.test.head.dev.yml \\ run --rm \\ -v "$(pwd)/../recording-oracle/tests/assets/cloud/audio_validation:/out" \\ test sh -c "alembic diff --git a/packages/examples/cvat/recording-oracle/README.md b/packages/examples/cvat/recording-oracle/README.md index d32bdf67ec..1b187b65dc 100644 --- a/packages/examples/cvat/recording-oracle/README.md +++ b/packages/examples/cvat/recording-oracle/README.md @@ -72,11 +72,11 @@ Available at `/docs` route A single command to build, run, and tear down the test suite: ```sh -docker compose -p "test" \ +docker compose -p ro-test \ -f docker-compose.test.yml \ -f docker-compose.test.head.yml \ up --build test --attach test --exit-code-from test; \ - docker compose -p "test" \ + docker compose -p ro-test \ -f docker-compose.test.yml \ -f docker-compose.test.head.yml down ``` @@ -90,19 +90,19 @@ commands allow running just the services, build, tear down, and run the test sui ```sh # run services -docker compose -p "test" \ +docker compose -p ro-test \ -f docker-compose.test.yml \ up -d --build # run the tests -docker compose -p "test" \ +docker compose -p ro-test \ -f docker-compose.test.yml \ -f docker-compose.test.head.yml \ -f docker-compose.test.head.dev.yml \ up --build test --attach test --exit-code-from test # tear down -docker compose -p "test" \ +docker compose -p ro-test \ -f docker-compose.test.yml \ -f docker-compose.test.head.yml \ -f docker-compose.test.head.dev.yml \ From 4af197df4205e4a6159267cd7721ca403e7cff8c Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Wed, 15 Jul 2026 11:20:42 +0300 Subject: [PATCH 47/53] Update readme --- .../examples/cvat/exchange-oracle/README.md | 24 +++---------------- .../examples/cvat/recording-oracle/README.md | 16 +++++++++++++ 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/packages/examples/cvat/exchange-oracle/README.md b/packages/examples/cvat/exchange-oracle/README.md index 2af661be5d..9c8fc45ecd 100644 --- a/packages/examples/cvat/exchange-oracle/README.md +++ b/packages/examples/cvat/exchange-oracle/README.md @@ -116,18 +116,10 @@ docker compose -p eo-test \ The dev setup mounts the local directory to speed the things up. -#### Regenerating the audio-validation fixture +#### Regenerating the test fixtures -The recording oracle's audio-transcription validation test runs against a golden fixture — real -builder output (`gt.tsv`, `assignments.json`, `task_clips.json`, `manifest.json`) vendored under -`recording-oracle/tests/assets/cloud/audio_validation/`. Regenerate it whenever the builder output -layout or the shared audio task setup (`tests/utils/audio_transcription.py`) changes. - -The wrapper script `tests/assets/utils/gen_audio_validation_fixture.py` runs the real EO builder -once (against a mocked CVAT) and writes the fixture. Easiest is to run it inside the test-suite -container, which already has the env, minio and postgres — it overrides the service's `pytest` -command, applies migrations, and writes to a mounted output dir (the recording-oracle tree isn't -visible in the container, so mount it and pass its path): +Recording Oracle tests run against the fixtures produced by the Exchange Oracle implementation. +Regenerate them whenever the builder output layout or the shared task setups change. ```sh docker compose -p eo-test \ @@ -139,13 +131,3 @@ docker compose -p eo-test \ test sh -c "alembic upgrade head && PYTHONPATH=. \ python tests/assets/utils/gen_audio_validation_fixture.py /out" ``` - -To run it directly instead (against your own reachable minio + postgres, with the test-service env -exported and migrations applied): - -```sh -PYTHONPATH=. python tests/assets/utils/gen_audio_validation_fixture.py [OUTPUT_DIR] -``` - -`OUTPUT_DIR` defaults to the recording-oracle assets tree; pass a path to write elsewhere (e.g. a -scratch dir to diff before committing). diff --git a/packages/examples/cvat/recording-oracle/README.md b/packages/examples/cvat/recording-oracle/README.md index 1b187b65dc..f9f3adc68f 100644 --- a/packages/examples/cvat/recording-oracle/README.md +++ b/packages/examples/cvat/recording-oracle/README.md @@ -110,3 +110,19 @@ docker compose -p ro-test \ ``` The dev setup mounts the local directory to speed the things up. + +#### Regenerating the test fixtures + +Recording Oracle tests run against the fixtures produced by the Exchange Oracle implementation. +Regenerate them whenever the builder output layout or the shared task setups change. + +```sh +cd ../exchange-oracle && docker compose -p eo-test \ + -f docker-compose.test.yml \ + -f docker-compose.test.head.yml \ + -f docker-compose.test.head.dev.yml \ + run --rm \ + -v "$(pwd)/../recording-oracle/tests/assets/cloud/audio_validation:/out" \ + test sh -c "alembic upgrade head && PYTHONPATH=. \ + python tests/assets/utils/gen_audio_validation_fixture.py /out" +``` From cbbad7c89bc47d65cfcf0851e99cba119c967adc Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Wed, 15 Jul 2026 11:30:24 +0300 Subject: [PATCH 48/53] Bump cvat sdk --- .../examples/cvat/exchange-oracle/poetry.lock | 8 +-- .../cvat/exchange-oracle/pyproject.toml | 2 +- .../cvat/recording-oracle/poetry.lock | 58 ++----------------- .../cvat/recording-oracle/pyproject.toml | 2 +- 4 files changed, 12 insertions(+), 58 deletions(-) diff --git a/packages/examples/cvat/exchange-oracle/poetry.lock b/packages/examples/cvat/exchange-oracle/poetry.lock index 48e1008447..9bc4781f0d 100644 --- a/packages/examples/cvat/exchange-oracle/poetry.lock +++ b/packages/examples/cvat/exchange-oracle/poetry.lock @@ -922,13 +922,13 @@ test-randomorder = ["pytest-randomly"] [[package]] name = "cvat-sdk" -version = "2.69.0" +version = "2.70.0" description = "Software Development Kit for CVAT" optional = false python-versions = ">=3.10" files = [ - {file = "cvat_sdk-2.69.0-py3-none-any.whl", hash = "sha256:f239d3cb609f45e4fc1d7cce1e49fe920d515be19058648fb79eba923ba0023e"}, - {file = "cvat_sdk-2.69.0.tar.gz", hash = "sha256:799ff1a763e1444a515edafcd162b7ab2fa89638211872bad139c63796a9dc7a"}, + {file = "cvat_sdk-2.70.0-py3-none-any.whl", hash = "sha256:6098b01bda86433d96e177cfce5e28dce2c19f7c0bd3b0e814b8d7e45b8e1a46"}, + {file = "cvat_sdk-2.70.0.tar.gz", hash = "sha256:4d9304bd8334b05b5e5504466cedf1f538a667f5274544398dec78bba4b47e83"}, ] [package.dependencies] @@ -5561,4 +5561,4 @@ propcache = ">=0.2.1" [metadata] lock-version = "2.0" python-versions = "^3.10,<3.13" -content-hash = "e9469d22ecf7b753946d0a9a78b8e597ef90123dde33916b2ec5c0a6571081af" +content-hash = "843f77239edc1007d9c112b8555d0e7043b08d083f046a6d76ccaf1adeb5ded6" diff --git a/packages/examples/cvat/exchange-oracle/pyproject.toml b/packages/examples/cvat/exchange-oracle/pyproject.toml index 5dc9135894..adb6f4071f 100644 --- a/packages/examples/cvat/exchange-oracle/pyproject.toml +++ b/packages/examples/cvat/exchange-oracle/pyproject.toml @@ -14,7 +14,7 @@ psycopg2 = "^2.9.6" sqlalchemy-utils = "^0.41.1" alembic = "^1.11.1" httpx = "^0.24.1" -cvat-sdk = "2.69.0" +cvat-sdk = "2.70.0" sqlalchemy = "^2.0.16" apscheduler = "^3.10.1" xmltodict = "^0.13.0" diff --git a/packages/examples/cvat/recording-oracle/poetry.lock b/packages/examples/cvat/recording-oracle/poetry.lock index ffef5e6aaf..f075fdbc6f 100644 --- a/packages/examples/cvat/recording-oracle/poetry.lock +++ b/packages/examples/cvat/recording-oracle/poetry.lock @@ -875,13 +875,13 @@ test-randomorder = ["pytest-randomly"] [[package]] name = "cvat-sdk" -version = "2.37.0" -description = "CVAT REST API" +version = "2.70.0" +description = "Software Development Kit for CVAT" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" files = [ - {file = "cvat_sdk-2.37.0-py3-none-any.whl", hash = "sha256:faa94cfd6678089814179a8da828761dfa3daf08eb752490ee85551a1045dac5"}, - {file = "cvat_sdk-2.37.0.tar.gz", hash = "sha256:e990908a473c499eb6d7b84f7f2e640ea729ef027d4c4cc32a5a925752532689"}, + {file = "cvat_sdk-2.70.0-py3-none-any.whl", hash = "sha256:6098b01bda86433d96e177cfce5e28dce2c19f7c0bd3b0e814b8d7e45b8e1a46"}, + {file = "cvat_sdk-2.70.0.tar.gz", hash = "sha256:4d9304bd8334b05b5e5504466cedf1f538a667f5274544398dec78bba4b47e83"}, ] [package.dependencies] @@ -890,9 +890,7 @@ packaging = ">=21.3" Pillow = ">=10.3.0" platformdirs = ">=2.1.0" python_dateutil = ">=2.5.3" -setuptools = ">=21.0.0" tqdm = ">=4.64.0" -tuspy = "0.2.5" typing_extensions = ">=4.2.0" urllib3 = ">=1.25.3" @@ -1545,17 +1543,6 @@ files = [ {file = "frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817"}, ] -[[package]] -name = "future" -version = "1.0.0" -description = "Clean single-source support for Python 3 and 2" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216"}, - {file = "future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05"}, -] - [[package]] name = "google-api-core" version = "2.17.1" @@ -4110,17 +4097,6 @@ numpy = "*" packaging = "*" protobuf = ">=3.20" -[[package]] -name = "tinydb" -version = "4.8.2" -description = "TinyDB is a tiny, document oriented database optimized for your happiness :)" -optional = false -python-versions = "<4.0,>=3.8" -files = [ - {file = "tinydb-4.8.2-py3-none-any.whl", hash = "sha256:f97030ee5cbc91eeadd1d7af07ab0e48ceb04aa63d4a983adbaca4cba16e86c3"}, - {file = "tinydb-4.8.2.tar.gz", hash = "sha256:f7dfc39b8d7fda7a1ca62a8dbb449ffd340a117c1206b68c50b1a481fb95181d"}, -] - [[package]] name = "tomli" version = "2.0.1" @@ -4163,28 +4139,6 @@ notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] telegram = ["requests"] -[[package]] -name = "tuspy" -version = "0.2.5" -description = "A Python client for the tus resumable upload protocol -> http://tus.io" -optional = false -python-versions = "*" -files = [ - {file = "tuspy-0.2.5-py3-none-any.whl", hash = "sha256:8ce106dd139ac77a097e8728f15f90d99df7408d9e67ac780ce44aefb9b5dd8d"}, - {file = "tuspy-0.2.5.tar.gz", hash = "sha256:1f238f65d444c0688f57e1dd704d9aa11cf5747eec33669e917627150170b9b8"}, -] - -[package.dependencies] -certifi = ">=2018.1.18" -future = ">=0.16.0" -requests = ">=2.18.4" -six = ">=1.11.0" -tinydb = ">=3.5.0" - -[package.extras] -dev = ["Sphinx (==1.7.1)", "sphinx-autobuild (==0.7.1)", "tox (>=2.3.1)"] -test = ["coverage (>=4.2)", "mock (>=2.0.0)", "pytest (>=3.0.3)", "pytest-cov (>=2.3.1,<2.6)", "responses (>=0.5.1)"] - [[package]] name = "typer" version = "0.12.5" @@ -4691,4 +4645,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.0" python-versions = "^3.10, <3.13" -content-hash = "677d6c5f1b1ced0f0a0365a765be4b611ea9ee532d6bba80bd57c57b80327e72" +content-hash = "f6a952ebe424cf68982c33f87e6ade04da9271777669f724b9cafcb4446e92e5" diff --git a/packages/examples/cvat/recording-oracle/pyproject.toml b/packages/examples/cvat/recording-oracle/pyproject.toml index eb7475b986..2964c4b9c8 100644 --- a/packages/examples/cvat/recording-oracle/pyproject.toml +++ b/packages/examples/cvat/recording-oracle/pyproject.toml @@ -22,7 +22,7 @@ google-cloud-storage = "^2.14.0" datumaro = {git = "https://github.com/cvat-ai/datumaro.git", rev = "ff83c00c2c1bc4b8fdfcc55067fcab0a9b5b6b11"} hexbytes = ">=1.2.0" # required for to_0x_hex() function starlette = ">=0.40.0" # avoid the vulnerability with multipart/form-data -cvat-sdk = "2.37.0" +cvat-sdk = "2.70.0" cryptography = "<44.0.0" # human-protocol-sdk -> pgpy dep requires cryptography < 45 human-protocol-sdk = "^7.3.1" strenum = "^0.4.15" From 5afa1fc049457772a9d09150561a900b69179d73 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Wed, 15 Jul 2026 14:02:44 +0300 Subject: [PATCH 49/53] Sync with cvat sdk changes --- packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py b/packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py index bbd8977b22..0823e381b7 100644 --- a/packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py +++ b/packages/examples/cvat/exchange-oracle/src/cvat/api_calls.py @@ -603,7 +603,7 @@ def get_task_upload_status(cvat_id: int) -> tuple[RequestStatus, str]: # Double check task status - the request can be removed already # TODO: remove this workaround when there is a stable replacement task = api_client.tasks_api.retrieve(cvat_id)[0] - if task.size > 0: + if task.media_type: status = RequestStatus.FINISHED reason = "" else: From 9c49ef9485d19b64a8e2591ea5b716a2c32f50be Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Wed, 15 Jul 2026 14:44:12 +0300 Subject: [PATCH 50/53] Fix alembic migration autogeneration --- .../cvat/exchange-oracle/alembic/env.py | 6 +---- .../exchange-oracle/src/models/__init__.py | 27 +++++++++++++++++++ .../cvat/recording-oracle/alembic/env.py | 6 +---- .../recording-oracle/src/models/__init__.py | 10 +++++++ 4 files changed, 39 insertions(+), 10 deletions(-) diff --git a/packages/examples/cvat/exchange-oracle/alembic/env.py b/packages/examples/cvat/exchange-oracle/alembic/env.py index b642b5942c..0af4210614 100644 --- a/packages/examples/cvat/exchange-oracle/alembic/env.py +++ b/packages/examples/cvat/exchange-oracle/alembic/env.py @@ -18,13 +18,9 @@ disable_existing_loggers=config.attributes.get("disable_existing_loggers", True), ) +import src.models # noqa: E402, F401 (registers all models on Base.metadata for autogenerate) from src.db import Base # noqa: E402 -# add your model's MetaData object here -# for 'autogenerate' support -# from myapp import mymodel -# target_metadata = mymodel.Base.metadata - target_metadata = Base.metadata # other values from the config, defined by the needs of env.py, diff --git a/packages/examples/cvat/exchange-oracle/src/models/__init__.py b/packages/examples/cvat/exchange-oracle/src/models/__init__.py index e69de29bb2..fcb2784788 100644 --- a/packages/examples/cvat/exchange-oracle/src/models/__init__.py +++ b/packages/examples/cvat/exchange-oracle/src/models/__init__.py @@ -0,0 +1,27 @@ +from src.models.cvat import ( + Assignment, + CvatWebhook, + DataUpload, + EscrowCreation, + EscrowValidation, + Image, + Job, + Project, + Task, + User, +) +from src.models.webhook import Webhook + +__all__ = [ + "Assignment", + "CvatWebhook", + "DataUpload", + "EscrowCreation", + "EscrowValidation", + "Image", + "Job", + "Project", + "Task", + "User", + "Webhook", +] diff --git a/packages/examples/cvat/recording-oracle/alembic/env.py b/packages/examples/cvat/recording-oracle/alembic/env.py index fde70d21e7..09bbefa476 100644 --- a/packages/examples/cvat/recording-oracle/alembic/env.py +++ b/packages/examples/cvat/recording-oracle/alembic/env.py @@ -18,13 +18,9 @@ disable_existing_loggers=config.attributes.get("disable_existing_loggers", True), ) +import src.models # noqa: E402, F401 (registers all models on Base.metadata for autogenerate) from src.db import Base # noqa: E402 -# add your model's MetaData object here -# for 'autogenerate' support -# from myapp import mymodel -# target_metadata = mymodel.Base.metadata - target_metadata = Base.metadata # other values from the config, defined by the needs of env.py, diff --git a/packages/examples/cvat/recording-oracle/src/models/__init__.py b/packages/examples/cvat/recording-oracle/src/models/__init__.py index e69de29bb2..4edf9ab64c 100644 --- a/packages/examples/cvat/recording-oracle/src/models/__init__.py +++ b/packages/examples/cvat/recording-oracle/src/models/__init__.py @@ -0,0 +1,10 @@ +from src.models.validation import GtStats, Job, Task, ValidationResult +from src.models.webhook import Webhook + +__all__ = [ + "GtStats", + "Job", + "Task", + "ValidationResult", + "Webhook", +] From 1e9fda35ad27ffa7e87ba66193adb75fadfcbc48 Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Wed, 15 Jul 2026 16:46:21 +0300 Subject: [PATCH 51/53] Support getting job bounty from escrow --- ..._project_assignment_bounty_9884511c7077.py | 30 ++++ .../docker-compose.test.head.yml | 1 + .../exchange-oracle/docker-compose.test.yml | 4 - .../cvat/exchange-oracle/pyproject.toml | 1 + .../src/endpoints/serializers.py | 32 +++- .../builders/audio/transcription.py | 17 +- .../job_creation/builders/vision/basic.py | 14 +- .../builders/vision/boxes_from_points.py | 17 +- .../builders/vision/skeletons_from_boxes.py | 17 +- .../src/handlers/job_creation/utils.py | 26 +++ .../cvat/exchange-oracle/src/models/cvat.py | 1 + .../cvat/exchange-oracle/src/services/cvat.py | 3 + .../tests/integration/services/test_cvat.py | 150 +++++++++--------- .../tests/unit/test_audio_task_creation.py | 10 +- 14 files changed, 217 insertions(+), 106 deletions(-) create mode 100644 packages/examples/cvat/exchange-oracle/alembic/versions/1784115076_add_project_assignment_bounty_9884511c7077.py diff --git a/packages/examples/cvat/exchange-oracle/alembic/versions/1784115076_add_project_assignment_bounty_9884511c7077.py b/packages/examples/cvat/exchange-oracle/alembic/versions/1784115076_add_project_assignment_bounty_9884511c7077.py new file mode 100644 index 0000000000..0caf69311e --- /dev/null +++ b/packages/examples/cvat/exchange-oracle/alembic/versions/1784115076_add_project_assignment_bounty_9884511c7077.py @@ -0,0 +1,30 @@ +""" +add project assignment_bounty + +Revision ID: 9884511c7077 +Revises: 216af22b5590 +Create Date: 2026-07-15 14:31:16.493245 + +""" + +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "9884511c7077" +down_revision = "216af22b5590" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("projects", sa.Column("assignment_bounty", sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("projects", "assignment_bounty") + # ### end Alembic commands ### diff --git a/packages/examples/cvat/exchange-oracle/docker-compose.test.head.yml b/packages/examples/cvat/exchange-oracle/docker-compose.test.head.yml index 7647773326..825d84fa01 100644 --- a/packages/examples/cvat/exchange-oracle/docker-compose.test.head.yml +++ b/packages/examples/cvat/exchange-oracle/docker-compose.test.head.yml @@ -20,6 +20,7 @@ services: STORAGE_PROVIDER: 'aws' ENABLE_CUSTOM_CLOUD_HOST: 'Yes' REDIS_HOST: 'redis' + REDIS_PORT: 6379 depends_on: postgres: condition: service_started diff --git a/packages/examples/cvat/exchange-oracle/docker-compose.test.yml b/packages/examples/cvat/exchange-oracle/docker-compose.test.yml index 1943955b12..899b0d4953 100644 --- a/packages/examples/cvat/exchange-oracle/docker-compose.test.yml +++ b/packages/examples/cvat/exchange-oracle/docker-compose.test.yml @@ -9,8 +9,6 @@ services: PGDATA: '/var/lib/postgresql/data/pgdata' networks: - test-network - ports: - - 5432:5432 redis: container_name: oracle-redis @@ -21,8 +19,6 @@ services: "--save", "60", "100", "--appendonly", "yes", ] - ports: - - 6379:6379 networks: - test-network diff --git a/packages/examples/cvat/exchange-oracle/pyproject.toml b/packages/examples/cvat/exchange-oracle/pyproject.toml index adb6f4071f..8d2cff6de4 100644 --- a/packages/examples/cvat/exchange-oracle/pyproject.toml +++ b/packages/examples/cvat/exchange-oracle/pyproject.toml @@ -109,6 +109,7 @@ ignore = [ "ANN204", # Missing return type annotation for special method "ERA001", # Found commented-out code "N801", # Class name should use CapWords convention + "PLR0913", # Too many arguments in function definition "PLR0915", # Too many statements "PLR2004", # Magic value used in comparison, consider replacing with a constant variable "ANN002", # Missing type annotation for `*args` diff --git a/packages/examples/cvat/exchange-oracle/src/endpoints/serializers.py b/packages/examples/cvat/exchange-oracle/src/endpoints/serializers.py index 05b01ffb80..50049a9d5c 100644 --- a/packages/examples/cvat/exchange-oracle/src/endpoints/serializers.py +++ b/packages/examples/cvat/exchange-oracle/src/endpoints/serializers.py @@ -8,7 +8,7 @@ get_escrow_fund_token_symbol, get_escrow_manifest, ) -from src.core.manifest import parse_manifest +from src.core.manifest import ManifestBase, parse_manifest from src.core.types import AssignmentStatuses, ProjectStatuses from src.db import SessionLocal from src.schemas import exchange as service_api @@ -50,14 +50,16 @@ def serialize_job( else: raise AssertionError(f"Unexpected project status '{project.status}'") + bounty = get_assignment_bounty(project, manifest=manifest) reward_token = get_escrow_fund_token_symbol(project.chain_id, project.escrow_address) + return service_api.JobResponse( escrow_address=project.escrow_address, chain_id=project.chain_id, job_type=project.job_type, status=api_status, job_description=manifest.annotation.description if manifest else None, - reward_amount=str(manifest.job_bounty) if manifest else None, + reward_amount=bounty, reward_token=reward_token, created_at=project.created_at, updated_at=project.updated_at, @@ -65,6 +67,26 @@ def serialize_job( ) +def get_assignment_bounty( + project: cvat_service.Project, *, manifest: ManifestBase | None = None +) -> str | None: + if project.assignment_bounty is not None: + assignment_bounty = project.assignment_bounty + else: + with suppress(ManifestNotAvailableError): + if not manifest: + manifest = parse_manifest( + get_escrow_manifest(project.chain_id, project.escrow_address) + ) + + assignment_bounty = getattr(manifest, "job_bounty", None) + + if assignment_bounty is not None: + assignment_bounty = str(assignment_bounty) + + return assignment_bounty + + ASSIGNMENT_PROJECT_VALIDATION_STATUSES = [ cvat_service.ProjectStatuses.annotation, cvat_service.ProjectStatuses.completed, @@ -101,9 +123,6 @@ def serialize_assignment( f"or a cvat_service.Project instance, not {project!r}" ) - with suppress(ManifestNotAvailableError): - manifest = parse_manifest(get_escrow_manifest(project.chain_id, project.escrow_address)) - assignment_status_mapping = { AssignmentStatuses.created: service_api.AssignmentStatuses.active, AssignmentStatuses.completed: service_api.AssignmentStatuses.completed, @@ -119,6 +138,7 @@ def serialize_assignment( else: api_status = assignment_status_mapping[assignment.status] + bounty = get_assignment_bounty(project) reward_token = get_escrow_fund_token_symbol(project.chain_id, project.escrow_address) return service_api.AssignmentResponse( @@ -127,7 +147,7 @@ def serialize_assignment( chain_id=project.chain_id, job_type=project.job_type, status=api_status, - reward_amount=str(manifest.job_bounty) if manifest else None, + reward_amount=bounty, reward_token=reward_token, url=compose_assignment_url( task_id=assignment.job.cvat_task_id, diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py index f9761024cd..133786dd17 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py @@ -40,6 +40,7 @@ ) from src.handlers.job_creation.utils import ( MaybeUnset, + get_assignment_bounty, make_cvat_cloud_storage_params, unset, ) @@ -606,12 +607,18 @@ def _create_on_cvat(self) -> None: project_id = db_service.create_project( session, cvat_project.id, - cloud_storage.id, - TaskTypes.audio_transcription, - escrow_address, - chain_id, - self._oracle_data_bucket.to_url(), + cvat_cloudstorage_id=cloud_storage.id, + job_type=TaskTypes.audio_transcription, + escrow_address=escrow_address, + chain_id=chain_id, + bucket_url=self._oracle_data_bucket.to_url(), cvat_webhook_id=cvat_webhook.id, + assignment_bounty=get_assignment_bounty( + self.manifest, + escrow_address=escrow_address, + chain_id=chain_id, + job_count=len(self._assignments), + ), ) db_service.get_project_by_id(session, project_id, for_update=True) # lock the row diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/basic.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/basic.py index 2ff83b47da..b7fa30323f 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/basic.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/basic.py @@ -34,6 +34,7 @@ from src.handlers.job_creation.utils import ( MaybeUnset, filter_image_files, + get_assignment_bounty, make_cvat_cloud_storage_params, make_cvat_label_configuration, unset, @@ -154,12 +155,15 @@ def build(self): project_id = db_service.create_project( session, cvat_project.id, - cloud_storage.id, - manifest.annotation.type, - escrow_address, - chain_id, - data_bucket.to_url(), + cvat_cloudstorage_id=cloud_storage.id, + job_type=manifest.annotation.type, + escrow_address=escrow_address, + chain_id=chain_id, + bucket_url=data_bucket.to_url(), cvat_webhook_id=cvat_webhook.id, + assignment_bounty=get_assignment_bounty( + manifest, escrow_address=escrow_address, chain_id=chain_id, job_count=total_jobs + ), ) db_service.get_project_by_id(session, project_id, for_update=True) # lock the row diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/boxes_from_points.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/boxes_from_points.py index 7dba049790..19eaefe061 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/boxes_from_points.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/boxes_from_points.py @@ -51,6 +51,7 @@ from src.handlers.job_creation.utils import ( MaybeUnset, filter_image_files, + get_assignment_bounty, make_cvat_cloud_storage_params, make_cvat_label_configuration, strip_bucket_prefix, @@ -1063,14 +1064,20 @@ def _create_on_cvat(self): project_id = db_service.create_project( session, cvat_project.id, - cvat_cloud_storage.id, - self.manifest.annotation.type, - self.escrow_address, - self.chain_id, - oracle_bucket.to_url().rstrip("/") + cvat_cloudstorage_id=cvat_cloud_storage.id, + job_type=self.manifest.annotation.type, + escrow_address=self.escrow_address, + chain_id=self.chain_id, + bucket_url=oracle_bucket.to_url().rstrip("/") + "/" + compose_data_bucket_prefix(self.escrow_address, self.chain_id), cvat_webhook_id=cvat_webhook.id, + assignment_bounty=get_assignment_bounty( + self.manifest, + escrow_address=self.escrow_address, + chain_id=self.chain_id, + job_count=total_jobs, + ), ) db_service.get_project_by_id(session, project_id, for_update=True) # lock the row db_service.add_project_images( diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/skeletons_from_boxes.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/skeletons_from_boxes.py index 31558143d6..ce9a86fdcf 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/skeletons_from_boxes.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/vision/skeletons_from_boxes.py @@ -51,6 +51,7 @@ from src.handlers.job_creation.utils import ( MaybeUnset, filter_image_files, + get_assignment_bounty, make_cvat_cloud_storage_params, strip_bucket_prefix, unset, @@ -1328,14 +1329,20 @@ def _task_params_label_key(ts): project_id = db_service.create_project( session, cvat_project.id, - cvat_cloud_storage.id, - self.manifest.annotation.type, - self.escrow_address, - self.chain_id, - oracle_bucket.to_url().rstrip("/") + cvat_cloudstorage_id=cvat_cloud_storage.id, + job_type=self.manifest.annotation.type, + escrow_address=self.escrow_address, + chain_id=self.chain_id, + bucket_url=oracle_bucket.to_url().rstrip("/") + "/" + compose_data_bucket_prefix(self.escrow_address, self.chain_id), cvat_webhook_id=cvat_webhook.id, + assignment_bounty=get_assignment_bounty( + self.manifest, + escrow_address=self.escrow_address, + chain_id=self.chain_id, + job_count=total_jobs, + ), ) created_projects.append(project_id) diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py index b9b957d580..5746ba5295 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/utils.py @@ -1,15 +1,18 @@ from __future__ import annotations import os +from decimal import Decimal from typing import TYPE_CHECKING, TypeVar from datumaro.util.image import IMAGE_EXTENSIONS import src.cvat.api_calls as cvat_api +from src.chain.escrow import get_escrow from src.core.tasks import TaskTypes from src.services.cloud import CloudProviders if TYPE_CHECKING: + from src.core.manifest import ManifestBase from src.core.manifest.v1 import JobManifest from src.services.cloud.utils import BucketAccessInfo @@ -36,6 +39,29 @@ def __bool__(self) -> bool: MaybeUnset = T | _Undefined +def get_assignment_bounty( + manifest: ManifestBase, escrow_address: str, chain_id: int, *, job_count: int +) -> str | None: + match manifest.version: + case 1: + return str(manifest.job_bounty) + case 2: + return _get_assignment_bounty_from_escrow(escrow_address, chain_id, job_count) + case _ as version: + raise NotImplementedError(f"Unexpected manifest version '{version}'") + + +def _get_assignment_bounty_from_escrow( + escrow_address: str, chain_id: int, job_count: int +) -> str | None: + # TODO: refine (fees, already-paid amount, token decimals). Rough split of the escrow's total + # funds across the jobs. + if not job_count: + return None + escrow = get_escrow(chain_id, escrow_address) + return str(Decimal(escrow.total_funded_amount) / job_count) + + def is_image(path: str) -> bool: trunk, ext = os.path.splitext(os.path.basename(path)) return trunk and ext.lower() in IMAGE_EXTENSIONS diff --git a/packages/examples/cvat/exchange-oracle/src/models/cvat.py b/packages/examples/cvat/exchange-oracle/src/models/cvat.py index bebe3e571d..8d6dcfaae5 100644 --- a/packages/examples/cvat/exchange-oracle/src/models/cvat.py +++ b/packages/examples/cvat/exchange-oracle/src/models/cvat.py @@ -30,6 +30,7 @@ class Project(BaseUUID): ) # TODO: extract into a separate model chain_id = Column(Integer, Enum(Networks), nullable=False) bucket_url = Column(String, nullable=False) + assignment_bounty = Column(String, nullable=True) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now()) cvat_webhook_id = Column(Integer, nullable=True) diff --git a/packages/examples/cvat/exchange-oracle/src/services/cvat.py b/packages/examples/cvat/exchange-oracle/src/services/cvat.py index 3c1b3ced80..1691cb0d10 100644 --- a/packages/examples/cvat/exchange-oracle/src/services/cvat.py +++ b/packages/examples/cvat/exchange-oracle/src/services/cvat.py @@ -54,6 +54,7 @@ def batched(iterable: Iterable, *, batch_size: int) -> Iterable[Any]: def create_project( session: Session, cvat_id: int, + *, cvat_cloudstorage_id: int, job_type: str, escrow_address: str, @@ -61,6 +62,7 @@ def create_project( bucket_url: str, cvat_webhook_id: int | None = None, status: ProjectStatuses = ProjectStatuses.creation, + assignment_bounty: str | None = None, ) -> str: """ Create a project from CVAT. @@ -76,6 +78,7 @@ def create_project( chain_id=chain_id, bucket_url=bucket_url, cvat_webhook_id=cvat_webhook_id, + assignment_bounty=assignment_bounty, ) session.add(project) diff --git a/packages/examples/cvat/exchange-oracle/tests/integration/services/test_cvat.py b/packages/examples/cvat/exchange-oracle/tests/integration/services/test_cvat.py index 5f43f03d69..9b38f5c68d 100644 --- a/packages/examples/cvat/exchange-oracle/tests/integration/services/test_cvat.py +++ b/packages/examples/cvat/exchange-oracle/tests/integration/services/test_cvat.py @@ -59,11 +59,11 @@ def test_create_project(self): p_id = cvat_service.create_project( self.session, cvat_id, - cvat_cloudstorage_id, - job_type, - escrow_address, - chain_id, - bucket_url, + cvat_cloudstorage_id=cvat_cloudstorage_id, + job_type=job_type, + escrow_address=escrow_address, + chain_id=chain_id, + bucket_url=bucket_url, ) project = self.session.query(Project).filter_by(id=p_id).first() @@ -87,22 +87,22 @@ def test_create_duplicated_project(self): cvat_service.create_project( self.session, cvat_id, - cvat_cloudstorage_id, - job_type, - escrow_address, - chain_id, - bucket_url, + cvat_cloudstorage_id=cvat_cloudstorage_id, + job_type=job_type, + escrow_address=escrow_address, + chain_id=chain_id, + bucket_url=bucket_url, ) self.session.commit() cvat_service.create_project( self.session, cvat_id, - cvat_cloudstorage_id, - job_type, - escrow_address, - chain_id, - bucket_url, + cvat_cloudstorage_id=cvat_cloudstorage_id, + job_type=job_type, + escrow_address=escrow_address, + chain_id=chain_id, + bucket_url=bucket_url, ) with pytest.raises(IntegrityError): self.session.commit() @@ -116,11 +116,11 @@ def test_create_project_none_cvat_id(self): cvat_service.create_project( self.session, None, - cvat_cloudstorage_id, - job_type, - escrow_address, - chain_id, - bucket_url, + cvat_cloudstorage_id=cvat_cloudstorage_id, + job_type=job_type, + escrow_address=escrow_address, + chain_id=chain_id, + bucket_url=bucket_url, ) with pytest.raises(IntegrityError): self.session.commit() @@ -134,11 +134,11 @@ def test_create_project_none_cvat_cloudstorage_id(self): cvat_service.create_project( self.session, cvat_id, - None, - job_type, - escrow_address, - chain_id, - bucket_url, + cvat_cloudstorage_id=None, + job_type=job_type, + escrow_address=escrow_address, + chain_id=chain_id, + bucket_url=bucket_url, ) with pytest.raises(IntegrityError): self.session.commit() @@ -152,11 +152,11 @@ def test_create_project_none_job_type(self): cvat_service.create_project( self.session, cvat_id, - cvat_cloudstorage_id, - None, - escrow_address, - chain_id, - bucket_url, + cvat_cloudstorage_id=cvat_cloudstorage_id, + job_type=None, + escrow_address=escrow_address, + chain_id=chain_id, + bucket_url=bucket_url, ) with pytest.raises(IntegrityError): self.session.commit() @@ -170,11 +170,11 @@ def test_create_project_none_escrow_address(self): cvat_service.create_project( self.session, cvat_id, - cvat_cloudstorage_id, - job_type, - None, - chain_id, - bucket_url, + cvat_cloudstorage_id=cvat_cloudstorage_id, + job_type=job_type, + escrow_address=None, + chain_id=chain_id, + bucket_url=bucket_url, ) with pytest.raises(IntegrityError): self.session.commit() @@ -189,11 +189,11 @@ def test_create_project_none_chain_id(self): cvat_service.create_project( self.session, cvat_id, - cvat_cloudstorage_id, - job_type, - escrow_address, - None, - bucket_url, + cvat_cloudstorage_id=cvat_cloudstorage_id, + job_type=job_type, + escrow_address=escrow_address, + chain_id=None, + bucket_url=bucket_url, ) with pytest.raises(IntegrityError): self.session.commit() @@ -207,11 +207,11 @@ def test_create_project_none_bucket_url(self): cvat_service.create_project( self.session, cvat_id, - cvat_cloudstorage_id, - job_type, - escrow_address, - chain_id, - None, + cvat_cloudstorage_id=cvat_cloudstorage_id, + job_type=job_type, + escrow_address=escrow_address, + chain_id=chain_id, + bucket_url=None, ) with pytest.raises(IntegrityError): self.session.commit() @@ -226,11 +226,11 @@ def test_get_project_by_id(self): p_id = cvat_service.create_project( self.session, cvat_id, - cvat_cloudstorage_id, - job_type, - escrow_address, - chain_id, - bucket_url, + cvat_cloudstorage_id=cvat_cloudstorage_id, + job_type=job_type, + escrow_address=escrow_address, + chain_id=chain_id, + bucket_url=bucket_url, status=ProjectStatuses.annotation, ) @@ -276,11 +276,11 @@ def test_get_project_by_escrow_address(self): p_id = cvat_service.create_project( self.session, cvat_id, - cvat_cloudstorage_id, - job_type, - escrow_address, - chain_id, - bucket_url, + cvat_cloudstorage_id=cvat_cloudstorage_id, + job_type=job_type, + escrow_address=escrow_address, + chain_id=chain_id, + bucket_url=bucket_url, status=ProjectStatuses.annotation, ) @@ -309,11 +309,11 @@ def test_get_projects_by_status(self): p_id = cvat_service.create_project( self.session, cvat_id, - cvat_cloudstorage_id, - job_type, - escrow_address, - chain_id, - bucket_url, + cvat_cloudstorage_id=cvat_cloudstorage_id, + job_type=job_type, + escrow_address=escrow_address, + chain_id=chain_id, + bucket_url=bucket_url, status=ProjectStatuses.annotation, ) @@ -325,11 +325,11 @@ def test_get_projects_by_status(self): cvat_service.create_project( self.session, cvat_id, - cvat_cloudstorage_id, - job_type, - escrow_address, - chain_id, - bucket_url, + cvat_cloudstorage_id=cvat_cloudstorage_id, + job_type=job_type, + escrow_address=escrow_address, + chain_id=chain_id, + bucket_url=bucket_url, status=ProjectStatuses.annotation, ) @@ -341,11 +341,11 @@ def test_get_projects_by_status(self): cvat_service.create_project( self.session, cvat_id, - cvat_cloudstorage_id, - job_type, - escrow_address, - chain_id, - bucket_url, + cvat_cloudstorage_id=cvat_cloudstorage_id, + job_type=job_type, + escrow_address=escrow_address, + chain_id=chain_id, + bucket_url=bucket_url, status=ProjectStatuses.annotation, ) @@ -494,11 +494,11 @@ def test_update_project_status(self): p_id = cvat_service.create_project( self.session, cvat_id, - cvat_cloudstorage_id, - job_type, - escrow_address, - chain_id, - bucket_url, + cvat_cloudstorage_id=cvat_cloudstorage_id, + job_type=job_type, + escrow_address=escrow_address, + chain_id=chain_id, + bucket_url=bucket_url, ) cvat_service.update_project_status(self.session, p_id, ProjectStatuses.completed) diff --git a/packages/examples/cvat/exchange-oracle/tests/unit/test_audio_task_creation.py b/packages/examples/cvat/exchange-oracle/tests/unit/test_audio_task_creation.py index 1ca543d56c..96c9247a2f 100644 --- a/packages/examples/cvat/exchange-oracle/tests/unit/test_audio_task_creation.py +++ b/packages/examples/cvat/exchange-oracle/tests/unit/test_audio_task_creation.py @@ -121,7 +121,7 @@ def _make_cvat_api_mock() -> Mock: return cvat_api -def cleanup_oracle_bucket(prefix: str = "/"): +def cleanup_oracle_bucket(prefix: str = ""): storage_client = make_client(BucketAccessInfo.parse_obj(Config.storage_config)) storage_client.remove_files(prefix=prefix) @@ -143,12 +143,19 @@ def test_create_audio_transcription_task(fxt_audio_transcription_input): manifest, ds_rois_tsv, gt_tsv = fxt_audio_transcription_input cvat_api = _make_cvat_api_mock() + escrow = Mock(total_funded_amount="40") with ( patch.object(handlers, "get_escrow_manifest", return_value=manifest), patch("src.handlers.job_creation.builders.audio.transcription.cvat_api", cvat_api), + patch( + "src.handlers.job_creation.utils.get_escrow", return_value=escrow + ) as mock_get_escrow, ): handlers.create_task(ESCROW_ADDRESS, CHAIN_ID) + # v2 manifest → per-assignment bounty derived from escrow funds / job count + mock_get_escrow.assert_called_once_with(CHAIN_ID, ESCROW_ADDRESS) + # Check CVAT API calls cvat_api.create_project.assert_called_once() cvat_api.create_cvat_webhook.assert_called_once() @@ -167,6 +174,7 @@ def test_create_audio_transcription_task(fxt_audio_transcription_input): assert project.cvat_cloudstorage_id == 100 assert project.job_type == TaskTypes.audio_transcription.value assert project.chain_id == CHAIN_ID + assert project.assignment_bounty == "10" # 40 escrow funds / 4 assignments tasks = db_service.get_tasks_by_cvat_project_id(session, project.cvat_id) assert len(tasks) == assignment_count From b1158308b0dc8af457d6da7411838c76d4c0376d Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Wed, 15 Jul 2026 17:40:13 +0300 Subject: [PATCH 52/53] Fix formatting --- .../exchange-oracle/tests/unit/test_audio_task_creation.py | 4 +--- .../examples/cvat/recording-oracle/.pre-commit-config.yaml | 4 ++-- packages/examples/cvat/recording-oracle/pyproject.toml | 3 ++- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/examples/cvat/exchange-oracle/tests/unit/test_audio_task_creation.py b/packages/examples/cvat/exchange-oracle/tests/unit/test_audio_task_creation.py index 96c9247a2f..2c853789f5 100644 --- a/packages/examples/cvat/exchange-oracle/tests/unit/test_audio_task_creation.py +++ b/packages/examples/cvat/exchange-oracle/tests/unit/test_audio_task_creation.py @@ -147,9 +147,7 @@ def test_create_audio_transcription_task(fxt_audio_transcription_input): with ( patch.object(handlers, "get_escrow_manifest", return_value=manifest), patch("src.handlers.job_creation.builders.audio.transcription.cvat_api", cvat_api), - patch( - "src.handlers.job_creation.utils.get_escrow", return_value=escrow - ) as mock_get_escrow, + patch("src.handlers.job_creation.utils.get_escrow", return_value=escrow) as mock_get_escrow, ): handlers.create_task(ESCROW_ADDRESS, CHAIN_ID) diff --git a/packages/examples/cvat/recording-oracle/.pre-commit-config.yaml b/packages/examples/cvat/recording-oracle/.pre-commit-config.yaml index 395275745a..3211dfbcc9 100644 --- a/packages/examples/cvat/recording-oracle/.pre-commit-config.yaml +++ b/packages/examples/cvat/recording-oracle/.pre-commit-config.yaml @@ -3,14 +3,14 @@ repos: hooks: - id: lint name: lint - entry: ruff check --fix --unsafe-fixes --show-fixes + entry: ruff check --force-exclude --fix --unsafe-fixes --show-fixes language: system require_serial: true files: "^packages/examples/cvat/recording-oracle/.*" types: [python] - id: format name: format - entry: ruff format + entry: ruff format --force-exclude require_serial: true language: system files: "^packages/examples/cvat/recording-oracle/.*" diff --git a/packages/examples/cvat/recording-oracle/pyproject.toml b/packages/examples/cvat/recording-oracle/pyproject.toml index 2964c4b9c8..8ccea6d74a 100644 --- a/packages/examples/cvat/recording-oracle/pyproject.toml +++ b/packages/examples/cvat/recording-oracle/pyproject.toml @@ -36,12 +36,13 @@ ruff = "^0.6.0" [tool.ruff] line-length = 100 target-version = "py310" +extend-exclude = ["libs/"] [tool.ruff.lint] select = ["ALL"] unfixable = [ - "RUF005", # messes up concantenation with numpy structures + "RUF005", # messes up concatenation with numpy structures ] ignore = [ "W191", # Rules conflicting with ruff format (https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules) From a953cc393cc02adff5eb0163aff09eb4420d949e Mon Sep 17 00:00:00 2001 From: Maxim Zhiltsov Date: Wed, 15 Jul 2026 18:18:36 +0300 Subject: [PATCH 53/53] Validate media file paths --- .../src/handlers/job_creation/builders/audio/transcription.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py index 133786dd17..e8b6752a6f 100644 --- a/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py +++ b/packages/examples/cvat/exchange-oracle/src/handlers/job_creation/builders/audio/transcription.py @@ -303,6 +303,7 @@ def _download_input_data(self) -> None: media_paths: dict[str, Path] = {} for filename in sorted(referenced): local_path = media_dir / PurePosixPath(filename) + local_path = local_path.resolve().relative_to(media_dir) local_path.parent.mkdir(parents=True, exist_ok=True) data = media_client.download_file(_bucket_key(media_bucket.path, filename))