From 28b6c1c5cf5e32c05c7384c1724a322db454f119 Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Tue, 13 Jan 2026 13:13:02 +0200 Subject: [PATCH 01/25] Refine LLM unlearning models --- hirundo/__init__.py | 16 +- hirundo/unlearning_llm.py | 377 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 392 insertions(+), 1 deletion(-) create mode 100644 hirundo/unlearning_llm.py diff --git a/hirundo/__init__.py b/hirundo/__init__.py index 96c51f2e..1dfaf935 100644 --- a/hirundo/__init__.py +++ b/hirundo/__init__.py @@ -30,6 +30,14 @@ StorageGit, StorageS3, ) +from .unlearning_llm import ( + BiasRunInfo, + BiasType, + HuggingFaceTransformersModel, + LlmModel, + LlmUnlearningRun, + LocalTransformersModel, +) from .unzip import load_df, load_from_zip __all__ = [ @@ -59,8 +67,14 @@ "StorageGit", "StorageConfig", "DatasetQAResults", + "BiasRunInfo", + "BiasType", + "HuggingFaceTransformersModel", + "LlmModel", + "LlmUnlearningRun", + "LocalTransformersModel", "load_df", "load_from_zip", ] -__version__ = "0.1.21" +__version__ = "0.1.22" diff --git a/hirundo/unlearning_llm.py b/hirundo/unlearning_llm.py new file mode 100644 index 00000000..36aa4325 --- /dev/null +++ b/hirundo/unlearning_llm.py @@ -0,0 +1,377 @@ +import datetime +import typing +from enum import Enum + +from pydantic import BaseModel +from typing_extensions import Literal + +from hirundo._env import API_HOST +from hirundo._headers import get_headers +from hirundo._http import raise_for_status_with_reason, requests +from hirundo._timeouts import MODIFY_TIMEOUT, READ_TIMEOUT +from hirundo.logger import get_logger + +logger = get_logger(__name__) + + +class ModelSourceType(str, Enum): + HUGGINGFACE_TRANSFORMERS = "huggingface_transformers" + LOCAL_TRANSFORMERS = "local_transformers" + + +class HuggingFaceTransformersModel(BaseModel): + type: Literal[ModelSourceType.HUGGINGFACE_TRANSFORMERS] = ( + ModelSourceType.HUGGINGFACE_TRANSFORMERS + ) + revision: typing.Optional[str] = None + code_revision: typing.Optional[str] = None + model_name: str + token: typing.Optional[str] = None + + +class HuggingFaceTransformersModelOutput(BaseModel): + type: Literal[ModelSourceType.HUGGINGFACE_TRANSFORMERS] = ( + ModelSourceType.HUGGINGFACE_TRANSFORMERS + ) + model_name: str + + +class LocalTransformersModel(BaseModel): + type: Literal[ModelSourceType.LOCAL_TRANSFORMERS] = ( + ModelSourceType.LOCAL_TRANSFORMERS + ) + revision: None = None + code_revision: None = None + local_path: str + + +LlmSources = typing.Union[HuggingFaceTransformersModel, LocalTransformersModel] +LlmSourcesOutput = typing.Union[ + HuggingFaceTransformersModelOutput, LocalTransformersModel +] + + +class LlmModel(BaseModel): + id: typing.Optional[int] = None + organization_id: typing.Optional[int] = None + model_name: str + model_source: LlmSources + archive_existing_runs: bool = True + + def create( + self, + replace_if_exists: bool = False, + ) -> int: + llm_model_response = requests.post( + f"{API_HOST}/unlearning-llm/llm/", + json={ + **self.model_dump(mode="json"), + "replace_if_exists": replace_if_exists, + }, + headers=get_headers(), + timeout=MODIFY_TIMEOUT, + ) + raise_for_status_with_reason(llm_model_response) + llm_model_id = llm_model_response.json()["id"] + self.id = llm_model_id + return llm_model_id + + @staticmethod + def get_by_id(llm_model_id: int) -> "LlmModelOut": + llm_model_response = requests.get( + f"{API_HOST}/unlearning-llm/llm/{llm_model_id}", + headers=get_headers(), + timeout=READ_TIMEOUT, + ) + raise_for_status_with_reason(llm_model_response) + return LlmModelOut.from_response(llm_model_response.json()) + + @staticmethod + def get_by_name(llm_model_name: str) -> "LlmModelOut": + llm_model_response = requests.get( + f"{API_HOST}/unlearning-llm/llm/by-name/{llm_model_name}", + headers=get_headers(), + timeout=READ_TIMEOUT, + ) + raise_for_status_with_reason(llm_model_response) + return LlmModelOut.from_response(llm_model_response.json()) + + @staticmethod + def list(organization_id: typing.Optional[int] = None) -> list["LlmModelOut"]: + params = {} + if organization_id is not None: + params["model_organization_id"] = organization_id + llm_model_response = requests.get( + f"{API_HOST}/unlearning-llm/llm/", + params=params, + headers=get_headers(), + timeout=READ_TIMEOUT, + ) + raise_for_status_with_reason(llm_model_response) + llm_model_json = llm_model_response.json() + return [LlmModelOut.from_response(llm_model) for llm_model in llm_model_json] + + @staticmethod + def delete_by_id(llm_model_id: int) -> None: + llm_model_response = requests.delete( + f"{API_HOST}/unlearning-llm/llm/{llm_model_id}", + headers=get_headers(), + timeout=MODIFY_TIMEOUT, + ) + raise_for_status_with_reason(llm_model_response) + logger.info("Deleted LLM model with ID: %s", llm_model_id) + + def delete(self) -> None: + if not self.id: + raise ValueError("No LLM model has been created") + self.delete_by_id(self.id) + + def update( + self, + model_name: typing.Optional[str] = None, + model_source: typing.Optional[LlmSources] = None, + archive_existing_runs: typing.Optional[bool] = None, + ) -> None: + if not self.id: + raise ValueError("No LLM model has been created") + payload: dict[str, typing.Any] = { + "model_name": model_name, + "model_source": model_source.model_dump(mode="json") + if model_source + else None, + "archive_existing_runs": archive_existing_runs, + "organization_id": self.organization_id, + } + llm_model_response = requests.put( + f"{API_HOST}/unlearning-llm/llm/{self.id}", + json=payload, + headers=get_headers(), + timeout=MODIFY_TIMEOUT, + ) + raise_for_status_with_reason(llm_model_response) + if model_name is not None: + self.model_name = model_name + if model_source is not None: + self.model_source = model_source + if archive_existing_runs is not None: + self.archive_existing_runs = archive_existing_runs + + +class LlmModelOut(BaseModel): + id: int + organization_id: int + creator_id: int + creator_name: str + created_at: datetime.datetime + updated_at: datetime.datetime + model_name: str + model_source: LlmSourcesOutput + + @staticmethod + def from_response(response_payload: dict[str, typing.Any]) -> "LlmModelOut": + return LlmModelOut( + id=response_payload["id"], + organization_id=response_payload["organization_id"], + creator_id=response_payload["creator_id"], + creator_name=response_payload["creator_name"], + created_at=response_payload["created_at"], + updated_at=response_payload["updated_at"], + model_name=response_payload["model_name"], + model_source=response_payload["model_source"], + ) + + +class DatasetType(str, Enum): + NORMAL = "normal" + BIAS = "bias" + UNBIAS = "unbias" + + +class UnlearningLlmAdvancedOptions(BaseModel): + max_tokens_for_model: typing.Optional[typing.Union[dict[DatasetType, int], int]] = ( + None + ) + + +class BiasType(str, Enum): + ALL = "ALL" + RACE = "RACE" + NATIONALITY = "NATIONALITY" + GENDER = "GENDER" + PHYSICAL_APPEARANCE = "PHYSICAL_APPEARANCE" + RELIGION = "RELIGION" + AGE = "AGE" + + +class UtilityType(str, Enum): + DEFAULT = "DEFAULT" + CUSTOM = "CUSTOM" + + +class DefaultUtility(BaseModel): + utility_type: Literal[UtilityType.DEFAULT] = UtilityType.DEFAULT + + +class HirundoCSVDataset(BaseModel): + type: Literal["HirundoCSV"] = "HirundoCSV" + csv_url: str + + +class HuggingFaceDataset(BaseModel): + type: Literal["HuggingFaceDataset"] = "HuggingFaceDataset" + hugging_face_dataset_name: str + + +CustomDataset = typing.Union[HirundoCSVDataset, HuggingFaceDataset] + + +class CustomUtility(BaseModel): + utility_type: Literal[UtilityType.CUSTOM] = UtilityType.CUSTOM + dataset: CustomDataset + + +class BiasBehavior(BaseModel): + type: Literal["BIAS"] = "BIAS" + bias_type: BiasType + + +class HallucinationType(str, Enum): + GENERAL = "GENERAL" + MEDICAL = "MEDICAL" + LEGAL = "LEGAL" + DEFENSE = "DEFENSE" + + +class HallucinationBehavior(BaseModel): + type: Literal["HALLUCINATION"] = "HALLUCINATION" + hallucination_type: HallucinationType + + +class SecurityBehavior(BaseModel): + type: Literal["SECURITY"] = "SECURITY" + + +class CustomBehavior(BaseModel): + type: Literal["CUSTOM"] = "CUSTOM" + biased_dataset: CustomDataset + unbiased_dataset: CustomDataset + + +TargetBehavior = typing.Union[ + BiasBehavior, + HallucinationBehavior, + SecurityBehavior, + CustomBehavior, +] + +TargetUtility = typing.Union[DefaultUtility, CustomUtility] + + +class LlmRunInfo(BaseModel): + organization_id: typing.Optional[int] = None + name: typing.Optional[str] = None + model_id: typing.Optional[int] = None + target_behaviors: list[TargetBehavior] + target_utilities: list[TargetUtility] + advanced_options: typing.Optional[UnlearningLlmAdvancedOptions] = None + + +class BiasRunInfo(BaseModel): + bias_type: BiasType + organization_id: typing.Optional[int] = None + name: typing.Optional[str] = None + target_utilities: typing.Optional[list[TargetUtility]] = None + advanced_options: typing.Optional[UnlearningLlmAdvancedOptions] = None + + def to_run_info(self) -> LlmRunInfo: + default_utilities = self.target_utilities or [DefaultUtility()] + return LlmRunInfo( + organization_id=self.organization_id, + name=self.name, + target_behaviors=[BiasBehavior(bias_type=self.bias_type)], + target_utilities=default_utilities, + advanced_options=self.advanced_options, + ) + + +class LlmUnlearningRun: + @staticmethod + def launch(model_id: int, run_info: typing.Union[LlmRunInfo, BiasRunInfo]) -> str: + resolved_run_info = ( + run_info.to_run_info() if isinstance(run_info, BiasRunInfo) else run_info + ) + run_response = requests.post( + f"{API_HOST}/unlearning-llm/run/{model_id}", + json=resolved_run_info.model_dump(mode="json"), + headers=get_headers(), + timeout=MODIFY_TIMEOUT, + ) + raise_for_status_with_reason(run_response) + run_response_json = run_response.json() if run_response.content else {} + run_id = ( + run_response_json.get("run_id") + or run_response_json.get("hir_run_id") + or run_response_json.get("id") + ) + if isinstance(run_response_json, str): + return run_response_json + if not run_id: + raise ValueError("No run ID returned from launch request") + return run_id + + @staticmethod + def cancel(run_id: str) -> None: + run_response = requests.patch( + f"{API_HOST}/unlearning-llm/run/cancel/{run_id}", + headers=get_headers(), + timeout=MODIFY_TIMEOUT, + ) + raise_for_status_with_reason(run_response) + + @staticmethod + def rename(run_id: str, new_name: str) -> None: + run_response = requests.patch( + f"{API_HOST}/unlearning-llm/run/rename/{run_id}", + json={"new_name": new_name}, + headers=get_headers(), + timeout=MODIFY_TIMEOUT, + ) + raise_for_status_with_reason(run_response) + + @staticmethod + def archive(run_id: str) -> None: + run_response = requests.patch( + f"{API_HOST}/unlearning-llm/run/archive/{run_id}", + headers=get_headers(), + timeout=MODIFY_TIMEOUT, + ) + raise_for_status_with_reason(run_response) + + @staticmethod + def restore(run_id: str) -> None: + run_response = requests.patch( + f"{API_HOST}/unlearning-llm/run/restore/{run_id}", + headers=get_headers(), + timeout=MODIFY_TIMEOUT, + ) + raise_for_status_with_reason(run_response) + + @staticmethod + def list( + organization_id: typing.Optional[int] = None, + archived: bool = False, + ) -> list[dict[str, typing.Any]]: + params = {"archived": archived} + if organization_id is not None: + params["unlearning_organization_id"] = organization_id + run_response = requests.get( + f"{API_HOST}/unlearning-llm/run/list", + params=params, + headers=get_headers(), + timeout=READ_TIMEOUT, + ) + raise_for_status_with_reason(run_response) + response_json = run_response.json() + if isinstance(response_json, list): + return response_json + return [response_json] From 96e813798e69bc5a2963768fdbdc08b9cf042199 Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Tue, 13 Jan 2026 22:30:53 +0200 Subject: [PATCH 02/25] Move `get_unique_id` to a new `testing_utils` file --- hirundo/__init__.py | 2 ++ tests/classification/classification_aws_test.py | 2 +- tests/classification/classification_gcp_test.py | 2 +- tests/classification/sanity_gcp_test.py | 2 +- tests/dataset_qa_shared.py | 6 ------ tests/get_by_name_test.py | 2 +- tests/object-detection/od_aws_test.py | 2 +- tests/object-detection/od_git_test.py | 2 +- tests/object-detection/rockpaperscisssors_yolo_test.py | 2 +- tests/object-detection/sama_coco_test.py | 2 +- tests/object-detection/sanity_aws_test.py | 2 +- tests/speech-to-text/sanity_stt_git_test.py | 2 +- tests/speech-to-text/stt_git_test.py | 2 +- tests/testing_utils.py | 7 +++++++ 14 files changed, 20 insertions(+), 17 deletions(-) create mode 100644 tests/testing_utils.py diff --git a/hirundo/__init__.py b/hirundo/__init__.py index 1dfaf935..59e27251 100644 --- a/hirundo/__init__.py +++ b/hirundo/__init__.py @@ -35,6 +35,7 @@ BiasType, HuggingFaceTransformersModel, LlmModel, + LlmSources, LlmUnlearningRun, LocalTransformersModel, ) @@ -71,6 +72,7 @@ "BiasType", "HuggingFaceTransformersModel", "LlmModel", + "LlmSources", "LlmUnlearningRun", "LocalTransformersModel", "load_df", diff --git a/tests/classification/classification_aws_test.py b/tests/classification/classification_aws_test.py index c133a1e5..3dd39cd7 100644 --- a/tests/classification/classification_aws_test.py +++ b/tests/classification/classification_aws_test.py @@ -13,8 +13,8 @@ from tests.dataset_qa_shared import ( cleanup, dataset_qa_sync_test, - get_unique_id, ) +from tests.testing_utils import get_unique_id logger = logging.getLogger(__name__) diff --git a/tests/classification/classification_gcp_test.py b/tests/classification/classification_gcp_test.py index d3c444f0..a29cf13d 100644 --- a/tests/classification/classification_gcp_test.py +++ b/tests/classification/classification_gcp_test.py @@ -15,8 +15,8 @@ from tests.dataset_qa_shared import ( cleanup, dataset_qa_sync_test, - get_unique_id, ) +from tests.testing_utils import get_unique_id logger = logging.getLogger(__name__) diff --git a/tests/classification/sanity_gcp_test.py b/tests/classification/sanity_gcp_test.py index 8b1fc1d5..70f53f52 100644 --- a/tests/classification/sanity_gcp_test.py +++ b/tests/classification/sanity_gcp_test.py @@ -16,8 +16,8 @@ cleanup, dataset_qa_async_test, dataset_qa_sync_test, - get_unique_id, ) +from tests.testing_utils import get_unique_id logger = logging.getLogger(__name__) diff --git a/tests/dataset_qa_shared.py b/tests/dataset_qa_shared.py index 754290b8..6e49c407 100644 --- a/tests/dataset_qa_shared.py +++ b/tests/dataset_qa_shared.py @@ -16,12 +16,6 @@ logger = get_logger(__name__) -def get_unique_id(): - return ( - os.getenv("UNIQUE_ID", "").replace(".", "-").replace("/", "-").replace("+", "-") - ) - - def cleanup_conflict_by_unique_id(unique_id: typing.Optional[str]): if not unique_id: return diff --git a/tests/get_by_name_test.py b/tests/get_by_name_test.py index 78348f2c..43f35082 100644 --- a/tests/get_by_name_test.py +++ b/tests/get_by_name_test.py @@ -13,7 +13,7 @@ StorageGit, StorageTypes, ) -from tests.dataset_qa_shared import get_unique_id +from tests.testing_utils import get_unique_id unique_id = get_unique_id() gcp_storage_config_name = f"T-cifar1bucket_get_by_name{unique_id}" diff --git a/tests/object-detection/od_aws_test.py b/tests/object-detection/od_aws_test.py index da74d18d..3023908f 100644 --- a/tests/object-detection/od_aws_test.py +++ b/tests/object-detection/od_aws_test.py @@ -13,8 +13,8 @@ from tests.dataset_qa_shared import ( cleanup, dataset_qa_sync_test, - get_unique_id, ) +from tests.testing_utils import get_unique_id logger = logging.getLogger(__name__) diff --git a/tests/object-detection/od_git_test.py b/tests/object-detection/od_git_test.py index 9836ff62..194c1e22 100644 --- a/tests/object-detection/od_git_test.py +++ b/tests/object-detection/od_git_test.py @@ -13,8 +13,8 @@ from tests.dataset_qa_shared import ( cleanup, dataset_qa_sync_test, - get_unique_id, ) +from tests.testing_utils import get_unique_id logger = logging.getLogger(__name__) diff --git a/tests/object-detection/rockpaperscisssors_yolo_test.py b/tests/object-detection/rockpaperscisssors_yolo_test.py index 6bca0b34..d3767424 100644 --- a/tests/object-detection/rockpaperscisssors_yolo_test.py +++ b/tests/object-detection/rockpaperscisssors_yolo_test.py @@ -15,8 +15,8 @@ cleanup, dataset_qa_async_test, dataset_qa_sync_test, - get_unique_id, ) +from tests.testing_utils import get_unique_id logger = logging.getLogger(__name__) diff --git a/tests/object-detection/sama_coco_test.py b/tests/object-detection/sama_coco_test.py index 50fb23dd..81c29b48 100644 --- a/tests/object-detection/sama_coco_test.py +++ b/tests/object-detection/sama_coco_test.py @@ -15,8 +15,8 @@ cleanup, dataset_qa_async_test, dataset_qa_sync_test, - get_unique_id, ) +from tests.testing_utils import get_unique_id logger = logging.getLogger(__name__) diff --git a/tests/object-detection/sanity_aws_test.py b/tests/object-detection/sanity_aws_test.py index f5375a16..9cc9d032 100644 --- a/tests/object-detection/sanity_aws_test.py +++ b/tests/object-detection/sanity_aws_test.py @@ -15,8 +15,8 @@ cleanup, dataset_qa_async_test, dataset_qa_sync_test, - get_unique_id, ) +from tests.testing_utils import get_unique_id logger = logging.getLogger(__name__) diff --git a/tests/speech-to-text/sanity_stt_git_test.py b/tests/speech-to-text/sanity_stt_git_test.py index 104d5f5f..6b4afc65 100644 --- a/tests/speech-to-text/sanity_stt_git_test.py +++ b/tests/speech-to-text/sanity_stt_git_test.py @@ -16,8 +16,8 @@ from tests.dataset_qa_shared import ( cleanup, dataset_qa_sync_test, - get_unique_id, ) +from tests.testing_utils import get_unique_id logger = logging.getLogger(__name__) diff --git a/tests/speech-to-text/stt_git_test.py b/tests/speech-to-text/stt_git_test.py index 6a12a95f..c0798014 100644 --- a/tests/speech-to-text/stt_git_test.py +++ b/tests/speech-to-text/stt_git_test.py @@ -16,8 +16,8 @@ from tests.dataset_qa_shared import ( cleanup, dataset_qa_sync_test, - get_unique_id, ) +from tests.testing_utils import get_unique_id logger = logging.getLogger(__name__) diff --git a/tests/testing_utils.py b/tests/testing_utils.py new file mode 100644 index 00000000..4e8b25e5 --- /dev/null +++ b/tests/testing_utils.py @@ -0,0 +1,7 @@ +import os + + +def get_unique_id(): + return ( + os.getenv("UNIQUE_ID", "").replace(".", "-").replace("/", "-").replace("+", "-") + ) From ea216699f266e2873e4417de1141c8725378e31d Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Tue, 13 Jan 2026 22:43:37 +0200 Subject: [PATCH 03/25] Add small / Smol test Pun intended --- .../unlearn_llm_behavior_test.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/unlearning-llm/unlearn_llm_behavior_test.py diff --git a/tests/unlearning-llm/unlearn_llm_behavior_test.py b/tests/unlearning-llm/unlearn_llm_behavior_test.py new file mode 100644 index 00000000..33d5f604 --- /dev/null +++ b/tests/unlearning-llm/unlearn_llm_behavior_test.py @@ -0,0 +1,30 @@ +import logging + +from hirundo import ( + BiasRunInfo, + BiasType, + HuggingFaceTransformersModel, + LlmModel, + LlmUnlearningRun, +) +from tests.testing_utils import get_unique_id + +logger = logging.getLogger(__name__) + +unique_id = get_unique_id() + + +def test_unlearn_llm_behavior(): + llm_id = LlmModel( + model_name=f"TEST-UNLEARN-LLM-BEHAVIOR-SmolLM2-{unique_id}", + model_source=HuggingFaceTransformersModel( + model_name="HuggingFaceTB/SmolLM2-135M", + ), + ).create() + run_info = BiasRunInfo( + bias_type=BiasType.ALL, + ) + LlmUnlearningRun.launch( + llm_id, + run_info, + ) From a3737df1192c457824244d704ce2d32cfd8ee216 Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Tue, 13 Jan 2026 23:31:06 +0200 Subject: [PATCH 04/25] Switch to `nvidia/Nemotron-Flash-1B` since it turns out that SmolLM2-135M model is not a chat model --- tests/unlearning-llm/unlearn_llm_behavior_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unlearning-llm/unlearn_llm_behavior_test.py b/tests/unlearning-llm/unlearn_llm_behavior_test.py index 33d5f604..2be94259 100644 --- a/tests/unlearning-llm/unlearn_llm_behavior_test.py +++ b/tests/unlearning-llm/unlearn_llm_behavior_test.py @@ -16,9 +16,9 @@ def test_unlearn_llm_behavior(): llm_id = LlmModel( - model_name=f"TEST-UNLEARN-LLM-BEHAVIOR-SmolLM2-{unique_id}", + model_name=f"TEST-UNLEARN-LLM-BEHAVIOR-Nemotron-Flash-1B-{unique_id}", model_source=HuggingFaceTransformersModel( - model_name="HuggingFaceTB/SmolLM2-135M", + model_name="nvidia/Nemotron-Flash-1B", ), ).create() run_info = BiasRunInfo( From 4c968f0175b5abc792162d9a2a705d4b8720e787 Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Tue, 13 Jan 2026 23:44:10 +0200 Subject: [PATCH 05/25] Fix unlearning LLM run typing --- hirundo/unlearning_llm.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/hirundo/unlearning_llm.py b/hirundo/unlearning_llm.py index 36aa4325..2c6b3cfb 100644 --- a/hirundo/unlearning_llm.py +++ b/hirundo/unlearning_llm.py @@ -284,7 +284,11 @@ class BiasRunInfo(BaseModel): advanced_options: typing.Optional[UnlearningLlmAdvancedOptions] = None def to_run_info(self) -> LlmRunInfo: - default_utilities = self.target_utilities or [DefaultUtility()] + default_utilities = ( + self.target_utilities + if self.target_utilities is not None + else [DefaultUtility()] + ) return LlmRunInfo( organization_id=self.organization_id, name=self.name, @@ -308,13 +312,13 @@ def launch(model_id: int, run_info: typing.Union[LlmRunInfo, BiasRunInfo]) -> st ) raise_for_status_with_reason(run_response) run_response_json = run_response.json() if run_response.content else {} + if isinstance(run_response_json, str): + return run_response_json run_id = ( run_response_json.get("run_id") or run_response_json.get("hir_run_id") or run_response_json.get("id") ) - if isinstance(run_response_json, str): - return run_response_json if not run_id: raise ValueError("No run ID returned from launch request") return run_id @@ -361,7 +365,7 @@ def list( organization_id: typing.Optional[int] = None, archived: bool = False, ) -> list[dict[str, typing.Any]]: - params = {"archived": archived} + params: dict[str, typing.Any] = {"archived": archived} if organization_id is not None: params["unlearning_organization_id"] = organization_id run_response = requests.get( From 493191ebdc58725cf3f0410fb796d5eabfa92030 Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Tue, 13 Jan 2026 23:44:15 +0200 Subject: [PATCH 06/25] Refine LLM run list typing --- hirundo/unlearning_llm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hirundo/unlearning_llm.py b/hirundo/unlearning_llm.py index 2c6b3cfb..a4d52733 100644 --- a/hirundo/unlearning_llm.py +++ b/hirundo/unlearning_llm.py @@ -364,8 +364,8 @@ def restore(run_id: str) -> None: def list( organization_id: typing.Optional[int] = None, archived: bool = False, - ) -> list[dict[str, typing.Any]]: - params: dict[str, typing.Any] = {"archived": archived} + ) -> list[dict[str, object]]: + params: dict[str, bool | int] = {"archived": archived} if organization_id is not None: params["unlearning_organization_id"] = organization_id run_response = requests.get( From 32c2096143f162695c10b1565f61d462a1ba54fa Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Tue, 13 Jan 2026 23:44:22 +0200 Subject: [PATCH 07/25] Type unlearning run list responses --- hirundo/unlearning_llm.py | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/hirundo/unlearning_llm.py b/hirundo/unlearning_llm.py index a4d52733..845403e6 100644 --- a/hirundo/unlearning_llm.py +++ b/hirundo/unlearning_llm.py @@ -298,6 +298,31 @@ def to_run_info(self) -> LlmRunInfo: ) +OutputLlm = dict[str, object] +BehaviorOptions = TargetBehavior +UtilityOptions = TargetUtility +CeleryTaskState = str + + +class OutputUnlearningLlmRun(BaseModel): + id: int + name: str + model_id: int + model: OutputLlm + target_behaviors: list[BehaviorOptions] + target_utilities: list[UtilityOptions] + advanced_options: UnlearningLlmAdvancedOptions | None + run_id: str + mlflow_run_id: str | None + status: CeleryTaskState + approved: bool + created_at: datetime.datetime + completed_at: datetime.datetime | None + pre_process_progress: float + optimization_progress: float + post_process_progress: float + + class LlmUnlearningRun: @staticmethod def launch(model_id: int, run_info: typing.Union[LlmRunInfo, BiasRunInfo]) -> str: @@ -364,7 +389,7 @@ def restore(run_id: str) -> None: def list( organization_id: typing.Optional[int] = None, archived: bool = False, - ) -> list[dict[str, object]]: + ) -> list[OutputUnlearningLlmRun]: params: dict[str, bool | int] = {"archived": archived} if organization_id is not None: params["unlearning_organization_id"] = organization_id @@ -377,5 +402,8 @@ def list( raise_for_status_with_reason(run_response) response_json = run_response.json() if isinstance(response_json, list): - return response_json - return [response_json] + return [ + OutputUnlearningLlmRun.model_validate(run_payload) + for run_payload in response_json + ] + return [OutputUnlearningLlmRun.model_validate(response_json)] From 0349fae99e025d5707c99422d3cdf73f4083c8b3 Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Wed, 14 Jan 2026 00:19:12 +0200 Subject: [PATCH 08/25] Handle empty target utilities explicitly --- hirundo/unlearning_llm.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hirundo/unlearning_llm.py b/hirundo/unlearning_llm.py index 845403e6..02df79cb 100644 --- a/hirundo/unlearning_llm.py +++ b/hirundo/unlearning_llm.py @@ -285,9 +285,9 @@ class BiasRunInfo(BaseModel): def to_run_info(self) -> LlmRunInfo: default_utilities = ( - self.target_utilities - if self.target_utilities is not None - else [DefaultUtility()] + [DefaultUtility()] + if self.target_utilities is None + else list(self.target_utilities) ) return LlmRunInfo( organization_id=self.organization_id, From b1b182368e131dd45bfc59c13952c1253078f029 Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Wed, 14 Jan 2026 00:39:28 +0200 Subject: [PATCH 09/25] Fix Ruff lint errors and basedpyright errors Python version was upgraded to Python 3.10 and new syntax can now be used --- hirundo/unlearning_llm.py | 65 +++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 36 deletions(-) diff --git a/hirundo/unlearning_llm.py b/hirundo/unlearning_llm.py index 02df79cb..ae8eff19 100644 --- a/hirundo/unlearning_llm.py +++ b/hirundo/unlearning_llm.py @@ -1,9 +1,9 @@ import datetime import typing from enum import Enum +from typing import Literal from pydantic import BaseModel -from typing_extensions import Literal from hirundo._env import API_HOST from hirundo._headers import get_headers @@ -23,10 +23,10 @@ class HuggingFaceTransformersModel(BaseModel): type: Literal[ModelSourceType.HUGGINGFACE_TRANSFORMERS] = ( ModelSourceType.HUGGINGFACE_TRANSFORMERS ) - revision: typing.Optional[str] = None - code_revision: typing.Optional[str] = None + revision: str | None = None + code_revision: str | None = None model_name: str - token: typing.Optional[str] = None + token: str | None = None class HuggingFaceTransformersModelOutput(BaseModel): @@ -45,15 +45,13 @@ class LocalTransformersModel(BaseModel): local_path: str -LlmSources = typing.Union[HuggingFaceTransformersModel, LocalTransformersModel] -LlmSourcesOutput = typing.Union[ - HuggingFaceTransformersModelOutput, LocalTransformersModel -] +LlmSources = HuggingFaceTransformersModel | LocalTransformersModel +LlmSourcesOutput = HuggingFaceTransformersModelOutput | LocalTransformersModel class LlmModel(BaseModel): - id: typing.Optional[int] = None - organization_id: typing.Optional[int] = None + id: int | None = None + organization_id: int | None = None model_name: str model_source: LlmSources archive_existing_runs: bool = True @@ -97,7 +95,7 @@ def get_by_name(llm_model_name: str) -> "LlmModelOut": return LlmModelOut.from_response(llm_model_response.json()) @staticmethod - def list(organization_id: typing.Optional[int] = None) -> list["LlmModelOut"]: + def list(organization_id: int | None = None) -> list["LlmModelOut"]: params = {} if organization_id is not None: params["model_organization_id"] = organization_id @@ -128,9 +126,9 @@ def delete(self) -> None: def update( self, - model_name: typing.Optional[str] = None, - model_source: typing.Optional[LlmSources] = None, - archive_existing_runs: typing.Optional[bool] = None, + model_name: str | None = None, + model_source: LlmSources | None = None, + archive_existing_runs: bool | None = None, ) -> None: if not self.id: raise ValueError("No LLM model has been created") @@ -188,9 +186,7 @@ class DatasetType(str, Enum): class UnlearningLlmAdvancedOptions(BaseModel): - max_tokens_for_model: typing.Optional[typing.Union[dict[DatasetType, int], int]] = ( - None - ) + max_tokens_for_model: dict[DatasetType, int] | int | None = None class BiasType(str, Enum): @@ -222,7 +218,7 @@ class HuggingFaceDataset(BaseModel): hugging_face_dataset_name: str -CustomDataset = typing.Union[HirundoCSVDataset, HuggingFaceDataset] +CustomDataset = HirundoCSVDataset | HuggingFaceDataset class CustomUtility(BaseModel): @@ -257,34 +253,31 @@ class CustomBehavior(BaseModel): unbiased_dataset: CustomDataset -TargetBehavior = typing.Union[ - BiasBehavior, - HallucinationBehavior, - SecurityBehavior, - CustomBehavior, -] +TargetBehavior = ( + BiasBehavior | HallucinationBehavior | SecurityBehavior | CustomBehavior +) -TargetUtility = typing.Union[DefaultUtility, CustomUtility] +TargetUtility = DefaultUtility | CustomUtility class LlmRunInfo(BaseModel): - organization_id: typing.Optional[int] = None - name: typing.Optional[str] = None - model_id: typing.Optional[int] = None + organization_id: int | None = None + name: str | None = None + model_id: int | None = None target_behaviors: list[TargetBehavior] target_utilities: list[TargetUtility] - advanced_options: typing.Optional[UnlearningLlmAdvancedOptions] = None + advanced_options: UnlearningLlmAdvancedOptions | None = None class BiasRunInfo(BaseModel): bias_type: BiasType - organization_id: typing.Optional[int] = None - name: typing.Optional[str] = None - target_utilities: typing.Optional[list[TargetUtility]] = None - advanced_options: typing.Optional[UnlearningLlmAdvancedOptions] = None + organization_id: int | None = None + name: str | None = None + target_utilities: list[TargetUtility] | None = None + advanced_options: UnlearningLlmAdvancedOptions | None = None def to_run_info(self) -> LlmRunInfo: - default_utilities = ( + default_utilities: list[TargetUtility] = ( [DefaultUtility()] if self.target_utilities is None else list(self.target_utilities) @@ -325,7 +318,7 @@ class OutputUnlearningLlmRun(BaseModel): class LlmUnlearningRun: @staticmethod - def launch(model_id: int, run_info: typing.Union[LlmRunInfo, BiasRunInfo]) -> str: + def launch(model_id: int, run_info: LlmRunInfo | BiasRunInfo) -> str: resolved_run_info = ( run_info.to_run_info() if isinstance(run_info, BiasRunInfo) else run_info ) @@ -387,7 +380,7 @@ def restore(run_id: str) -> None: @staticmethod def list( - organization_id: typing.Optional[int] = None, + organization_id: int | None = None, archived: bool = False, ) -> list[OutputUnlearningLlmRun]: params: dict[str, bool | int] = {"archived": archived} From ec8938017c247161937d42eec400c7f088ce2703 Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Wed, 14 Jan 2026 23:28:50 +0200 Subject: [PATCH 10/25] Add `check_run` and `acheck_run` (and `check_run_by_id` and `acheck_run_by_id`) for unlearning LLM behavior --- hirundo/unlearning_llm.py | 266 +++++++++++++++++++++++++++++++++++++- 1 file changed, 265 insertions(+), 1 deletion(-) diff --git a/hirundo/unlearning_llm.py b/hirundo/unlearning_llm.py index ae8eff19..089f28c6 100644 --- a/hirundo/unlearning_llm.py +++ b/hirundo/unlearning_llm.py @@ -1,18 +1,27 @@ import datetime +import json import typing +from collections.abc import AsyncGenerator, Generator from enum import Enum -from typing import Literal +from typing import Literal, overload +import httpx from pydantic import BaseModel +from tqdm import tqdm +from tqdm.contrib.logging import logging_redirect_tqdm from hirundo._env import API_HOST from hirundo._headers import get_headers from hirundo._http import raise_for_status_with_reason, requests +from hirundo._iter_sse_retrying import aiter_sse_retrying, iter_sse_retrying from hirundo._timeouts import MODIFY_TIMEOUT, READ_TIMEOUT +from hirundo.dataset_qa import HirundoError from hirundo.logger import get_logger logger = get_logger(__name__) +MAX_RETRIES = 200 # Max 200 retries for HTTP SSE connection + class ModelSourceType(str, Enum): HUGGINGFACE_TRANSFORMERS = "huggingface_transformers" @@ -316,6 +325,39 @@ class OutputUnlearningLlmRun(BaseModel): post_process_progress: float +class RunStatus(Enum): + PENDING = "PENDING" + STARTED = "STARTED" + SUCCESS = "SUCCESS" + FAILURE = "FAILURE" + AWAITING_MANUAL_APPROVAL = "AWAITING MANUAL APPROVAL" + REVOKED = "REVOKED" + REJECTED = "REJECTED" + RETRY = "RETRY" + + +STATUS_TO_TEXT_MAP = { + RunStatus.STARTED.value: "LLM unlearning run in progress", + RunStatus.PENDING.value: "LLM unlearning run queued and not yet started", + RunStatus.SUCCESS.value: "LLM unlearning run completed successfully", + RunStatus.FAILURE.value: "LLM unlearning run failed", + RunStatus.AWAITING_MANUAL_APPROVAL.value: "Awaiting manual approval", + RunStatus.RETRY.value: "LLM unlearning run failed. Retrying", + RunStatus.REVOKED.value: "LLM unlearning run was cancelled", + RunStatus.REJECTED.value: "LLM unlearning run was rejected", +} +STATUS_TO_PROGRESS_MAP = { + RunStatus.STARTED.value: 0.0, + RunStatus.PENDING.value: 0.0, + RunStatus.SUCCESS.value: 100.0, + RunStatus.FAILURE.value: 100.0, + RunStatus.AWAITING_MANUAL_APPROVAL.value: 100.0, + RunStatus.RETRY.value: 0.0, + RunStatus.REVOKED.value: 100.0, + RunStatus.REJECTED.value: 0.0, +} + + class LlmUnlearningRun: @staticmethod def launch(model_id: int, run_info: LlmRunInfo | BiasRunInfo) -> str: @@ -400,3 +442,225 @@ def list( for run_payload in response_json ] return [OutputUnlearningLlmRun.model_validate(response_json)] + + @staticmethod + def _check_run_by_id(run_id: str, retry=0) -> Generator[dict, None, None]: + if retry > MAX_RETRIES: + raise HirundoError("Max retries reached") + last_event = None + with httpx.Client(timeout=httpx.Timeout(None, connect=5.0)) as client: + for sse in iter_sse_retrying( + client, + "GET", + f"{API_HOST}/unlearning-llm/run/{run_id}", + headers=get_headers(), + ): + if sse.event == "ping": + continue + logger.debug( + "[SYNC] received event: %s with data: %s and ID: %s and retry: %s", + sse.event, + sse.data, + sse.id, + sse.retry, + ) + last_event = json.loads(sse.data) + if not last_event: + continue + if "data" in last_event: + data = last_event["data"] + else: + if "detail" in last_event: + raise HirundoError(last_event["detail"]) + elif "reason" in last_event: + raise HirundoError(last_event["reason"]) + else: + raise HirundoError("Unknown error") + yield data + last_state = None + if last_event and "data" in last_event: + last_state = last_event["data"].get("state") or last_event["data"].get( + "status" + ) + if not last_event or last_state == RunStatus.PENDING.value: + LlmUnlearningRun._check_run_by_id(run_id, retry + 1) + + @staticmethod + def _handle_failure(iteration: dict) -> None: + if iteration.get("result"): + raise HirundoError( + f"LLM unlearning run failed with error: {iteration['result']}" + ) + raise HirundoError("LLM unlearning run failed with an unknown error") + + @staticmethod + @overload + def check_run_by_id( + run_id: str, stop_on_manual_approval: Literal[True] + ) -> typing.Any | None: ... + + @staticmethod + @overload + def check_run_by_id( + run_id: str, stop_on_manual_approval: Literal[False] = False + ) -> typing.Any: ... + + @staticmethod + @overload + def check_run_by_id( + run_id: str, stop_on_manual_approval: bool + ) -> typing.Any | None: ... + + @staticmethod + def check_run_by_id(run_id: str, stop_on_manual_approval: bool = False): + """ + Check the status of a run given its ID + + Args: + run_id: The `run_id` produced by a `launch` call + stop_on_manual_approval: If True, the function will return `None` if the run is awaiting manual approval + + Returns: + The result payload for the run, if available + + Raises: + HirundoError: If the maximum number of retries is reached or if the run fails + """ + logger.debug("Checking run with ID: %s", run_id) + with logging_redirect_tqdm(): + t = tqdm(total=100.0) + for iteration in LlmUnlearningRun._check_run_by_id(run_id): + state = iteration.get("state") or iteration.get("status") + if state in STATUS_TO_PROGRESS_MAP: + t.set_description(STATUS_TO_TEXT_MAP[state]) + t.n = STATUS_TO_PROGRESS_MAP[state] + logger.debug("Setting progress to %s", t.n) + t.refresh() + if state in [ + RunStatus.FAILURE.value, + RunStatus.REJECTED.value, + RunStatus.REVOKED.value, + ]: + logger.error( + "State is failure, rejected, or revoked: %s", + state, + ) + LlmUnlearningRun._handle_failure(iteration) + elif state == RunStatus.SUCCESS.value: + t.close() + return iteration.get("result") or iteration + elif ( + state == RunStatus.AWAITING_MANUAL_APPROVAL.value + and stop_on_manual_approval + ): + t.close() + return None + elif state is None: + if ( + iteration.get("result") + and isinstance(iteration["result"], dict) + and iteration["result"].get("result") + and isinstance(iteration["result"]["result"], str) + ): + result_info = iteration["result"]["result"].split(":") + if len(result_info) > 1: + stage = result_info[0] + current_progress_percentage = float( + result_info[1].removeprefix(" ").removesuffix("% done") + ) + elif len(result_info) == 1: + stage = result_info[0] + current_progress_percentage = t.n + else: + stage = "Unknown progress state" + current_progress_percentage = t.n + desc = ( + "LLM unlearning run completed. Uploading results" + if current_progress_percentage == 100.0 + else stage + ) + t.set_description(desc) + t.n = current_progress_percentage + logger.debug("Setting progress to %s", t.n) + t.refresh() + raise HirundoError("LLM unlearning run failed with an unknown error") + + @staticmethod + def check_run(run_id: str, stop_on_manual_approval: bool = False): + """ + Check the status of the given run. + + Returns: + The result payload for the run, if available + """ + return LlmUnlearningRun.check_run_by_id(run_id, stop_on_manual_approval) + + @staticmethod + async def acheck_run_by_id(run_id: str, retry=0) -> AsyncGenerator[dict, None]: + """ + Async version of :func:`check_run_by_id` + + Check the status of a run given its ID. + + This generator will produce values to show progress of the run. + + Args: + run_id: The `run_id` produced by a `launch` call + retry: A number used to track the number of retries to limit re-checks. *Do not* provide this value manually. + + Yields: + Each event will be a dict, where: + - `"state"` is PENDING, STARTED, RETRY, FAILURE or SUCCESS + - `"result"` is a string describing the progress as a percentage for a PENDING state, or the error for a FAILURE state or the results for a SUCCESS state + + """ + logger.debug("Checking run with ID: %s", run_id) + if retry > MAX_RETRIES: + raise HirundoError("Max retries reached") + last_event = None + async with httpx.AsyncClient( + timeout=httpx.Timeout(None, connect=5.0) + ) as client: + async_iterator = await aiter_sse_retrying( + client, + "GET", + f"{API_HOST}/unlearning-llm/run/{run_id}", + headers=get_headers(), + ) + async for sse in async_iterator: + if sse.event == "ping": + continue + logger.debug( + "[ASYNC] Received event: %s with data: %s and ID: %s and retry: %s", + sse.event, + sse.data, + sse.id, + sse.retry, + ) + last_event = json.loads(sse.data) + yield last_event["data"] + last_state = None + if last_event and "data" in last_event: + last_state = last_event["data"].get("state") or last_event["data"].get( + "status" + ) + if not last_event or last_state == RunStatus.PENDING.value: + LlmUnlearningRun.acheck_run_by_id(run_id, retry + 1) + + @staticmethod + async def acheck_run(run_id: str) -> AsyncGenerator[dict, None]: + """ + Async version of :func:`check_run` + + Check the status of the given run. + + This generator will produce values to show progress of the run. + + Yields: + Each event will be a dict, where: + - `"state"` is PENDING, STARTED, RETRY, FAILURE or SUCCESS + - `"result"` is a string describing the progress as a percentage for a PENDING state, or the error for a FAILURE state or the results for a SUCCESS state + + """ + async for iteration in LlmUnlearningRun.acheck_run_by_id(run_id): + yield iteration From be58f5d07e145394fbd79dc62de09d36d1ed68ab Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Thu, 15 Jan 2026 09:47:30 +0200 Subject: [PATCH 11/25] Reduce duplicate code --- hirundo/_run_checking.py | 287 ++++++++++++++++++++++++++++++++++++++ hirundo/dataset_qa.py | 189 +++++++------------------ hirundo/unlearning_llm.py | 183 ++++++------------------ 3 files changed, 376 insertions(+), 283 deletions(-) create mode 100644 hirundo/_run_checking.py diff --git a/hirundo/_run_checking.py b/hirundo/_run_checking.py new file mode 100644 index 00000000..a0245039 --- /dev/null +++ b/hirundo/_run_checking.py @@ -0,0 +1,287 @@ +import json +from collections.abc import AsyncGenerator, Generator +from enum import Enum + +import httpx +from tqdm import tqdm + +from hirundo._iter_sse_retrying import aiter_sse_retrying, iter_sse_retrying +from hirundo.logger import get_logger + +_logger = get_logger(__name__) + +DEFAULT_MAX_RETRIES = 200 + + +class RunStatus(Enum): + PENDING = "PENDING" + STARTED = "STARTED" + SUCCESS = "SUCCESS" + FAILURE = "FAILURE" + AWAITING_MANUAL_APPROVAL = "AWAITING MANUAL APPROVAL" + REVOKED = "REVOKED" + REJECTED = "REJECTED" + RETRY = "RETRY" + + +STATUS_TO_PROGRESS_MAP = { + RunStatus.STARTED.value: 0.0, + RunStatus.PENDING.value: 0.0, + RunStatus.SUCCESS.value: 100.0, + RunStatus.FAILURE.value: 100.0, + RunStatus.AWAITING_MANUAL_APPROVAL.value: 100.0, + RunStatus.RETRY.value: 0.0, + RunStatus.REVOKED.value: 100.0, + RunStatus.REJECTED.value: 0.0, +} + + +def build_status_text_map( + run_label: str, *, started_detail: str | None = None +) -> dict[str, str]: + """ + Build a status->text mapping for a given run label. + + Args: + run_label: Human-readable label used in status text. + started_detail: Optional override for the STARTED status text. + + Returns: + Mapping of run state values to user-facing status text. + """ + started_text = started_detail or f"{run_label} run in progress" + return { + RunStatus.STARTED.value: started_text, + RunStatus.PENDING.value: f"{run_label} run queued and not yet started", + RunStatus.SUCCESS.value: f"{run_label} run completed successfully", + RunStatus.FAILURE.value: f"{run_label} run failed", + RunStatus.AWAITING_MANUAL_APPROVAL.value: "Awaiting manual approval", + RunStatus.RETRY.value: f"{run_label} run failed. Retrying", + RunStatus.REVOKED.value: f"{run_label} run was cancelled", + RunStatus.REJECTED.value: f"{run_label} run was rejected", + } + + +def get_state(payload: dict, status_keys: tuple[str, ...]) -> str | None: + """ + Return the first non-null state value from a payload using a list of keys. + + Args: + payload: Run payload containing state/status information. + status_keys: Ordered keys to search for state values. + + Returns: + The first non-null state value, or None if none are present. + """ + for key in status_keys: + value = payload.get(key) + if value is not None: + return value + return None + + +def iter_run_events( + url: str, + *, + headers: dict[str, str] | None = None, + retry: int = 0, + max_retries: int = DEFAULT_MAX_RETRIES, + pending_state_value: str = RunStatus.PENDING.value, + status_keys: tuple[str, ...] = ("state",), + error_cls: type[Exception] = RuntimeError, + log=_logger, +) -> Generator[dict, None, None]: + """ + Stream run events from an SSE endpoint with retries. + + Args: + url: SSE endpoint URL. + headers: Optional HTTP headers. + retry: Internal retry counter (do not set manually). + max_retries: Maximum number of retry attempts. + pending_state_value: State value that triggers a re-check loop. + status_keys: Payload keys to search for the run state. + error_cls: Exception type to raise on errors. + log: Logger instance for debug output. + + Yields: + Event payloads decoded from the SSE data field. + """ + if retry > max_retries: + raise error_cls("Max retries reached") + last_event = None + with httpx.Client(timeout=httpx.Timeout(None, connect=5.0)) as client: + for sse in iter_sse_retrying( + client, + "GET", + url, + headers=headers, + ): + if sse.event == "ping": + continue + log.debug( + "[SYNC] received event: %s with data: %s and ID: %s and retry: %s", + sse.event, + sse.data, + sse.id, + sse.retry, + ) + last_event = json.loads(sse.data) + if not last_event: + continue + if "data" in last_event: + data = last_event["data"] + else: + if "detail" in last_event: + raise error_cls(last_event["detail"]) + if "reason" in last_event: + raise error_cls(last_event["reason"]) + raise error_cls("Unknown error") + yield data + last_state = None + if last_event and "data" in last_event: + last_state = get_state(last_event["data"], status_keys) + if not last_event or last_state == pending_state_value: + iter_run_events( + url, + headers=headers, + retry=retry + 1, + max_retries=max_retries, + pending_state_value=pending_state_value, + status_keys=status_keys, + error_cls=error_cls, + log=log, + ) + + +async def aiter_run_events( + url: str, + *, + headers: dict[str, str] | None = None, + retry: int = 0, + max_retries: int = DEFAULT_MAX_RETRIES, + pending_state_value: str = RunStatus.PENDING.value, + status_keys: tuple[str, ...] = ("state",), + error_cls: type[Exception] = RuntimeError, + log=_logger, +) -> AsyncGenerator[dict, None]: + """ + Async stream run events from an SSE endpoint with retries. + + Args: + url: SSE endpoint URL. + headers: Optional HTTP headers. + retry: Internal retry counter (do not set manually). + max_retries: Maximum number of retry attempts. + pending_state_value: State value that triggers a re-check loop. + status_keys: Payload keys to search for the run state. + error_cls: Exception type to raise on errors. + log: Logger instance for debug output. + + Yields: + Event payloads decoded from the SSE data field. + """ + if retry > max_retries: + raise error_cls("Max retries reached") + last_event = None + async with httpx.AsyncClient(timeout=httpx.Timeout(None, connect=5.0)) as client: + async_iterator = await aiter_sse_retrying( + client, + "GET", + url, + headers=headers or {}, + ) + async for sse in async_iterator: + if sse.event == "ping": + continue + log.debug( + "[ASYNC] Received event: %s with data: %s and ID: %s and retry: %s", + sse.event, + sse.data, + sse.id, + sse.retry, + ) + last_event = json.loads(sse.data) + if "data" not in last_event: + if "detail" in last_event: + raise error_cls(last_event["detail"]) + if "reason" in last_event: + raise error_cls(last_event["reason"]) + raise error_cls("Unknown error") + yield last_event["data"] + last_state = None + if last_event and "data" in last_event: + last_state = get_state(last_event["data"], status_keys) + if not last_event or last_state == pending_state_value: + aiter_run_events( + url, + headers=headers, + retry=retry + 1, + max_retries=max_retries, + pending_state_value=pending_state_value, + status_keys=status_keys, + error_cls=error_cls, + log=log, + ) + + +def update_progress_from_result( + iteration: dict, + progress: tqdm, + *, + uploading_text: str, + log=_logger, +) -> bool: + """ + Update a tqdm progress bar based on a serialized progress result string. + + Args: + iteration: Payload containing a nested result string. + progress: tqdm instance to update. + uploading_text: Description to show when progress reaches 100%. + log: Logger instance for debug output. + + Returns: + True if a progress update occurred, False otherwise. + """ + if ( + iteration.get("result") + and isinstance(iteration["result"], dict) + and iteration["result"].get("result") + and isinstance(iteration["result"]["result"], str) + ): + result_info = iteration["result"]["result"].split(":") + if len(result_info) > 1: + stage = result_info[0] + current_progress_percentage = float( + result_info[1].removeprefix(" ").removesuffix("% done") + ) + elif len(result_info) == 1: + stage = result_info[0] + current_progress_percentage = progress.n + else: + stage = "Unknown progress state" + current_progress_percentage = progress.n + desc = uploading_text if current_progress_percentage == 100.0 else stage + progress.set_description(desc) + progress.n = current_progress_percentage + log.debug("Setting progress to %s", progress.n) + progress.refresh() + return True + return False + + +def handle_run_failure( + iteration: dict, *, error_cls: type[Exception], run_label: str +) -> None: + """ + Raise a run-specific failure exception based on the iteration payload. + + Args: + iteration: Payload containing error details. + error_cls: Exception type to raise. + run_label: Human-readable label for the run type. + """ + if iteration.get("result"): + raise error_cls(f"{run_label} run failed with error: {iteration['result']}") + raise error_cls(f"{run_label} run failed with an unknown error") diff --git a/hirundo/dataset_qa.py b/hirundo/dataset_qa.py index 994c192e..bea1ecda 100644 --- a/hirundo/dataset_qa.py +++ b/hirundo/dataset_qa.py @@ -1,11 +1,9 @@ import datetime -import json import typing from collections.abc import AsyncGenerator, Generator from enum import Enum from typing import overload -import httpx from pydantic import BaseModel, Field, model_validator from tqdm import tqdm from tqdm.contrib.logging import logging_redirect_tqdm @@ -14,7 +12,16 @@ from hirundo._env import API_HOST from hirundo._headers import get_headers from hirundo._http import raise_for_status_with_reason, requests -from hirundo._iter_sse_retrying import aiter_sse_retrying, iter_sse_retrying +from hirundo._run_checking import ( + STATUS_TO_PROGRESS_MAP, + RunStatus, + aiter_run_events, + build_status_text_map, + get_state, + handle_run_failure, + iter_run_events, + update_progress_from_result, +) from hirundo._timeouts import MODIFY_TIMEOUT, READ_TIMEOUT from hirundo._urls import HirundoUrl from hirundo.dataset_enum import DatasetMetadataType, LabelingType @@ -35,40 +42,10 @@ class HirundoError(Exception): pass -MAX_RETRIES = 200 # Max 200 retries for HTTP SSE connection - - -class RunStatus(Enum): - PENDING = "PENDING" - STARTED = "STARTED" - SUCCESS = "SUCCESS" - FAILURE = "FAILURE" - AWAITING_MANUAL_APPROVAL = "AWAITING MANUAL APPROVAL" - REVOKED = "REVOKED" - REJECTED = "REJECTED" - RETRY = "RETRY" - - -STATUS_TO_TEXT_MAP = { - RunStatus.STARTED.value: "Dataset QA run in progress. Downloading dataset", - RunStatus.PENDING.value: "Dataset QA run queued and not yet started", - RunStatus.SUCCESS.value: "Dataset QA run completed successfully", - RunStatus.FAILURE.value: "Dataset QA run failed", - RunStatus.AWAITING_MANUAL_APPROVAL.value: "Awaiting manual approval", - RunStatus.RETRY.value: "Dataset QA run failed. Retrying", - RunStatus.REVOKED.value: "Dataset QA run was cancelled", - RunStatus.REJECTED.value: "Dataset QA run was rejected", -} -STATUS_TO_PROGRESS_MAP = { - RunStatus.STARTED.value: 0.0, - RunStatus.PENDING.value: 0.0, - RunStatus.SUCCESS.value: 100.0, - RunStatus.FAILURE.value: 100.0, - RunStatus.AWAITING_MANUAL_APPROVAL.value: 100.0, - RunStatus.RETRY.value: 0.0, - RunStatus.REVOKED.value: 100.0, - RunStatus.REJECTED.value: 0.0, -} +STATUS_TO_TEXT_MAP = build_status_text_map( + "Dataset QA", + started_detail="Dataset QA run in progress. Downloading dataset", +) class ClassificationRunArgs(BaseModel): @@ -559,47 +536,14 @@ def clean_ids(self): @staticmethod def _check_run_by_id(run_id: str, retry=0) -> Generator[dict, None, None]: - if retry > MAX_RETRIES: - raise HirundoError("Max retries reached") - last_event = None - with httpx.Client(timeout=httpx.Timeout(None, connect=5.0)) as client: - for sse in iter_sse_retrying( - client, - "GET", - f"{API_HOST}/dataset-qa/run/{run_id}", - headers=get_headers(), - ): - if sse.event == "ping": - continue - logger.debug( - "[SYNC] received event: %s with data: %s and ID: %s and retry: %s", - sse.event, - sse.data, - sse.id, - sse.retry, - ) - last_event = json.loads(sse.data) - if not last_event: - continue - if "data" in last_event: - data = last_event["data"] - else: - if "detail" in last_event: - raise HirundoError(last_event["detail"]) - elif "reason" in last_event: - raise HirundoError(last_event["reason"]) - else: - raise HirundoError("Unknown error") - yield data - if not last_event or last_event["data"]["state"] == RunStatus.PENDING.value: - QADataset._check_run_by_id(run_id, retry + 1) - - @staticmethod - def _handle_failure(iteration: dict): - if iteration["result"]: - raise HirundoError(f"QA run failed with error: {iteration['result']}") - else: - raise HirundoError("QA run failed with an unknown error in _handle_failure") + yield from iter_run_events( + f"{API_HOST}/dataset-qa/run/{run_id}", + headers=get_headers(), + retry=retry, + status_keys=("state",), + error_cls=HirundoError, + log=logger, + ) @staticmethod @overload @@ -640,22 +584,25 @@ def check_run_by_id( with logging_redirect_tqdm(): t = tqdm(total=100.0) for iteration in QADataset._check_run_by_id(run_id): - if iteration["state"] in STATUS_TO_PROGRESS_MAP: - t.set_description(STATUS_TO_TEXT_MAP[iteration["state"]]) - t.n = STATUS_TO_PROGRESS_MAP[iteration["state"]] + state = get_state(iteration, ("state",)) + if state in STATUS_TO_PROGRESS_MAP: + t.set_description(STATUS_TO_TEXT_MAP[state]) + t.n = STATUS_TO_PROGRESS_MAP[state] logger.debug("Setting progress to %s", t.n) t.refresh() - if iteration["state"] in [ + if state in [ RunStatus.FAILURE.value, RunStatus.REJECTED.value, RunStatus.REVOKED.value, ]: logger.error( "State is failure, rejected, or revoked: %s", - iteration["state"], + state, + ) + handle_run_failure( + iteration, error_cls=HirundoError, run_label="QA" ) - QADataset._handle_failure(iteration) - elif iteration["state"] == RunStatus.SUCCESS.value: + elif state == RunStatus.SUCCESS.value: t.close() zip_temporary_url = iteration["result"] logger.debug("QA run completed. Downloading results") @@ -665,39 +612,18 @@ def check_run_by_id( zip_temporary_url, ) elif ( - iteration["state"] == RunStatus.AWAITING_MANUAL_APPROVAL.value + state == RunStatus.AWAITING_MANUAL_APPROVAL.value and stop_on_manual_approval ): t.close() return None - elif iteration["state"] is None: - if ( - iteration["result"] - and isinstance(iteration["result"], dict) - and iteration["result"]["result"] - and isinstance(iteration["result"]["result"], str) - ): - result_info = iteration["result"]["result"].split(":") - if len(result_info) > 1: - stage = result_info[0] - current_progress_percentage = float( - result_info[1].removeprefix(" ").removesuffix("% done") - ) - elif len(result_info) == 1: - stage = result_info[0] - current_progress_percentage = t.n # Keep the same progress - else: - stage = "Unknown progress state" - current_progress_percentage = t.n # Keep the same progress - desc = ( - "QA run completed. Uploading results" - if current_progress_percentage == 100.0 - else stage - ) - t.set_description(desc) - t.n = current_progress_percentage - logger.debug("Setting progress to %s", t.n) - t.refresh() + elif state is None: + update_progress_from_result( + iteration, + t, + uploading_text="QA run completed. Uploading results", + log=logger, + ) raise HirundoError("QA run failed with an unknown error in check_run_by_id") @overload @@ -744,32 +670,15 @@ async def acheck_run_by_id(run_id: str, retry=0) -> AsyncGenerator[dict, None]: """ logger.debug("Checking run with ID: %s", run_id) - if retry > MAX_RETRIES: - raise HirundoError("Max retries reached") - last_event = None - async with httpx.AsyncClient( - timeout=httpx.Timeout(None, connect=5.0) - ) as client: - async_iterator = await aiter_sse_retrying( - client, - "GET", - f"{API_HOST}/dataset-qa/run/{run_id}", - headers=get_headers(), - ) - async for sse in async_iterator: - if sse.event == "ping": - continue - logger.debug( - "[ASYNC] Received event: %s with data: %s and ID: %s and retry: %s", - sse.event, - sse.data, - sse.id, - sse.retry, - ) - last_event = json.loads(sse.data) - yield last_event["data"] - if not last_event or last_event["data"]["state"] == RunStatus.PENDING.value: - QADataset.acheck_run_by_id(run_id, retry + 1) + async for iteration in aiter_run_events( + f"{API_HOST}/dataset-qa/run/{run_id}", + headers=get_headers(), + retry=retry, + status_keys=("state",), + error_cls=HirundoError, + log=logger, + ): + yield iteration async def acheck_run(self) -> AsyncGenerator[dict, None]: """ diff --git a/hirundo/unlearning_llm.py b/hirundo/unlearning_llm.py index 089f28c6..2ca839fe 100644 --- a/hirundo/unlearning_llm.py +++ b/hirundo/unlearning_llm.py @@ -1,11 +1,9 @@ import datetime -import json import typing from collections.abc import AsyncGenerator, Generator from enum import Enum from typing import Literal, overload -import httpx from pydantic import BaseModel from tqdm import tqdm from tqdm.contrib.logging import logging_redirect_tqdm @@ -13,15 +11,22 @@ from hirundo._env import API_HOST from hirundo._headers import get_headers from hirundo._http import raise_for_status_with_reason, requests -from hirundo._iter_sse_retrying import aiter_sse_retrying, iter_sse_retrying +from hirundo._run_checking import ( + STATUS_TO_PROGRESS_MAP, + RunStatus, + aiter_run_events, + build_status_text_map, + get_state, + handle_run_failure, + iter_run_events, + update_progress_from_result, +) from hirundo._timeouts import MODIFY_TIMEOUT, READ_TIMEOUT from hirundo.dataset_qa import HirundoError from hirundo.logger import get_logger logger = get_logger(__name__) -MAX_RETRIES = 200 # Max 200 retries for HTTP SSE connection - class ModelSourceType(str, Enum): HUGGINGFACE_TRANSFORMERS = "huggingface_transformers" @@ -325,37 +330,7 @@ class OutputUnlearningLlmRun(BaseModel): post_process_progress: float -class RunStatus(Enum): - PENDING = "PENDING" - STARTED = "STARTED" - SUCCESS = "SUCCESS" - FAILURE = "FAILURE" - AWAITING_MANUAL_APPROVAL = "AWAITING MANUAL APPROVAL" - REVOKED = "REVOKED" - REJECTED = "REJECTED" - RETRY = "RETRY" - - -STATUS_TO_TEXT_MAP = { - RunStatus.STARTED.value: "LLM unlearning run in progress", - RunStatus.PENDING.value: "LLM unlearning run queued and not yet started", - RunStatus.SUCCESS.value: "LLM unlearning run completed successfully", - RunStatus.FAILURE.value: "LLM unlearning run failed", - RunStatus.AWAITING_MANUAL_APPROVAL.value: "Awaiting manual approval", - RunStatus.RETRY.value: "LLM unlearning run failed. Retrying", - RunStatus.REVOKED.value: "LLM unlearning run was cancelled", - RunStatus.REJECTED.value: "LLM unlearning run was rejected", -} -STATUS_TO_PROGRESS_MAP = { - RunStatus.STARTED.value: 0.0, - RunStatus.PENDING.value: 0.0, - RunStatus.SUCCESS.value: 100.0, - RunStatus.FAILURE.value: 100.0, - RunStatus.AWAITING_MANUAL_APPROVAL.value: 100.0, - RunStatus.RETRY.value: 0.0, - RunStatus.REVOKED.value: 100.0, - RunStatus.REJECTED.value: 0.0, -} +STATUS_TO_TEXT_MAP = build_status_text_map("LLM unlearning") class LlmUnlearningRun: @@ -445,53 +420,14 @@ def list( @staticmethod def _check_run_by_id(run_id: str, retry=0) -> Generator[dict, None, None]: - if retry > MAX_RETRIES: - raise HirundoError("Max retries reached") - last_event = None - with httpx.Client(timeout=httpx.Timeout(None, connect=5.0)) as client: - for sse in iter_sse_retrying( - client, - "GET", - f"{API_HOST}/unlearning-llm/run/{run_id}", - headers=get_headers(), - ): - if sse.event == "ping": - continue - logger.debug( - "[SYNC] received event: %s with data: %s and ID: %s and retry: %s", - sse.event, - sse.data, - sse.id, - sse.retry, - ) - last_event = json.loads(sse.data) - if not last_event: - continue - if "data" in last_event: - data = last_event["data"] - else: - if "detail" in last_event: - raise HirundoError(last_event["detail"]) - elif "reason" in last_event: - raise HirundoError(last_event["reason"]) - else: - raise HirundoError("Unknown error") - yield data - last_state = None - if last_event and "data" in last_event: - last_state = last_event["data"].get("state") or last_event["data"].get( - "status" - ) - if not last_event or last_state == RunStatus.PENDING.value: - LlmUnlearningRun._check_run_by_id(run_id, retry + 1) - - @staticmethod - def _handle_failure(iteration: dict) -> None: - if iteration.get("result"): - raise HirundoError( - f"LLM unlearning run failed with error: {iteration['result']}" - ) - raise HirundoError("LLM unlearning run failed with an unknown error") + yield from iter_run_events( + f"{API_HOST}/unlearning-llm/run/{run_id}", + headers=get_headers(), + retry=retry, + status_keys=("state", "status"), + error_cls=HirundoError, + log=logger, + ) @staticmethod @overload @@ -530,7 +466,7 @@ def check_run_by_id(run_id: str, stop_on_manual_approval: bool = False): with logging_redirect_tqdm(): t = tqdm(total=100.0) for iteration in LlmUnlearningRun._check_run_by_id(run_id): - state = iteration.get("state") or iteration.get("status") + state = get_state(iteration, ("state", "status")) if state in STATUS_TO_PROGRESS_MAP: t.set_description(STATUS_TO_TEXT_MAP[state]) t.n = STATUS_TO_PROGRESS_MAP[state] @@ -545,7 +481,11 @@ def check_run_by_id(run_id: str, stop_on_manual_approval: bool = False): "State is failure, rejected, or revoked: %s", state, ) - LlmUnlearningRun._handle_failure(iteration) + handle_run_failure( + iteration, + error_cls=HirundoError, + run_label="LLM unlearning", + ) elif state == RunStatus.SUCCESS.value: t.close() return iteration.get("result") or iteration @@ -556,33 +496,12 @@ def check_run_by_id(run_id: str, stop_on_manual_approval: bool = False): t.close() return None elif state is None: - if ( - iteration.get("result") - and isinstance(iteration["result"], dict) - and iteration["result"].get("result") - and isinstance(iteration["result"]["result"], str) - ): - result_info = iteration["result"]["result"].split(":") - if len(result_info) > 1: - stage = result_info[0] - current_progress_percentage = float( - result_info[1].removeprefix(" ").removesuffix("% done") - ) - elif len(result_info) == 1: - stage = result_info[0] - current_progress_percentage = t.n - else: - stage = "Unknown progress state" - current_progress_percentage = t.n - desc = ( - "LLM unlearning run completed. Uploading results" - if current_progress_percentage == 100.0 - else stage - ) - t.set_description(desc) - t.n = current_progress_percentage - logger.debug("Setting progress to %s", t.n) - t.refresh() + update_progress_from_result( + iteration, + t, + uploading_text="LLM unlearning run completed. Uploading results", + log=logger, + ) raise HirundoError("LLM unlearning run failed with an unknown error") @staticmethod @@ -615,37 +534,15 @@ async def acheck_run_by_id(run_id: str, retry=0) -> AsyncGenerator[dict, None]: """ logger.debug("Checking run with ID: %s", run_id) - if retry > MAX_RETRIES: - raise HirundoError("Max retries reached") - last_event = None - async with httpx.AsyncClient( - timeout=httpx.Timeout(None, connect=5.0) - ) as client: - async_iterator = await aiter_sse_retrying( - client, - "GET", - f"{API_HOST}/unlearning-llm/run/{run_id}", - headers=get_headers(), - ) - async for sse in async_iterator: - if sse.event == "ping": - continue - logger.debug( - "[ASYNC] Received event: %s with data: %s and ID: %s and retry: %s", - sse.event, - sse.data, - sse.id, - sse.retry, - ) - last_event = json.loads(sse.data) - yield last_event["data"] - last_state = None - if last_event and "data" in last_event: - last_state = last_event["data"].get("state") or last_event["data"].get( - "status" - ) - if not last_event or last_state == RunStatus.PENDING.value: - LlmUnlearningRun.acheck_run_by_id(run_id, retry + 1) + async for iteration in aiter_run_events( + f"{API_HOST}/unlearning-llm/run/{run_id}", + headers=get_headers(), + retry=retry, + status_keys=("state", "status"), + error_cls=HirundoError, + log=logger, + ): + yield iteration @staticmethod async def acheck_run(run_id: str) -> AsyncGenerator[dict, None]: From 17f756ef21ddca8dea4ab7f6e42e2b29e9595237 Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Wed, 21 Jan 2026 23:17:32 +0200 Subject: [PATCH 12/25] LLM behavior unlearning test and loading of transformers Pipeline --- .github/workflows/lint.yaml | 4 +- .github/workflows/pytest-full.yaml | 2 +- .github/workflows/pytest-sanity.yaml | 4 +- .github/workflows/vulnerability-scan.yml | 1 + .pre-commit-config.yaml | 15 +- DEV_README.md | 8 +- README.md | 28 +- dev.Dockerfile | 1 + hirundo/_llm_pipeline.py | 152 +++++ hirundo/dataset_qa.py | 2 + hirundo/unlearning_llm.py | 56 +- pyproject.toml | 1 + requirements/dev.txt | 21 +- requirements/docs.txt | 21 +- requirements/pandas.txt | 13 +- requirements/polars.txt | 11 - requirements/requirements.txt | 10 +- requirements/transformers.txt | 271 ++++++++ scripts/cleanup_test_artifacts.py | 115 +++- .../unlearn_llm_behavior_test.py | 14 +- uv.lock | 588 +++++++++++++++++- 21 files changed, 1236 insertions(+), 102 deletions(-) create mode 100644 hirundo/_llm_pipeline.py create mode 100644 requirements/transformers.txt diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 7e305071..4a58a1c2 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -27,7 +27,7 @@ jobs: python -m pip install --upgrade pip python -m venv .venv source .venv/bin/activate - pip install -r requirements/dev.txt -r requirements/pandas.txt -r requirements/polars.txt + pip install -r requirements/dev.txt -r requirements/pandas.txt -r requirements/polars.txt -r requirements/transformers.txt - run: echo "$PWD/.venv/bin" >> $GITHUB_PATH - uses: astral-sh/ruff-action@v3 - run: ruff check @@ -50,6 +50,6 @@ jobs: python -m pip install --upgrade pip python -m venv .venv source .venv/bin/activate - pip install -r requirements/dev.txt -r requirements/pandas.txt -r requirements/polars.txt + pip install -r requirements/dev.txt -r requirements/pandas.txt -r requirements/polars.txt -r requirements/transformers.txt - run: echo "$PWD/.venv/bin" >> $GITHUB_PATH - run: basedpyright diff --git a/.github/workflows/pytest-full.yaml b/.github/workflows/pytest-full.yaml index 65786d86..b2db8e60 100644 --- a/.github/workflows/pytest-full.yaml +++ b/.github/workflows/pytest-full.yaml @@ -38,7 +38,7 @@ jobs: python -m pip install --upgrade pip python -m venv .venv source .venv/bin/activate - pip install -r requirements/dev.txt -r requirements/polars.txt + pip install -r requirements/dev.txt -r requirements/polars.txt -r requirements/transformers.txt - name: Run PyTest run: .venv/bin/pytest tests/${{ matrix.data-qa-test['test'] }} env: diff --git a/.github/workflows/pytest-sanity.yaml b/.github/workflows/pytest-sanity.yaml index d2a2dd71..03802fa4 100644 --- a/.github/workflows/pytest-sanity.yaml +++ b/.github/workflows/pytest-sanity.yaml @@ -56,14 +56,14 @@ jobs: python -m pip install --upgrade pip python -m venv .venv source .venv/bin/activate - pip install -r requirements/dev.txt -r requirements/polars.txt + pip install -r requirements/dev.txt -r requirements/polars.txt -r requirements/transformers.txt - name: Run commands on Windows if: github.event_name != 'pull_request' && runner.os == 'Windows' && steps.changes.outputs.non_workflow == 'true' run: | python -m pip install --upgrade 'pip>=24.1.2' python -m venv .venv .venv\Scripts\activate - python -m pip install -r requirements\dev.txt -r requirements\polars.txt + python -m pip install -r requirements\dev.txt -r requirements\polars.txt -r requirements\transformers.txt - name: Run PyTest on Linux and macOS if: github.event_name != 'pull_request' && runner.os != 'Windows' && steps.changes.outputs.non_workflow == 'true' run: .venv/bin/pytest diff --git a/.github/workflows/vulnerability-scan.yml b/.github/workflows/vulnerability-scan.yml index 8f5112a7..40fe962d 100644 --- a/.github/workflows/vulnerability-scan.yml +++ b/.github/workflows/vulnerability-scan.yml @@ -24,6 +24,7 @@ jobs: - requirements/docs.txt - requirements/pandas.txt - requirements/polars.txt + - requirements/transformers.txt runs-on: ubuntu-latest permissions: contents: read diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 59bb01f3..25c82ebb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,11 +61,18 @@ repos: always_run: false files: pyproject.toml$ additional_dependencies: [uv] - - id: pip-sync - name: sync + - id: pip-compile-transformers + name: compile requirements/transformers.txt entry: uv - args: ["pip", "sync", "requirements/dev.txt", "requirements/docs.txt", "requirements/pandas.txt", "requirements/polars.txt"] + args: ["pip", "compile", "--extra", "transformers", "-o", "requirements/transformers.txt", "-c", "requirements/requirements.txt"] language: python always_run: false - files: requirements.txt$ + files: pyproject.toml$ additional_dependencies: [uv] + - repo: https://github.com/astral-sh/uv-pre-commit + # uv version. + rev: 0.9.6 + hooks: + - id: uv-lock + - id: uv-sync + args: ["--extra", "dev", "--extra", "docs", "--extra", "pandas", "--extra", "polars", "--extra", "transformers"] diff --git a/DEV_README.md b/DEV_README.md index c137e980..a3ebf336 100644 --- a/DEV_README.md +++ b/DEV_README.md @@ -44,12 +44,18 @@ uv pip compile --extra dev -o requirements/dev.txt -c requirements.txt pyproject uv pip compile --extra pandas -o requirements/pandas.txt -c requirements.txt pyproject.toml uv pip compile --extra polars -o requirements/polars.txt -c requirements.txt pyproject.toml uv pip compile --extra docs -o requirements/docs.txt -c requirements.txt pyproject.toml +uv pip compile --extra transformers -o requirements/transformers.txt -c requirements.txt pyproject.toml ``` #### Sync installed packages ```bash -uv pip sync requirements/dev.txt requirements/polars.txt +uv pip sync requirements/dev.txt requirements/pandas.txt requirements/polars.txt requirements/docs.txt requirements/transformers.txt +``` +or + +```bash +uv sync --extra dev --extra pandas --extra polars --extra docs --extra transformers ``` ### Build process diff --git a/README.md b/README.md index 892c3b6a..ea23c2fe 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,31 @@ You can install the codebase with a simple `pip install hirundo` to install the ## Usage -Classification example: +### Unlearning LLM behavior + +Make sure to install the `transformers` extra, i.e. `pip install hirundo[transformers]` or `uv pip install hirundo[transformers]` if you have `uv` installed which is much faster than `pip`. + +```python +llm = LlmModel( + model_name="Nemotron-Flash-1B", + model_source=HuggingFaceTransformersModel( + model_name="nvidia/Nemotron-Flash-1B", + ), +) +llm_id = llm.create() +run_info = BiasRunInfo( + bias_type=BiasType.ALL, +) +run_id = LlmUnlearningRun.launch( + llm_id, + run_info, +) +new_adapter = llm.get_pipeline_for_run(run_id) +``` + +### Dataset QA + +#### Classification example: ```python from hirundo import ( @@ -104,7 +128,7 @@ results = test_dataset.check_run() print(results) ``` -Object detection example: +#### Object detection example: ```python from hirundo import ( diff --git a/dev.Dockerfile b/dev.Dockerfile index 30ad2d0d..35ee2d1b 100644 --- a/dev.Dockerfile +++ b/dev.Dockerfile @@ -7,6 +7,7 @@ COPY . . RUN pip install -r requirements/requirements.txt \ -r requirements/dev.txt -r requirements/docs.txt \ -r requirements/pandas.txt -r requirements/polars.txt \ + -r requirements/transformers.txt \ && pip install ipykernel CMD ["python"] diff --git a/hirundo/_llm_pipeline.py b/hirundo/_llm_pipeline.py new file mode 100644 index 00000000..41b75825 --- /dev/null +++ b/hirundo/_llm_pipeline.py @@ -0,0 +1,152 @@ +import importlib.util +import tempfile +import zipfile +from pathlib import Path +from typing import TYPE_CHECKING, cast + +from hirundo import HirundoError +from hirundo._http import requests +from hirundo._timeouts import DOWNLOAD_READ_TIMEOUT +from hirundo.logger import get_logger + +if TYPE_CHECKING: + from torch import device as torch_device + from transformers.configuration_utils import PretrainedConfig + from transformers.modeling_utils import PreTrainedModel + from transformers.pipelines.base import Pipeline + + from hirundo.unlearning_llm import LlmModel, LlmModelOut + +logger = get_logger(__name__) + + +ZIP_FILE_CHUNK_SIZE = 50 * 1024 * 1024 # 50 MB +REQUIRED_PACKAGES_FOR_PIPELINE = ["peft", "transformers", "accelerate"] + + +def get_pipeline_for_run_given_model( + llm: "LlmModel | LlmModelOut", + run_id: str, + config: "PretrainedConfig | None" = None, + device: "str | int | torch_device | None" = None, + device_map: str | dict[str, int | str] | None = None, + trust_remote_code: bool = False, + token: str | None = None, +) -> "Pipeline": + for package in REQUIRED_PACKAGES_FOR_PIPELINE: + if importlib.util.find_spec(package) is None: + raise HirundoError( + f'{package} is not installed. Please install transformers extra with pip install "hirundo[transformers]"' + ) + from peft import PeftModel + from transformers.models.auto.configuration_auto import AutoConfig + from transformers.models.auto.modeling_auto import ( + MODEL_FOR_VISION_2_SEQ_MAPPING_NAMES, + AutoModelForCausalLM, + AutoModelForImageTextToText, + ) + from transformers.models.auto.tokenization_auto import AutoTokenizer + from transformers.pipelines import pipeline + + from hirundo.unlearning_llm import ( + HuggingFaceTransformersModel, + HuggingFaceTransformersModelOutput, + LlmUnlearningRun, + ) + + run_results = LlmUnlearningRun.check_run_by_id(run_id) + if run_results is None: + raise HirundoError("No run results found") + result_payload = ( + run_results.get("result", run_results) + if isinstance(run_results, dict) + else run_results + ) + if isinstance(result_payload, dict): + result_url = result_payload.get("result") + else: + result_url = result_payload + if not isinstance(result_url, str): + raise HirundoError("Run results did not include a download URL") + # Stream the zip file download + + zip_file_path = tempfile.NamedTemporaryFile(delete=False).name + with requests.get( + result_url, + timeout=DOWNLOAD_READ_TIMEOUT, + stream=True, + ) as r: + r.raise_for_status() + with open(zip_file_path, "wb") as zip_file: + for chunk in r.iter_content(chunk_size=ZIP_FILE_CHUNK_SIZE): + zip_file.write(chunk) + logger.info( + "Successfully downloaded the result zip file for run ID %s to %s", + run_id, + zip_file_path, + ) + + with tempfile.TemporaryDirectory() as temp_dir: + temp_dir_path = Path(temp_dir) + with zipfile.ZipFile(zip_file_path, "r") as zip_file: + zip_file.extractall(temp_dir_path) + # Attempt to load the tokenizer normally + base_model_name = ( + llm.model_source.model_name + if isinstance( + llm.model_source, + HuggingFaceTransformersModel | HuggingFaceTransformersModelOutput, + ) + else llm.model_source.local_path + ) + token = ( + llm.model_source.token + if isinstance( + llm.model_source, + HuggingFaceTransformersModel, + ) + else token + ) + tokenizer = AutoTokenizer.from_pretrained( + base_model_name, + token=token, + trust_remote_code=trust_remote_code, + ) + if tokenizer.pad_token is None: + tokenizer.pad_token = tokenizer.eos_token + config = AutoConfig.from_pretrained( + base_model_name, + token=token, + trust_remote_code=trust_remote_code, + ) + config_dict = config.to_dict() if hasattr(config, "to_dict") else config + is_multimodal = ( + config_dict.get("model_type") in MODEL_FOR_VISION_2_SEQ_MAPPING_NAMES.keys() + ) + if is_multimodal: + base_model = AutoModelForImageTextToText.from_pretrained( + base_model_name, + token=token, + trust_remote_code=trust_remote_code, + ) + else: + base_model = AutoModelForCausalLM.from_pretrained( + base_model_name, + token=token, + trust_remote_code=trust_remote_code, + ) + model = cast( + "PreTrainedModel", + PeftModel.from_pretrained( + base_model, str(temp_dir_path / "unlearned_model_folder") + ), + ) + + return pipeline( + task="text-generation", + model=model, + tokenizer=tokenizer, + config=config, + device=device, + device_map=device_map, + ) diff --git a/hirundo/dataset_qa.py b/hirundo/dataset_qa.py index bea1ecda..ced45cb5 100644 --- a/hirundo/dataset_qa.py +++ b/hirundo/dataset_qa.py @@ -776,3 +776,5 @@ class DataQARunOut(BaseModel): approved: bool created_at: datetime.datetime run_args: RunArgs | None + + deleted_at: datetime.datetime | None diff --git a/hirundo/unlearning_llm.py b/hirundo/unlearning_llm.py index 2ca839fe..47a38a2c 100644 --- a/hirundo/unlearning_llm.py +++ b/hirundo/unlearning_llm.py @@ -2,15 +2,16 @@ import typing from collections.abc import AsyncGenerator, Generator from enum import Enum -from typing import Literal, overload +from typing import TYPE_CHECKING, Literal, overload -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict from tqdm import tqdm from tqdm.contrib.logging import logging_redirect_tqdm from hirundo._env import API_HOST from hirundo._headers import get_headers from hirundo._http import raise_for_status_with_reason, requests +from hirundo._llm_pipeline import get_pipeline_for_run_given_model from hirundo._run_checking import ( STATUS_TO_PROGRESS_MAP, RunStatus, @@ -25,6 +26,11 @@ from hirundo.dataset_qa import HirundoError from hirundo.logger import get_logger +if TYPE_CHECKING: + from torch import device as torch_device + from transformers.configuration_utils import PretrainedConfig + from transformers.pipelines.base import Pipeline + logger = get_logger(__name__) @@ -34,6 +40,8 @@ class ModelSourceType(str, Enum): class HuggingFaceTransformersModel(BaseModel): + model_config = ConfigDict(protected_namespaces=("model_validate", "model_dump")) + type: Literal[ModelSourceType.HUGGINGFACE_TRANSFORMERS] = ( ModelSourceType.HUGGINGFACE_TRANSFORMERS ) @@ -44,6 +52,8 @@ class HuggingFaceTransformersModel(BaseModel): class HuggingFaceTransformersModelOutput(BaseModel): + model_config = ConfigDict(protected_namespaces=("model_validate", "model_dump")) + type: Literal[ModelSourceType.HUGGINGFACE_TRANSFORMERS] = ( ModelSourceType.HUGGINGFACE_TRANSFORMERS ) @@ -64,6 +74,8 @@ class LocalTransformersModel(BaseModel): class LlmModel(BaseModel): + model_config = ConfigDict(protected_namespaces=("model_validate", "model_dump")) + id: int | None = None organization_id: int | None = None model_name: str @@ -168,8 +180,22 @@ def update( if archive_existing_runs is not None: self.archive_existing_runs = archive_existing_runs + def get_pipeline_for_run( + self, + run_id: str, + config: "PretrainedConfig | None" = None, + device: "str | int | torch_device | None" = None, + device_map: str | dict[str, int | str] | None = None, + trust_remote_code: bool = False, + ) -> "Pipeline": + return get_pipeline_for_run_given_model( + self, run_id, config, device, device_map, trust_remote_code + ) + class LlmModelOut(BaseModel): + model_config = ConfigDict(protected_namespaces=("model_validate", "model_dump")) + id: int organization_id: int creator_id: int @@ -192,6 +218,25 @@ def from_response(response_payload: dict[str, typing.Any]) -> "LlmModelOut": model_source=response_payload["model_source"], ) + def get_pipeline_for_run( + self, + run_id: str, + config: "PretrainedConfig | None" = None, + device: "str | int | torch_device | None" = None, + device_map: str | dict[str, int | str] | None = None, + trust_remote_code: bool = False, + token: str | None = None, + ) -> "Pipeline": + return get_pipeline_for_run_given_model( + self, + run_id, + config, + device, + device_map, + trust_remote_code, + token=token, + ) + class DatasetType(str, Enum): NORMAL = "normal" @@ -275,6 +320,8 @@ class CustomBehavior(BaseModel): class LlmRunInfo(BaseModel): + model_config = ConfigDict(protected_namespaces=("model_validate", "model_dump")) + organization_id: int | None = None name: str | None = None model_id: int | None = None @@ -312,6 +359,8 @@ def to_run_info(self) -> LlmRunInfo: class OutputUnlearningLlmRun(BaseModel): + model_config = ConfigDict(protected_namespaces=("model_validate", "model_dump")) + id: int name: str model_id: int @@ -329,6 +378,8 @@ class OutputUnlearningLlmRun(BaseModel): optimization_progress: float post_process_progress: float + deleted_at: datetime.datetime | None + STATUS_TO_TEXT_MAP = build_status_text_map("LLM unlearning") @@ -481,6 +532,7 @@ def check_run_by_id(run_id: str, stop_on_manual_approval: bool = False): "State is failure, rejected, or revoked: %s", state, ) + t.close() handle_run_failure( iteration, error_cls=HirundoError, diff --git a/pyproject.toml b/pyproject.toml index 3f821931..e37d6ec6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,6 +98,7 @@ docs = [ ] pandas = ["pandas>=2.2.3"] polars = ["polars>=1.0.0"] +transformers = ["transformers>=4.57.3", "peft>=0.18.1", "accelerate>=1.12.0"] [tool.bumpver] current_version = "0.1.3b1" diff --git a/requirements/dev.txt b/requirements/dev.txt index a32a93bc..8f688546 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -10,12 +10,6 @@ anyio==4.4.0 # httpx authlib==1.6.6 # via hirundo (pyproject.toml) -backports-datetime-fromisoformat==2.0.3 - # via marshmallow -backports-tarfile==1.2.0 - # via - # -c requirements/requirements.txt - # jaraco-context basedpyright==1.37.1 # via hirundo (pyproject.toml) bumpver==2023.1129 @@ -51,11 +45,6 @@ docutils==0.21.2 # via # -c requirements/requirements.txt # readme-renderer -exceptiongroup==1.3.1 - # via - # -c requirements/requirements.txt - # anyio - # pytest filelock==3.20.3 # via # hirundo (pyproject.toml) @@ -88,7 +77,6 @@ idna==3.7 importlib-metadata==8.0.0 # via # -c requirements/requirements.txt - # keyring # twine iniconfig==2.0.0 # via pytest @@ -118,7 +106,7 @@ markdown-it-py==3.0.0 # via # -c requirements/requirements.txt # rich -markupsafe==3.0.2 +markupsafe==3.0.3 # via jinja2 marshmallow==4.2.0 # via hirundo (pyproject.toml) @@ -139,7 +127,7 @@ nodeenv==1.9.1 # via pre-commit nodejs-wheel-binaries==24.12.0 # via basedpyright -packaging==24.1 +packaging==26.0 # via pytest pkginfo==1.10.0 # via @@ -231,8 +219,6 @@ tenacity==8.5.0 # stamina toml==0.10.2 # via bumpver -tomli==2.4.0 - # via pytest tqdm==4.66.5 # via # -c requirements/requirements.txt @@ -258,9 +244,6 @@ types-setuptools==70.3.0.20240710 typing-extensions==4.12.2 # via # -c requirements/requirements.txt - # anyio - # exceptiongroup - # marshmallow # pydantic # pydantic-core # typer diff --git a/requirements/docs.txt b/requirements/docs.txt index 61d361de..013633d9 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -20,10 +20,6 @@ autodoc-pydantic==2.2.0 # via hirundo (pyproject.toml) babel==2.15.0 # via sphinx -backports-tarfile==1.2.0 - # via - # -c requirements/requirements.txt - # jaraco-context beautifulsoup4==4.12.3 # via furo cattrs==24.1.2 @@ -56,11 +52,6 @@ docutils==0.21.2 # sphinx-click esbonio==0.16.5 # via hirundo (pyproject.toml) -exceptiongroup==1.3.1 - # via - # -c requirements/requirements.txt - # anyio - # cattrs furo==2024.7.18 # via hirundo (pyproject.toml) h11==0.16.0 @@ -92,7 +83,6 @@ imagesize==1.4.1 importlib-metadata==8.0.0 # via # -c requirements/requirements.txt - # keyring # twine jaraco-classes==3.4.0 # via @@ -120,7 +110,7 @@ markdown-it-py==3.0.0 # via # -c requirements/requirements.txt # rich -markupsafe==3.0.2 +markupsafe==3.0.3 # via # hirundo (pyproject.toml) # jinja2 @@ -137,7 +127,7 @@ nh3==0.2.18 # via # -c requirements/requirements.txt # readme-renderer -packaging==24.1 +packaging==26.0 # via sphinx pkginfo==1.10.0 # via @@ -260,8 +250,6 @@ tenacity==8.5.0 # via # -c requirements/requirements.txt # stamina -tomli==2.4.0 - # via sphinx tqdm==4.66.5 # via # -c requirements/requirements.txt @@ -285,14 +273,9 @@ types-requests==2.32.0.20240712 typing-extensions==4.12.2 # via # -c requirements/requirements.txt - # anyio - # cattrs - # exceptiongroup # pydantic # pydantic-core - # starlette # typer - # uvicorn urllib3==2.6.3 # via # -c requirements/requirements.txt diff --git a/requirements/pandas.txt b/requirements/pandas.txt index 16d1e9ea..763566ec 100644 --- a/requirements/pandas.txt +++ b/requirements/pandas.txt @@ -8,10 +8,6 @@ anyio==4.4.0 # via # -c requirements/requirements.txt # httpx -backports-tarfile==1.2.0 - # via - # -c requirements/requirements.txt - # jaraco-context certifi==2024.7.4 # via # -c requirements/requirements.txt @@ -30,10 +26,6 @@ docutils==0.21.2 # via # -c requirements/requirements.txt # readme-renderer -exceptiongroup==1.3.1 - # via - # -c requirements/requirements.txt - # anyio h11==0.16.0 # via # -c requirements/requirements.txt @@ -60,7 +52,6 @@ idna==3.7 importlib-metadata==8.0.0 # via # -c requirements/requirements.txt - # keyring # twine jaraco-classes==3.4.0 # via @@ -95,7 +86,7 @@ nh3==0.2.18 # via # -c requirements/requirements.txt # readme-renderer -numpy==2.0.1 +numpy==2.4.1 # via pandas pandas==2.2.3 # via hirundo (pyproject.toml) @@ -197,8 +188,6 @@ types-requests==2.32.0.20240712 typing-extensions==4.12.2 # via # -c requirements/requirements.txt - # anyio - # exceptiongroup # pydantic # pydantic-core # typer diff --git a/requirements/polars.txt b/requirements/polars.txt index 52339dcc..78f096c1 100644 --- a/requirements/polars.txt +++ b/requirements/polars.txt @@ -8,10 +8,6 @@ anyio==4.4.0 # via # -c requirements/requirements.txt # httpx -backports-tarfile==1.2.0 - # via - # -c requirements/requirements.txt - # jaraco-context certifi==2024.7.4 # via # -c requirements/requirements.txt @@ -30,10 +26,6 @@ docutils==0.21.2 # via # -c requirements/requirements.txt # readme-renderer -exceptiongroup==1.3.1 - # via - # -c requirements/requirements.txt - # anyio h11==0.16.0 # via # -c requirements/requirements.txt @@ -60,7 +52,6 @@ idna==3.7 importlib-metadata==8.0.0 # via # -c requirements/requirements.txt - # keyring # twine jaraco-classes==3.4.0 # via @@ -189,8 +180,6 @@ types-requests==2.32.0.20240712 typing-extensions==4.12.2 # via # -c requirements/requirements.txt - # anyio - # exceptiongroup # pydantic # pydantic-core # typer diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 2f1ef6ef..96adea09 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -4,8 +4,6 @@ annotated-types==0.7.0 # via pydantic anyio==4.4.0 # via httpx -backports-tarfile==1.2.0 - # via jaraco-context certifi==2024.7.4 # via # httpcore @@ -17,8 +15,6 @@ click==8.1.7 # via typer docutils==0.21.2 # via readme-renderer -exceptiongroup==1.3.1 - # via anyio h11==0.16.0 # via # hirundo (pyproject.toml) @@ -35,9 +31,7 @@ idna==3.7 # httpx # requests importlib-metadata==8.0.0 - # via - # keyring - # twine + # via twine jaraco-classes==3.4.0 # via keyring jaraco-context==5.3.0 @@ -109,8 +103,6 @@ types-requests==2.32.0.20240712 # via hirundo (pyproject.toml) typing-extensions==4.12.2 # via - # anyio - # exceptiongroup # pydantic # pydantic-core # typer diff --git a/requirements/transformers.txt b/requirements/transformers.txt new file mode 100644 index 00000000..5a9eef12 --- /dev/null +++ b/requirements/transformers.txt @@ -0,0 +1,271 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --extra transformers -o requirements/transformers.txt -c requirements/requirements.txt pyproject.toml +accelerate==1.12.0 + # via + # hirundo (pyproject.toml) + # peft +annotated-types==0.7.0 + # via + # -c requirements/requirements.txt + # pydantic +anyio==4.4.0 + # via + # -c requirements/requirements.txt + # httpx +certifi==2024.7.4 + # via + # -c requirements/requirements.txt + # httpcore + # httpx + # requests +charset-normalizer==3.3.2 + # via + # -c requirements/requirements.txt + # requests +click==8.1.7 + # via + # -c requirements/requirements.txt + # typer +docutils==0.21.2 + # via + # -c requirements/requirements.txt + # readme-renderer +filelock==3.20.3 + # via + # huggingface-hub + # torch + # transformers +fsspec==2026.1.0 + # via + # huggingface-hub + # torch +h11==0.16.0 + # via + # -c requirements/requirements.txt + # hirundo (pyproject.toml) + # httpcore +hf-xet==1.2.0 + # via huggingface-hub +httpcore==1.0.9 + # via + # -c requirements/requirements.txt + # httpx +httpx==0.27.0 + # via + # -c requirements/requirements.txt + # hirundo (pyproject.toml) +httpx-sse==0.4.0 + # via + # -c requirements/requirements.txt + # hirundo (pyproject.toml) +huggingface-hub==0.36.0 + # via + # accelerate + # peft + # tokenizers + # transformers +idna==3.7 + # via + # -c requirements/requirements.txt + # anyio + # httpx + # requests +importlib-metadata==8.0.0 + # via + # -c requirements/requirements.txt + # twine +jaraco-classes==3.4.0 + # via + # -c requirements/requirements.txt + # keyring +jaraco-context==5.3.0 + # via + # -c requirements/requirements.txt + # keyring +jaraco-functools==4.0.1 + # via + # -c requirements/requirements.txt + # keyring +jinja2==3.1.6 + # via torch +keyring==25.2.1 + # via + # -c requirements/requirements.txt + # twine +markdown-it-py==3.0.0 + # via + # -c requirements/requirements.txt + # rich +markupsafe==3.0.3 + # via jinja2 +mdurl==0.1.2 + # via + # -c requirements/requirements.txt + # markdown-it-py +more-itertools==10.3.0 + # via + # -c requirements/requirements.txt + # jaraco-classes + # jaraco-functools +mpmath==1.3.0 + # via sympy +networkx==3.6.1 + # via torch +nh3==0.2.18 + # via + # -c requirements/requirements.txt + # readme-renderer +numpy==2.4.1 + # via + # accelerate + # peft + # transformers +packaging==26.0 + # via + # accelerate + # huggingface-hub + # peft + # transformers +peft==0.18.1 + # via hirundo (pyproject.toml) +pkginfo==1.10.0 + # via + # -c requirements/requirements.txt + # twine +psutil==7.2.1 + # via + # accelerate + # peft +pydantic==2.8.2 + # via + # -c requirements/requirements.txt + # hirundo (pyproject.toml) +pydantic-core==2.20.1 + # via + # -c requirements/requirements.txt + # pydantic +pygments==2.18.0 + # via + # -c requirements/requirements.txt + # readme-renderer + # rich +python-dotenv==1.0.1 + # via + # -c requirements/requirements.txt + # hirundo (pyproject.toml) +pyyaml==6.0.1 + # via + # -c requirements/requirements.txt + # hirundo (pyproject.toml) + # accelerate + # huggingface-hub + # peft + # transformers +readme-renderer==44.0 + # via + # -c requirements/requirements.txt + # twine +regex==2026.1.15 + # via transformers +requests==2.32.4 + # via + # -c requirements/requirements.txt + # hirundo (pyproject.toml) + # huggingface-hub + # requests-toolbelt + # transformers + # twine +requests-toolbelt==1.0.0 + # via + # -c requirements/requirements.txt + # twine +rfc3986==2.0.0 + # via + # -c requirements/requirements.txt + # twine +rich==13.7.1 + # via + # -c requirements/requirements.txt + # twine + # typer +safetensors==0.7.0 + # via + # accelerate + # peft + # transformers +setuptools==80.9.0 + # via + # -c requirements/requirements.txt + # hirundo (pyproject.toml) + # torch +shellingham==1.5.4 + # via + # -c requirements/requirements.txt + # typer +sniffio==1.3.1 + # via + # -c requirements/requirements.txt + # anyio + # httpx +stamina==24.2.0 + # via + # -c requirements/requirements.txt + # hirundo (pyproject.toml) +sympy==1.14.0 + # via torch +tenacity==8.5.0 + # via + # -c requirements/requirements.txt + # stamina +tokenizers==0.22.2 + # via transformers +torch==2.10.0 + # via + # accelerate + # peft +tqdm==4.66.5 + # via + # -c requirements/requirements.txt + # hirundo (pyproject.toml) + # huggingface-hub + # peft + # transformers +transformers==4.57.6 + # via + # hirundo (pyproject.toml) + # peft +twine==5.1.1 + # via + # -c requirements/requirements.txt + # hirundo (pyproject.toml) +typer==0.12.3 + # via + # -c requirements/requirements.txt + # hirundo (pyproject.toml) +types-pyyaml==6.0.12.20240311 + # via + # -c requirements/requirements.txt + # hirundo (pyproject.toml) +types-requests==2.32.0.20240712 + # via + # -c requirements/requirements.txt + # hirundo (pyproject.toml) +typing-extensions==4.12.2 + # via + # -c requirements/requirements.txt + # huggingface-hub + # pydantic + # pydantic-core + # torch + # typer +urllib3==2.6.3 + # via + # -c requirements/requirements.txt + # hirundo (pyproject.toml) + # requests + # twine + # types-requests +zipp==3.19.2 + # via + # -c requirements/requirements.txt + # importlib-metadata diff --git a/scripts/cleanup_test_artifacts.py b/scripts/cleanup_test_artifacts.py index 0e672bac..5699bde9 100644 --- a/scripts/cleanup_test_artifacts.py +++ b/scripts/cleanup_test_artifacts.py @@ -1,19 +1,24 @@ import datetime from datetime import timedelta, timezone -from typing import Union import requests from hirundo import GitRepo, QADataset, StorageConfig from hirundo.dataset_qa import DataQARunOut, HirundoError, QADatasetOut, RunStatus from hirundo.logger import get_logger from hirundo.storage import ResponseStorageConfig +from hirundo.unlearning_llm import ( + LlmModel, + LlmModelOut, + LlmUnlearningRun, + OutputUnlearningLlmRun, +) logger = get_logger(__name__) def _delete_dataset( dataset_id: int, - storage_config: Union[StorageConfig, ResponseStorageConfig, None], + storage_config: StorageConfig | ResponseStorageConfig | None, deleted_datasets: set[int], deleted_storage_configs: set[int], deleted_git_repos: set[int], @@ -49,8 +54,23 @@ def _delete_dataset( return deleted_datasets, deleted_storage_configs, deleted_git_repos -def _should_delete_dataset( - dataset_name: str, dataset_runs: list, expiry_date: datetime.datetime +def _delete_llm( + llm_id: int, + deleted_llms: set[int], +) -> set[int]: + try: + LlmModel.delete_by_id(llm_id) + deleted_llms.add(llm_id) + except (HirundoError, requests.HTTPError) as exc: + logger.warning("Failed to delete LLM with ID %s: %s", llm_id, exc) + + return deleted_llms + + +def _should_delete_resource( + resource_name: str, + runs: list[DataQARunOut | OutputUnlearningLlmRun], + expiry_date: datetime.datetime, ) -> bool: """ Return ``True`` if the dataset should be deleted. @@ -61,17 +81,16 @@ def _should_delete_dataset( expiry_date: The expiry date of the dataset. """ - if not (dataset_name.startswith("TEST-") or dataset_name.startswith("T-")): + if not (resource_name.startswith("TEST-") or resource_name.startswith("T-")): return False all_runs_successful_or_archived = all( - run.status == RunStatus.SUCCESS or run.deleted_at is not None - for run in dataset_runs + run.status == RunStatus.SUCCESS or run.deleted_at is not None for run in runs ) if all_runs_successful_or_archived: return True - most_recent_run_time = max(run.created_at for run in dataset_runs) + most_recent_run_time = max(run.created_at for run in runs) return most_recent_run_time <= expiry_date @@ -106,6 +125,26 @@ def _collect_runs_by_dataset( return runs_by_dataset +def _collect_runs_by_llm( + llms: list[LlmModelOut], + all_live_runs: list[OutputUnlearningLlmRun], + all_archived_runs: list[OutputUnlearningLlmRun], +): + runs_by_llm: dict[int, list] = dict() + for llm in llms: + runs_by_llm[llm.id] = [] + for run in all_live_runs: + if run.model_id is None or run.run_id is None: + continue + if run.model_id not in runs_by_llm: + logger.warning( + "Run with ID %s has model ID that is not in the LLMs list", run.run_id + ) + continue + runs_by_llm[run.model_id].append(run) + return runs_by_llm + + def _cleanup_storage_configs(one_week_ago: datetime.datetime) -> None: trying_to_delete_storage_configs = set[int]() deleted_storage_configs = set[int]() @@ -139,9 +178,58 @@ def _cleanup_storage_configs(one_week_ago: datetime.datetime) -> None: ) +def _handle_llm_cleanup(one_week_ago: datetime.datetime): + archived_runs = set[str]() + trying_to_delete_llms = set[int]() + deleted_llms = set[int]() + llms = LlmModel.list() + llm_dict: dict[int, LlmModelOut] = {llm.id: llm for llm in llms} + all_live_llm_unlearning_runs = LlmUnlearningRun.list() + all_archived_llm_unlearning_runs = LlmUnlearningRun.list(archived=True) + runs_by_llm = _collect_runs_by_llm( + llms, + all_live_llm_unlearning_runs, + all_archived_llm_unlearning_runs, + ) + + for llm_id, llm_runs in runs_by_llm.items(): + llm = llm_dict.get(llm_id) + + if llm and _should_delete_resource(llm.model_name, llm_runs, one_week_ago): + trying_to_delete_llms.add(llm_id) + for run in llm_runs: + if run.deleted_at is not None: + continue + try: + LlmUnlearningRun.archive(run.run_id) + archived_runs.add(run.run_id) + except (HirundoError, requests.HTTPError) as exc: + logger.warning( + "Failed to archive run with ID %s: %s", run.run_id, exc + ) + deleted_llms = _delete_llm( + llm_id, + deleted_llms, + ) + + logger.info( + "Deleted %s (%s) LLMs and archived %s (%s) runs", + deleted_llms, + len(deleted_llms), + archived_runs, + len(archived_runs), + ) + if trying_to_delete_llms != deleted_llms: + logger.warning( + "Tried to delete %s LLMs, but only deleted %s LLMs", + trying_to_delete_llms, + deleted_llms, + ) + + def main() -> None: - all_live_runs = QADataset.list_runs() - all_archived_runs = QADataset.list_runs(archived=True) + all_live_data_qa_runs = QADataset.list_runs() + all_archived_data_qa_runs = QADataset.list_runs(archived=True) datasets = { dataset_entry.id: dataset_entry for dataset_entry in QADataset.list_datasets() @@ -151,7 +239,7 @@ def main() -> None: one_week_ago = now - timedelta(days=7) runs_by_dataset = _collect_runs_by_dataset( - datasets, all_live_runs, all_archived_runs + datasets, all_live_data_qa_runs, all_archived_data_qa_runs ) trying_to_delete_datasets = set[int]() deleted_datasets = set[int]() @@ -161,7 +249,9 @@ def main() -> None: for dataset_id, dataset_runs in runs_by_dataset.items(): dataset = datasets.get(dataset_id) - if dataset and _should_delete_dataset(dataset.name, dataset_runs, one_week_ago): + if dataset and _should_delete_resource( + dataset.name, dataset_runs, one_week_ago + ): trying_to_delete_datasets.add(dataset_id) for run in dataset_runs: if run.deleted_at is not None: @@ -202,6 +292,7 @@ def main() -> None: deleted_datasets, ) + _handle_llm_cleanup(one_week_ago) _cleanup_storage_configs(one_week_ago) diff --git a/tests/unlearning-llm/unlearn_llm_behavior_test.py b/tests/unlearning-llm/unlearn_llm_behavior_test.py index 2be94259..caea3558 100644 --- a/tests/unlearning-llm/unlearn_llm_behavior_test.py +++ b/tests/unlearning-llm/unlearn_llm_behavior_test.py @@ -8,6 +8,7 @@ LlmUnlearningRun, ) from tests.testing_utils import get_unique_id +from transformers.pipelines.base import Pipeline logger = logging.getLogger(__name__) @@ -15,16 +16,19 @@ def test_unlearn_llm_behavior(): - llm_id = LlmModel( - model_name=f"TEST-UNLEARN-LLM-BEHAVIOR-Nemotron-Flash-1B-{unique_id}", + llm = LlmModel( + model_name=f"TEST-UNLEARN-LLM-BEHAVIOR-Granite-4-micro-{unique_id}", model_source=HuggingFaceTransformersModel( - model_name="nvidia/Nemotron-Flash-1B", + model_name="ibm-granite/granite-4.0-micro", ), - ).create() + ) + llm_id = llm.create() run_info = BiasRunInfo( bias_type=BiasType.ALL, ) - LlmUnlearningRun.launch( + run_id = LlmUnlearningRun.launch( llm_id, run_info, ) + new_adapter = llm.get_pipeline_for_run(run_id) + assert isinstance(new_adapter, Pipeline) diff --git a/uv.lock b/uv.lock index fc45579c..682425e3 100644 --- a/uv.lock +++ b/uv.lock @@ -8,6 +8,24 @@ resolution-markers = [ "python_full_version < '3.11'", ] +[[package]] +name = "accelerate" +version = "1.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/8e/ac2a9566747a93f8be36ee08532eb0160558b07630a081a6056a9f89bf1d/accelerate-1.12.0.tar.gz", hash = "sha256:70988c352feb481887077d2ab845125024b2a137a5090d6d7a32b57d03a45df6", size = 398399, upload-time = "2025-11-21T11:27:46.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d2/c581486aa6c4fbd7394c23c47b83fa1a919d34194e16944241daf9e762dd/accelerate-1.12.0-py3-none-any.whl", hash = "sha256:3e2091cd341423207e2f084a6654b1efcd250dc326f2a37d6dde446e07cabb11", size = 380935, upload-time = "2025-11-21T11:27:44.522Z" }, +] + [[package]] name = "accessible-pygments" version = "0.0.5" @@ -463,6 +481,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, ] +[[package]] +name = "fsspec" +version = "2026.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/7d/5df2650c57d47c57232af5ef4b4fdbff182070421e405e0d62c6cdbfaa87/fsspec-2026.1.0.tar.gz", hash = "sha256:e987cb0496a0d81bba3a9d1cee62922fb395e7d4c3b575e57f547953334fe07b", size = 310496, upload-time = "2026-01-09T15:21:35.562Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/c9/97cc5aae1648dcb851958a3ddf73ccd7dbe5650d95203ecb4d7720b4cdbf/fsspec-2026.1.0-py3-none-any.whl", hash = "sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc", size = 201838, upload-time = "2026-01-09T15:21:34.041Z" }, +] + [[package]] name = "furo" version = "2025.7.19" @@ -488,6 +515,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "hf-xet" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870, upload-time = "2025-10-24T19:04:11.422Z" }, + { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584, upload-time = "2025-10-24T19:04:09.586Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004, upload-time = "2025-10-24T19:04:00.314Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636, upload-time = "2025-10-24T19:03:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" }, + { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866, upload-time = "2025-10-24T19:04:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861, upload-time = "2025-10-24T19:04:19.01Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699, upload-time = "2025-10-24T19:04:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885, upload-time = "2025-10-24T19:04:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550, upload-time = "2025-10-24T19:04:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010, upload-time = "2025-10-24T19:04:28.598Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264, upload-time = "2025-10-24T19:04:30.397Z" }, + { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071, upload-time = "2025-10-24T19:04:37.463Z" }, + { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" }, + { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" }, + { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" }, + { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" }, +] + [[package]] name = "hirundo" version = "0.1.21" @@ -556,9 +612,15 @@ pandas = [ polars = [ { name = "polars" }, ] +transformers = [ + { name = "accelerate" }, + { name = "peft" }, + { name = "transformers" }, +] [package.metadata] requires-dist = [ + { name = "accelerate", marker = "extra == 'transformers'", specifier = ">=1.12.0" }, { name = "authlib", marker = "extra == 'dev'", specifier = ">=1.6.6" }, { name = "autodoc-pydantic", marker = "extra == 'docs'", specifier = ">=2.2.0" }, { name = "basedpyright", marker = "extra == 'dev'", specifier = "==1.37.1" }, @@ -577,6 +639,7 @@ requires-dist = [ { name = "markupsafe", marker = "extra == 'docs'", specifier = ">=3.0.2" }, { name = "marshmallow", marker = "extra == 'dev'", specifier = ">=3.26.2" }, { name = "pandas", marker = "extra == 'pandas'", specifier = ">=2.2.3" }, + { name = "peft", marker = "extra == 'transformers'", specifier = ">=0.18.1" }, { name = "platformdirs", marker = "extra == 'dev'", specifier = ">=4.3.6" }, { name = "polars", marker = "extra == 'polars'", specifier = ">=1.0.0" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.7.1" }, @@ -599,6 +662,7 @@ requires-dist = [ { name = "stamina", marker = "extra == 'dev'", specifier = ">=24.2.0" }, { name = "starlette", marker = "extra == 'docs'", specifier = ">=0.49.1" }, { name = "tqdm", specifier = ">=4.66.5" }, + { name = "transformers", marker = "extra == 'transformers'", specifier = ">=4.57.3" }, { name = "twine", specifier = ">=5.0.0" }, { name = "twine", marker = "extra == 'dev'", specifier = ">=5.0.0" }, { name = "typer", specifier = ">=0.12.3" }, @@ -612,7 +676,7 @@ requires-dist = [ { name = "uv", marker = "extra == 'dev'", specifier = ">=0.9.6" }, { name = "virtualenv", marker = "extra == 'dev'", specifier = ">=20.6.6" }, ] -provides-extras = ["dev", "docs", "pandas", "polars"] +provides-extras = ["dev", "docs", "pandas", "polars", "transformers"] [[package]] name = "httpcore" @@ -651,6 +715,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054, upload-time = "2025-06-24T13:21:04.772Z" }, ] +[[package]] +name = "huggingface-hub" +version = "0.36.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/63/4910c5fa9128fdadf6a9c5ac138e8b1b6cee4ca44bf7915bbfbce4e355ee/huggingface_hub-0.36.0.tar.gz", hash = "sha256:47b3f0e2539c39bf5cde015d63b72ec49baff67b6931c3d97f3f84532e2b8d25", size = 463358, upload-time = "2025-10-23T12:12:01.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/bd/1a875e0d592d447cbc02805fd3fe0f497714d6a2583f59d14fa9ebad96eb/huggingface_hub-0.36.0-py3-none-any.whl", hash = "sha256:7bcc9ad17d5b3f07b57c78e79d527102d08313caa278a641993acddcb894548d", size = 566094, upload-time = "2025-10-23T12:11:59.557Z" }, +] + [[package]] name = "id" version = "1.5.0" @@ -909,6 +992,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2b/9f/7ba6f94fc1e9ac3d2b853fdff3035fb2fa5afbed898c4a72b8a020610594/more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e", size = 65278, upload-time = "2025-04-22T14:17:40.49Z" }, ] +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + [[package]] name = "nh3" version = "0.3.0" @@ -1005,6 +1123,140 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179, upload-time = "2024-08-26T20:14:08.786Z" }, ] +[[package]] +name = "nvidia-cublas-cu12" +version = "12.8.4.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.10.2.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.3.3.83" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, +] + +[[package]] +name = "nvidia-cufile-cu12" +version = "1.13.1.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.9.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.7.3.90" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cusparse-cu12" }, + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.5.8.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.27.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu12" +version = "3.3.20" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/6c/99acb2f9eb85c29fc6f3a7ac4dccfd992e22666dd08a642b303311326a97/nvidia_nvshmem_cu12-3.3.20-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d00f26d3f9b2e3c3065be895e3059d6479ea5c638a3f38c9fec49b1b9dd7c1e5", size = 124657145, upload-time = "2025-08-04T20:25:19.995Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" }, +] + [[package]] name = "packaging" version = "25.0" @@ -1062,6 +1314,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d5/f9/07086f5b0f2a19872554abeea7658200824f5835c58a106fa8f2ae96a46c/pandas-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5db9637dbc24b631ff3707269ae4559bce4b7fd75c1c4d7e13f40edc42df4444", size = 13189044, upload-time = "2025-07-07T19:19:39.999Z" }, ] +[[package]] +name = "peft" +version = "0.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "accelerate" }, + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "transformers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/48/147b3ea999560b40a34fd78724c7777aa9d18409c2250bdcaf9c4f2db7fc/peft-0.18.1.tar.gz", hash = "sha256:2dd0d6bfce936d1850e48aaddbd250941c5c02fc8ef3237cd8fd5aac35e0bae2", size = 635030, upload-time = "2026-01-09T13:08:01.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/14/b4e3f574acf349ae6f61f9c000a77f97a3b315b4bb6ad03791e79ae4a568/peft-0.18.1-py3-none-any.whl", hash = "sha256:0bf06847a3551e3019fc58c440cffc9a6b73e6e2962c95b52e224f77bbdb50f1", size = 556960, upload-time = "2026-01-09T13:07:55.865Z" }, +] + [[package]] name = "platformdirs" version = "4.3.8" @@ -1110,6 +1383,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, ] +[[package]] +name = "psutil" +version = "7.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/cb/09e5184fb5fc0358d110fc3ca7f6b1d033800734d34cac10f4136cfac10e/psutil-7.2.1.tar.gz", hash = "sha256:f7583aec590485b43ca601dd9cea0dcd65bd7bb21d30ef4ddbf4ea6b5ed1bdd3", size = 490253, upload-time = "2025-12-29T08:26:00.169Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/8e/f0c242053a368c2aa89584ecd1b054a18683f13d6e5a318fc9ec36582c94/psutil-7.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9f33bb525b14c3ea563b2fd521a84d2fa214ec59e3e6a2858f78d0844dd60d", size = 129624, upload-time = "2025-12-29T08:26:04.255Z" }, + { url = "https://files.pythonhosted.org/packages/26/97/a58a4968f8990617decee234258a2b4fc7cd9e35668387646c1963e69f26/psutil-7.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:81442dac7abfc2f4f4385ea9e12ddf5a796721c0f6133260687fec5c3780fa49", size = 130132, upload-time = "2025-12-29T08:26:06.228Z" }, + { url = "https://files.pythonhosted.org/packages/db/6d/ed44901e830739af5f72a85fa7ec5ff1edea7f81bfbf4875e409007149bd/psutil-7.2.1-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea46c0d060491051d39f0d2cff4f98d5c72b288289f57a21556cc7d504db37fc", size = 180612, upload-time = "2025-12-29T08:26:08.276Z" }, + { url = "https://files.pythonhosted.org/packages/c7/65/b628f8459bca4efbfae50d4bf3feaab803de9a160b9d5f3bd9295a33f0c2/psutil-7.2.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35630d5af80d5d0d49cfc4d64c1c13838baf6717a13effb35869a5919b854cdf", size = 183201, upload-time = "2025-12-29T08:26:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/fb/23/851cadc9764edcc18f0effe7d0bf69f727d4cf2442deb4a9f78d4e4f30f2/psutil-7.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:923f8653416604e356073e6e0bccbe7c09990acef442def2f5640dd0faa9689f", size = 139081, upload-time = "2025-12-29T08:26:12.483Z" }, + { url = "https://files.pythonhosted.org/packages/59/82/d63e8494ec5758029f31c6cb06d7d161175d8281e91d011a4a441c8a43b5/psutil-7.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cfbe6b40ca48019a51827f20d830887b3107a74a79b01ceb8cc8de4ccb17b672", size = 134767, upload-time = "2025-12-29T08:26:14.528Z" }, + { url = "https://files.pythonhosted.org/packages/05/c2/5fb764bd61e40e1fe756a44bd4c21827228394c17414ade348e28f83cd79/psutil-7.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:494c513ccc53225ae23eec7fe6e1482f1b8a44674241b54561f755a898650679", size = 129716, upload-time = "2025-12-29T08:26:16.017Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d2/935039c20e06f615d9ca6ca0ab756cf8408a19d298ffaa08666bc18dc805/psutil-7.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fce5f92c22b00cdefd1645aa58ab4877a01679e901555067b1bd77039aa589f", size = 130133, upload-time = "2025-12-29T08:26:18.009Z" }, + { url = "https://files.pythonhosted.org/packages/77/69/19f1eb0e01d24c2b3eacbc2f78d3b5add8a89bf0bb69465bc8d563cc33de/psutil-7.2.1-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93f3f7b0bb07711b49626e7940d6fe52aa9940ad86e8f7e74842e73189712129", size = 181518, upload-time = "2025-12-29T08:26:20.241Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6d/7e18b1b4fa13ad370787626c95887b027656ad4829c156bb6569d02f3262/psutil-7.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d34d2ca888208eea2b5c68186841336a7f5e0b990edec929be909353a202768a", size = 184348, upload-time = "2025-12-29T08:26:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/60/1672114392dd879586d60dd97896325df47d9a130ac7401318005aab28ec/psutil-7.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2ceae842a78d1603753561132d5ad1b2f8a7979cb0c283f5b52fb4e6e14b1a79", size = 140400, upload-time = "2025-12-29T08:26:23.993Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7b/d0e9d4513c46e46897b46bcfc410d51fc65735837ea57a25170f298326e6/psutil-7.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:08a2f175e48a898c8eb8eace45ce01777f4785bc744c90aa2cc7f2fa5462a266", size = 135430, upload-time = "2025-12-29T08:26:25.999Z" }, + { url = "https://files.pythonhosted.org/packages/c5/cf/5180eb8c8bdf6a503c6919f1da28328bd1e6b3b1b5b9d5b01ae64f019616/psutil-7.2.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2e953fcfaedcfbc952b44744f22d16575d3aa78eb4f51ae74165b4e96e55f42", size = 128137, upload-time = "2025-12-29T08:26:27.759Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2c/78e4a789306a92ade5000da4f5de3255202c534acdadc3aac7b5458fadef/psutil-7.2.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:05cc68dbb8c174828624062e73078e7e35406f4ca2d0866c272c2410d8ef06d1", size = 128947, upload-time = "2025-12-29T08:26:29.548Z" }, + { url = "https://files.pythonhosted.org/packages/29/f8/40e01c350ad9a2b3cb4e6adbcc8a83b17ee50dd5792102b6142385937db5/psutil-7.2.1-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e38404ca2bb30ed7267a46c02f06ff842e92da3bb8c5bfdadbd35a5722314d8", size = 154694, upload-time = "2025-12-29T08:26:32.147Z" }, + { url = "https://files.pythonhosted.org/packages/06/e4/b751cdf839c011a9714a783f120e6a86b7494eb70044d7d81a25a5cd295f/psutil-7.2.1-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab2b98c9fc19f13f59628d94df5cc4cc4844bc572467d113a8b517d634e362c6", size = 156136, upload-time = "2025-12-29T08:26:34.079Z" }, + { url = "https://files.pythonhosted.org/packages/44/ad/bbf6595a8134ee1e94a4487af3f132cef7fce43aef4a93b49912a48c3af7/psutil-7.2.1-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f78baafb38436d5a128f837fab2d92c276dfb48af01a240b861ae02b2413ada8", size = 148108, upload-time = "2025-12-29T08:26:36.225Z" }, + { url = "https://files.pythonhosted.org/packages/1c/15/dd6fd869753ce82ff64dcbc18356093471a5a5adf4f77ed1f805d473d859/psutil-7.2.1-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:99a4cd17a5fdd1f3d014396502daa70b5ec21bf4ffe38393e152f8e449757d67", size = 147402, upload-time = "2025-12-29T08:26:39.21Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/d9317542e3f2b180c4306e3f45d3c922d7e86d8ce39f941bb9e2e9d8599e/psutil-7.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:b1b0671619343aa71c20ff9767eced0483e4fc9e1f489d50923738caf6a03c17", size = 136938, upload-time = "2025-12-29T08:26:41.036Z" }, + { url = "https://files.pythonhosted.org/packages/3e/73/2ce007f4198c80fcf2cb24c169884f833fe93fbc03d55d302627b094ee91/psutil-7.2.1-cp37-abi3-win_arm64.whl", hash = "sha256:0d67c1822c355aa6f7314d92018fb4268a76668a536f133599b91edd48759442", size = 133836, upload-time = "2025-12-29T08:26:43.086Z" }, +] + [[package]] name = "pycparser" version = "2.22" @@ -1373,6 +1674,127 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310, upload-time = "2024-07-08T15:00:56.577Z" }, ] +[[package]] +name = "regex" +version = "2026.1.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/d2/e6ee96b7dff201a83f650241c52db8e5bd080967cb93211f57aa448dc9d6/regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e", size = 488166, upload-time = "2026-01-14T23:13:46.408Z" }, + { url = "https://files.pythonhosted.org/packages/23/8a/819e9ce14c9f87af026d0690901b3931f3101160833e5d4c8061fa3a1b67/regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f", size = 290632, upload-time = "2026-01-14T23:13:48.688Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c3/23dfe15af25d1d45b07dfd4caa6003ad710dcdcb4c4b279909bdfe7a2de8/regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b", size = 288500, upload-time = "2026-01-14T23:13:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/c6/31/1adc33e2f717df30d2f4d973f8776d2ba6ecf939301efab29fca57505c95/regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c", size = 781670, upload-time = "2026-01-14T23:13:52.453Z" }, + { url = "https://files.pythonhosted.org/packages/23/ce/21a8a22d13bc4adcb927c27b840c948f15fc973e21ed2346c1bd0eae22dc/regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9", size = 850820, upload-time = "2026-01-14T23:13:54.894Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/3eeacdf587a4705a44484cd0b30e9230a0e602811fb3e2cc32268c70d509/regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c", size = 898777, upload-time = "2026-01-14T23:13:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/79/a9/1898a077e2965c35fc22796488141a22676eed2d73701e37c73ad7c0b459/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106", size = 791750, upload-time = "2026-01-14T23:13:58.527Z" }, + { url = "https://files.pythonhosted.org/packages/4c/84/e31f9d149a178889b3817212827f5e0e8c827a049ff31b4b381e76b26e2d/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618", size = 782674, upload-time = "2026-01-14T23:13:59.874Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ff/adf60063db24532add6a1676943754a5654dcac8237af024ede38244fd12/regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4", size = 767906, upload-time = "2026-01-14T23:14:01.298Z" }, + { url = "https://files.pythonhosted.org/packages/af/3e/e6a216cee1e2780fec11afe7fc47b6f3925d7264e8149c607ac389fd9b1a/regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79", size = 774798, upload-time = "2026-01-14T23:14:02.715Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/23a4a8378a9208514ed3efc7e7850c27fa01e00ed8557c958df0335edc4a/regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9", size = 845861, upload-time = "2026-01-14T23:14:04.824Z" }, + { url = "https://files.pythonhosted.org/packages/f8/57/d7605a9d53bd07421a8785d349cd29677fe660e13674fa4c6cbd624ae354/regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220", size = 755648, upload-time = "2026-01-14T23:14:06.371Z" }, + { url = "https://files.pythonhosted.org/packages/6f/76/6f2e24aa192da1e299cc1101674a60579d3912391867ce0b946ba83e2194/regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13", size = 836250, upload-time = "2026-01-14T23:14:08.343Z" }, + { url = "https://files.pythonhosted.org/packages/11/3a/1f2a1d29453299a7858eab7759045fc3d9d1b429b088dec2dc85b6fa16a2/regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3", size = 779919, upload-time = "2026-01-14T23:14:09.954Z" }, + { url = "https://files.pythonhosted.org/packages/c0/67/eab9bc955c9dcc58e9b222c801e39cff7ca0b04261792a2149166ce7e792/regex-2026.1.15-cp310-cp310-win32.whl", hash = "sha256:21ca32c28c30d5d65fc9886ff576fc9b59bbca08933e844fa2363e530f4c8218", size = 265888, upload-time = "2026-01-14T23:14:11.35Z" }, + { url = "https://files.pythonhosted.org/packages/1d/62/31d16ae24e1f8803bddb0885508acecaec997fcdcde9c243787103119ae4/regex-2026.1.15-cp310-cp310-win_amd64.whl", hash = "sha256:3038a62fc7d6e5547b8915a3d927a0fbeef84cdbe0b1deb8c99bbd4a8961b52a", size = 277830, upload-time = "2026-01-14T23:14:12.908Z" }, + { url = "https://files.pythonhosted.org/packages/e5/36/5d9972bccd6417ecd5a8be319cebfd80b296875e7f116c37fb2a2deecebf/regex-2026.1.15-cp310-cp310-win_arm64.whl", hash = "sha256:505831646c945e3e63552cc1b1b9b514f0e93232972a2d5bedbcc32f15bc82e3", size = 270376, upload-time = "2026-01-14T23:14:14.782Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c9/0c80c96eab96948363d270143138d671d5731c3a692b417629bf3492a9d6/regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a", size = 488168, upload-time = "2026-01-14T23:14:16.129Z" }, + { url = "https://files.pythonhosted.org/packages/17/f0/271c92f5389a552494c429e5cc38d76d1322eb142fb5db3c8ccc47751468/regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f", size = 290636, upload-time = "2026-01-14T23:14:17.715Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f9/5f1fd077d106ca5655a0f9ff8f25a1ab55b92128b5713a91ed7134ff688e/regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1", size = 288496, upload-time = "2026-01-14T23:14:19.326Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e1/8f43b03a4968c748858ec77f746c286d81f896c2e437ccf050ebc5d3128c/regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b", size = 793503, upload-time = "2026-01-14T23:14:20.922Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4e/a39a5e8edc5377a46a7c875c2f9a626ed3338cb3bb06931be461c3e1a34a/regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8", size = 860535, upload-time = "2026-01-14T23:14:22.405Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1c/9dce667a32a9477f7a2869c1c767dc00727284a9fa3ff5c09a5c6c03575e/regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413", size = 907225, upload-time = "2026-01-14T23:14:23.897Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3c/87ca0a02736d16b6262921425e84b48984e77d8e4e572c9072ce96e66c30/regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026", size = 800526, upload-time = "2026-01-14T23:14:26.039Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/647d5715aeea7c87bdcbd2f578f47b415f55c24e361e639fe8c0cc88878f/regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785", size = 773446, upload-time = "2026-01-14T23:14:28.109Z" }, + { url = "https://files.pythonhosted.org/packages/af/89/bf22cac25cb4ba0fe6bff52ebedbb65b77a179052a9d6037136ae93f42f4/regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e", size = 783051, upload-time = "2026-01-14T23:14:29.929Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f4/6ed03e71dca6348a5188363a34f5e26ffd5db1404780288ff0d79513bce4/regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763", size = 854485, upload-time = "2026-01-14T23:14:31.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/8e8560bd78caded8eb137e3e47612430a05b9a772caf60876435192d670a/regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb", size = 762195, upload-time = "2026-01-14T23:14:32.802Z" }, + { url = "https://files.pythonhosted.org/packages/38/6b/61fc710f9aa8dfcd764fe27d37edfaa023b1a23305a0d84fccd5adb346ea/regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2", size = 845986, upload-time = "2026-01-14T23:14:34.898Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2e/fbee4cb93f9d686901a7ca8d94285b80405e8c34fe4107f63ffcbfb56379/regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1", size = 788992, upload-time = "2026-01-14T23:14:37.116Z" }, + { url = "https://files.pythonhosted.org/packages/ed/14/3076348f3f586de64b1ab75a3fbabdaab7684af7f308ad43be7ef1849e55/regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569", size = 265893, upload-time = "2026-01-14T23:14:38.426Z" }, + { url = "https://files.pythonhosted.org/packages/0f/19/772cf8b5fc803f5c89ba85d8b1870a1ca580dc482aa030383a9289c82e44/regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7", size = 277840, upload-time = "2026-01-14T23:14:39.785Z" }, + { url = "https://files.pythonhosted.org/packages/78/84/d05f61142709474da3c0853222d91086d3e1372bcdab516c6fd8d80f3297/regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec", size = 270374, upload-time = "2026-01-14T23:14:41.592Z" }, + { url = "https://files.pythonhosted.org/packages/92/81/10d8cf43c807d0326efe874c1b79f22bfb0fb226027b0b19ebc26d301408/regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1", size = 489398, upload-time = "2026-01-14T23:14:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/90/b0/7c2a74e74ef2a7c32de724658a69a862880e3e4155cba992ba04d1c70400/regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681", size = 291339, upload-time = "2026-01-14T23:14:45.183Z" }, + { url = "https://files.pythonhosted.org/packages/19/4d/16d0773d0c818417f4cc20aa0da90064b966d22cd62a8c46765b5bd2d643/regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f", size = 289003, upload-time = "2026-01-14T23:14:47.25Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e4/1fc4599450c9f0863d9406e944592d968b8d6dfd0d552a7d569e43bceada/regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa", size = 798656, upload-time = "2026-01-14T23:14:48.77Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e6/59650d73a73fa8a60b3a590545bfcf1172b4384a7df2e7fe7b9aab4e2da9/regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804", size = 864252, upload-time = "2026-01-14T23:14:50.528Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ab/1d0f4d50a1638849a97d731364c9a80fa304fec46325e48330c170ee8e80/regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c", size = 912268, upload-time = "2026-01-14T23:14:52.952Z" }, + { url = "https://files.pythonhosted.org/packages/dd/df/0d722c030c82faa1d331d1921ee268a4e8fb55ca8b9042c9341c352f17fa/regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5", size = 803589, upload-time = "2026-01-14T23:14:55.182Z" }, + { url = "https://files.pythonhosted.org/packages/66/23/33289beba7ccb8b805c6610a8913d0131f834928afc555b241caabd422a9/regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3", size = 775700, upload-time = "2026-01-14T23:14:56.707Z" }, + { url = "https://files.pythonhosted.org/packages/e7/65/bf3a42fa6897a0d3afa81acb25c42f4b71c274f698ceabd75523259f6688/regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb", size = 787928, upload-time = "2026-01-14T23:14:58.312Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f5/13bf65864fc314f68cdd6d8ca94adcab064d4d39dbd0b10fef29a9da48fc/regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410", size = 858607, upload-time = "2026-01-14T23:15:00.657Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/040e589834d7a439ee43fb0e1e902bc81bd58a5ba81acffe586bb3321d35/regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4", size = 763729, upload-time = "2026-01-14T23:15:02.248Z" }, + { url = "https://files.pythonhosted.org/packages/9b/84/6921e8129687a427edf25a34a5594b588b6d88f491320b9de5b6339a4fcb/regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d", size = 850697, upload-time = "2026-01-14T23:15:03.878Z" }, + { url = "https://files.pythonhosted.org/packages/8a/87/3d06143d4b128f4229158f2de5de6c8f2485170c7221e61bf381313314b2/regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22", size = 789849, upload-time = "2026-01-14T23:15:06.102Z" }, + { url = "https://files.pythonhosted.org/packages/77/69/c50a63842b6bd48850ebc7ab22d46e7a2a32d824ad6c605b218441814639/regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913", size = 266279, upload-time = "2026-01-14T23:15:07.678Z" }, + { url = "https://files.pythonhosted.org/packages/f2/36/39d0b29d087e2b11fd8191e15e81cce1b635fcc845297c67f11d0d19274d/regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a", size = 277166, upload-time = "2026-01-14T23:15:09.257Z" }, + { url = "https://files.pythonhosted.org/packages/28/32/5b8e476a12262748851fa8ab1b0be540360692325975b094e594dfebbb52/regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056", size = 270415, upload-time = "2026-01-14T23:15:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2e/6870bb16e982669b674cce3ee9ff2d1d46ab80528ee6bcc20fb2292efb60/regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e", size = 489164, upload-time = "2026-01-14T23:15:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/9774542e203849b0286badf67199970a44ebdb0cc5fb739f06e47ada72f8/regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10", size = 291218, upload-time = "2026-01-14T23:15:15.647Z" }, + { url = "https://files.pythonhosted.org/packages/b2/87/b0cda79f22b8dee05f774922a214da109f9a4c0eca5da2c9d72d77ea062c/regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc", size = 288895, upload-time = "2026-01-14T23:15:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/3b/6a/0041f0a2170d32be01ab981d6346c83a8934277d82c780d60b127331f264/regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599", size = 798680, upload-time = "2026-01-14T23:15:19.342Z" }, + { url = "https://files.pythonhosted.org/packages/58/de/30e1cfcdbe3e891324aa7568b7c968771f82190df5524fabc1138cb2d45a/regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae", size = 864210, upload-time = "2026-01-14T23:15:22.005Z" }, + { url = "https://files.pythonhosted.org/packages/64/44/4db2f5c5ca0ccd40ff052ae7b1e9731352fcdad946c2b812285a7505ca75/regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5", size = 912358, upload-time = "2026-01-14T23:15:24.569Z" }, + { url = "https://files.pythonhosted.org/packages/79/b6/e6a5665d43a7c42467138c8a2549be432bad22cbd206f5ec87162de74bd7/regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6", size = 803583, upload-time = "2026-01-14T23:15:26.526Z" }, + { url = "https://files.pythonhosted.org/packages/e7/53/7cd478222169d85d74d7437e74750005e993f52f335f7c04ff7adfda3310/regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788", size = 775782, upload-time = "2026-01-14T23:15:29.352Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b5/75f9a9ee4b03a7c009fe60500fe550b45df94f0955ca29af16333ef557c5/regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714", size = 787978, upload-time = "2026-01-14T23:15:31.295Z" }, + { url = "https://files.pythonhosted.org/packages/72/b3/79821c826245bbe9ccbb54f6eadb7879c722fd3e0248c17bfc90bf54e123/regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d", size = 858550, upload-time = "2026-01-14T23:15:33.558Z" }, + { url = "https://files.pythonhosted.org/packages/4a/85/2ab5f77a1c465745bfbfcb3ad63178a58337ae8d5274315e2cc623a822fa/regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3", size = 763747, upload-time = "2026-01-14T23:15:35.206Z" }, + { url = "https://files.pythonhosted.org/packages/6d/84/c27df502d4bfe2873a3e3a7cf1bdb2b9cc10284d1a44797cf38bed790470/regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31", size = 850615, upload-time = "2026-01-14T23:15:37.523Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b7/658a9782fb253680aa8ecb5ccbb51f69e088ed48142c46d9f0c99b46c575/regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3", size = 789951, upload-time = "2026-01-14T23:15:39.582Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2a/5928af114441e059f15b2f63e188bd00c6529b3051c974ade7444b85fcda/regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f", size = 266275, upload-time = "2026-01-14T23:15:42.108Z" }, + { url = "https://files.pythonhosted.org/packages/4f/16/5bfbb89e435897bff28cf0352a992ca719d9e55ebf8b629203c96b6ce4f7/regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e", size = 277145, upload-time = "2026-01-14T23:15:44.244Z" }, + { url = "https://files.pythonhosted.org/packages/56/c1/a09ff7392ef4233296e821aec5f78c51be5e91ffde0d163059e50fd75835/regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337", size = 270411, upload-time = "2026-01-14T23:15:45.858Z" }, + { url = "https://files.pythonhosted.org/packages/3c/38/0cfd5a78e5c6db00e6782fdae70458f89850ce95baa5e8694ab91d89744f/regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be", size = 492068, upload-time = "2026-01-14T23:15:47.616Z" }, + { url = "https://files.pythonhosted.org/packages/50/72/6c86acff16cb7c959c4355826bbf06aad670682d07c8f3998d9ef4fee7cd/regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8", size = 292756, upload-time = "2026-01-14T23:15:49.307Z" }, + { url = "https://files.pythonhosted.org/packages/4e/58/df7fb69eadfe76526ddfce28abdc0af09ffe65f20c2c90932e89d705153f/regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd", size = 291114, upload-time = "2026-01-14T23:15:51.484Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6c/a4011cd1cf96b90d2cdc7e156f91efbd26531e822a7fbb82a43c1016678e/regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a", size = 807524, upload-time = "2026-01-14T23:15:53.102Z" }, + { url = "https://files.pythonhosted.org/packages/1d/25/a53ffb73183f69c3e9f4355c4922b76d2840aee160af6af5fac229b6201d/regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93", size = 873455, upload-time = "2026-01-14T23:15:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/66/0b/8b47fc2e8f97d9b4a851736f3890a5f786443aa8901061c55f24c955f45b/regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af", size = 915007, upload-time = "2026-01-14T23:15:57.041Z" }, + { url = "https://files.pythonhosted.org/packages/c2/fa/97de0d681e6d26fabe71968dbee06dd52819e9a22fdce5dac7256c31ed84/regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09", size = 812794, upload-time = "2026-01-14T23:15:58.916Z" }, + { url = "https://files.pythonhosted.org/packages/22/38/e752f94e860d429654aa2b1c51880bff8dfe8f084268258adf9151cf1f53/regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5", size = 781159, upload-time = "2026-01-14T23:16:00.817Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a7/d739ffaef33c378fc888302a018d7f81080393d96c476b058b8c64fd2b0d/regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794", size = 795558, upload-time = "2026-01-14T23:16:03.267Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c4/542876f9a0ac576100fc73e9c75b779f5c31e3527576cfc9cb3009dcc58a/regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a", size = 868427, upload-time = "2026-01-14T23:16:05.646Z" }, + { url = "https://files.pythonhosted.org/packages/fc/0f/d5655bea5b22069e32ae85a947aa564912f23758e112cdb74212848a1a1b/regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80", size = 769939, upload-time = "2026-01-14T23:16:07.542Z" }, + { url = "https://files.pythonhosted.org/packages/20/06/7e18a4fa9d326daeda46d471a44ef94201c46eaa26dbbb780b5d92cbfdda/regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2", size = 854753, upload-time = "2026-01-14T23:16:10.395Z" }, + { url = "https://files.pythonhosted.org/packages/3b/67/dc8946ef3965e166f558ef3b47f492bc364e96a265eb4a2bb3ca765c8e46/regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60", size = 799559, upload-time = "2026-01-14T23:16:12.347Z" }, + { url = "https://files.pythonhosted.org/packages/a5/61/1bba81ff6d50c86c65d9fd84ce9699dd106438ee4cdb105bf60374ee8412/regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952", size = 268879, upload-time = "2026-01-14T23:16:14.049Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/cef7d4c5fb0ea3ac5c775fd37db5747f7378b29526cc83f572198924ff47/regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10", size = 280317, upload-time = "2026-01-14T23:16:15.718Z" }, + { url = "https://files.pythonhosted.org/packages/b4/52/4317f7a5988544e34ab57b4bde0f04944c4786128c933fb09825924d3e82/regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829", size = 271551, upload-time = "2026-01-14T23:16:17.533Z" }, + { url = "https://files.pythonhosted.org/packages/52/0a/47fa888ec7cbbc7d62c5f2a6a888878e76169170ead271a35239edd8f0e8/regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac", size = 489170, upload-time = "2026-01-14T23:16:19.835Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c4/d000e9b7296c15737c9301708e9e7fbdea009f8e93541b6b43bdb8219646/regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6", size = 291146, upload-time = "2026-01-14T23:16:21.541Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b6/921cc61982e538682bdf3bdf5b2c6ab6b34368da1f8e98a6c1ddc503c9cf/regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2", size = 288986, upload-time = "2026-01-14T23:16:23.381Z" }, + { url = "https://files.pythonhosted.org/packages/ca/33/eb7383dde0bbc93f4fb9d03453aab97e18ad4024ac7e26cef8d1f0a2cff0/regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846", size = 799098, upload-time = "2026-01-14T23:16:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/27/56/b664dccae898fc8d8b4c23accd853f723bde0f026c747b6f6262b688029c/regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b", size = 864980, upload-time = "2026-01-14T23:16:27.297Z" }, + { url = "https://files.pythonhosted.org/packages/16/40/0999e064a170eddd237bae9ccfcd8f28b3aa98a38bf727a086425542a4fc/regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e", size = 911607, upload-time = "2026-01-14T23:16:29.235Z" }, + { url = "https://files.pythonhosted.org/packages/07/78/c77f644b68ab054e5a674fb4da40ff7bffb2c88df58afa82dbf86573092d/regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde", size = 803358, upload-time = "2026-01-14T23:16:31.369Z" }, + { url = "https://files.pythonhosted.org/packages/27/31/d4292ea8566eaa551fafc07797961c5963cf5235c797cc2ae19b85dfd04d/regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5", size = 775833, upload-time = "2026-01-14T23:16:33.141Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b2/cff3bf2fea4133aa6fb0d1e370b37544d18c8350a2fa118c7e11d1db0e14/regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34", size = 788045, upload-time = "2026-01-14T23:16:35.005Z" }, + { url = "https://files.pythonhosted.org/packages/8d/99/2cb9b69045372ec877b6f5124bda4eb4253bc58b8fe5848c973f752bc52c/regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75", size = 859374, upload-time = "2026-01-14T23:16:36.919Z" }, + { url = "https://files.pythonhosted.org/packages/09/16/710b0a5abe8e077b1729a562d2f297224ad079f3a66dce46844c193416c8/regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e", size = 763940, upload-time = "2026-01-14T23:16:38.685Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/7585c8e744e40eb3d32f119191969b91de04c073fca98ec14299041f6e7e/regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160", size = 850112, upload-time = "2026-01-14T23:16:40.646Z" }, + { url = "https://files.pythonhosted.org/packages/af/d6/43e1dd85df86c49a347aa57c1f69d12c652c7b60e37ec162e3096194a278/regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1", size = 789586, upload-time = "2026-01-14T23:16:42.799Z" }, + { url = "https://files.pythonhosted.org/packages/93/38/77142422f631e013f316aaae83234c629555729a9fbc952b8a63ac91462a/regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1", size = 271691, upload-time = "2026-01-14T23:16:44.671Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a9/ab16b4649524ca9e05213c1cdbb7faa85cc2aa90a0230d2f796cbaf22736/regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903", size = 280422, upload-time = "2026-01-14T23:16:46.607Z" }, + { url = "https://files.pythonhosted.org/packages/be/2a/20fd057bf3521cb4791f69f869635f73e0aaf2b9ad2d260f728144f9047c/regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705", size = 273467, upload-time = "2026-01-14T23:16:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/ad/77/0b1e81857060b92b9cad239104c46507dd481b3ff1fa79f8e7f865aae38a/regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8", size = 492073, upload-time = "2026-01-14T23:16:51.154Z" }, + { url = "https://files.pythonhosted.org/packages/70/f3/f8302b0c208b22c1e4f423147e1913fd475ddd6230565b299925353de644/regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf", size = 292757, upload-time = "2026-01-14T23:16:53.08Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f0/ef55de2460f3b4a6da9d9e7daacd0cb79d4ef75c64a2af316e68447f0df0/regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d", size = 291122, upload-time = "2026-01-14T23:16:55.383Z" }, + { url = "https://files.pythonhosted.org/packages/cf/55/bb8ccbacabbc3a11d863ee62a9f18b160a83084ea95cdfc5d207bfc3dd75/regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84", size = 807761, upload-time = "2026-01-14T23:16:57.251Z" }, + { url = "https://files.pythonhosted.org/packages/8f/84/f75d937f17f81e55679a0509e86176e29caa7298c38bd1db7ce9c0bf6075/regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df", size = 873538, upload-time = "2026-01-14T23:16:59.349Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d9/0da86327df70349aa8d86390da91171bd3ca4f0e7c1d1d453a9c10344da3/regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434", size = 915066, upload-time = "2026-01-14T23:17:01.607Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5e/f660fb23fc77baa2a61aa1f1fe3a4eea2bbb8a286ddec148030672e18834/regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a", size = 812938, upload-time = "2026-01-14T23:17:04.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/a47a29bfecebbbfd1e5cd3f26b28020a97e4820f1c5148e66e3b7d4b4992/regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10", size = 781314, upload-time = "2026-01-14T23:17:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/65/ec/7ec2bbfd4c3f4e494a24dec4c6943a668e2030426b1b8b949a6462d2c17b/regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac", size = 795652, upload-time = "2026-01-14T23:17:08.521Z" }, + { url = "https://files.pythonhosted.org/packages/46/79/a5d8651ae131fe27d7c521ad300aa7f1c7be1dbeee4d446498af5411b8a9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea", size = 868550, upload-time = "2026-01-14T23:17:10.573Z" }, + { url = "https://files.pythonhosted.org/packages/06/b7/25635d2809664b79f183070786a5552dd4e627e5aedb0065f4e3cf8ee37d/regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e", size = 769981, upload-time = "2026-01-14T23:17:12.871Z" }, + { url = "https://files.pythonhosted.org/packages/16/8b/fc3fcbb2393dcfa4a6c5ffad92dc498e842df4581ea9d14309fcd3c55fb9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521", size = 854780, upload-time = "2026-01-14T23:17:14.837Z" }, + { url = "https://files.pythonhosted.org/packages/d0/38/dde117c76c624713c8a2842530be9c93ca8b606c0f6102d86e8cd1ce8bea/regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db", size = 799778, upload-time = "2026-01-14T23:17:17.369Z" }, + { url = "https://files.pythonhosted.org/packages/e3/0d/3a6cfa9ae99606afb612d8fb7a66b245a9d5ff0f29bb347c8a30b6ad561b/regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e", size = 274667, upload-time = "2026-01-14T23:17:19.301Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b2/297293bb0742fd06b8d8e2572db41a855cdf1cae0bf009b1cb74fe07e196/regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf", size = 284386, upload-time = "2026-01-14T23:17:21.231Z" }, + { url = "https://files.pythonhosted.org/packages/95/e4/a3b9480c78cf8ee86626cb06f8d931d74d775897d44201ccb813097ae697/regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70", size = 274837, upload-time = "2026-01-14T23:17:23.146Z" }, +] + [[package]] name = "requests" version = "2.32.4" @@ -1447,6 +1869,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/5c/799a1efb8b5abab56e8a9f2a0b72d12bd64bb55815e9476c7d0a2887d2f7/ruff-0.12.8-py3-none-win_arm64.whl", hash = "sha256:c90e1a334683ce41b0e7a04f41790c429bf5073b62c1ae701c9dc5b3d14f0749", size = 11884718, upload-time = "2025-08-07T19:05:42.866Z" }, ] +[[package]] +name = "safetensors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6a/4d08d89a6fcbe905c5ae68b8b34f0791850882fc19782d0d02c65abbdf3b/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4729811a6640d019a4b7ba8638ee2fd21fa5ca8c7e7bdf0fed62068fcaac737", size = 492430, upload-time = "2025-11-19T15:18:11.884Z" }, + { url = "https://files.pythonhosted.org/packages/dd/29/59ed8152b30f72c42d00d241e58eaca558ae9dbfa5695206e2e0f54c7063/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12f49080303fa6bb424b362149a12949dfbbf1e06811a88f2307276b0c131afd", size = 503977, upload-time = "2025-11-19T15:18:17.523Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4811bfec67fa260e791369b16dab105e4bae82686120554cc484064e22b4/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0071bffba4150c2f46cae1432d31995d77acfd9f8db598b5d1a2ce67e8440ad2", size = 623890, upload-time = "2025-11-19T15:18:22.666Z" }, + { url = "https://files.pythonhosted.org/packages/58/5b/632a58724221ef03d78ab65062e82a1010e1bef8e8e0b9d7c6d7b8044841/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473b32699f4200e69801bf5abf93f1a4ecd432a70984df164fc22ccf39c4a6f3", size = 531885, upload-time = "2025-11-19T15:18:27.146Z" }, +] + [[package]] name = "secretstorage" version = "3.3.3" @@ -1677,6 +2125,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/da/545b75d420bb23b5d494b0517757b351963e974e79933f01e05c929f20a6/starlette-0.49.1-py3-none-any.whl", hash = "sha256:d92ce9f07e4a3caa3ac13a79523bd18e3bc0042bb8ff2d759a8e7dd0e1859875", size = 74175, upload-time = "2025-10-28T17:34:09.13Z" }, ] +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + [[package]] name = "tenacity" version = "9.1.2" @@ -1686,6 +2146,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, ] +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, + { url = "https://files.pythonhosted.org/packages/84/04/655b79dbcc9b3ac5f1479f18e931a344af67e5b7d3b251d2dcdcd7558592/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301, upload-time = "2026-01-05T10:40:34.858Z" }, + { url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" }, + { url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" }, +] + [[package]] name = "toml" version = "0.10.2" @@ -1734,6 +2224,67 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, ] +[[package]] +name = "torch" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "setuptools", marker = "python_full_version >= '3.12'" }, + { name = "sympy" }, + { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/56/9577683b23072075ed2e40d725c52c2019d71a972fab8e083763da8e707e/torch-2.9.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:1cc208435f6c379f9b8fdfd5ceb5be1e3b72a6bdf1cb46c0d2812aa73472db9e", size = 104207681, upload-time = "2025-11-12T15:19:56.48Z" }, + { url = "https://files.pythonhosted.org/packages/38/45/be5a74f221df8f4b609b78ff79dc789b0cc9017624544ac4dd1c03973150/torch-2.9.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:9fd35c68b3679378c11f5eb73220fdcb4e6f4592295277fbb657d31fd053237c", size = 899794036, upload-time = "2025-11-12T15:21:01.886Z" }, + { url = "https://files.pythonhosted.org/packages/67/95/a581e8a382596b69385a44bab2733f1273d45c842f5d4a504c0edc3133b6/torch-2.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:2af70e3be4a13becba4655d6cc07dcfec7ae844db6ac38d6c1dafeb245d17d65", size = 110969861, upload-time = "2025-11-12T15:21:30.145Z" }, + { url = "https://files.pythonhosted.org/packages/ad/51/1756dc128d2bf6ea4e0a915cb89ea5e730315ff33d60c1ff56fd626ba3eb/torch-2.9.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:a83b0e84cc375e3318a808d032510dde99d696a85fe9473fc8575612b63ae951", size = 74452222, upload-time = "2025-11-12T15:20:46.223Z" }, + { url = "https://files.pythonhosted.org/packages/15/db/c064112ac0089af3d2f7a2b5bfbabf4aa407a78b74f87889e524b91c5402/torch-2.9.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:62b3fd888277946918cba4478cf849303da5359f0fb4e3bfb86b0533ba2eaf8d", size = 104220430, upload-time = "2025-11-12T15:20:31.705Z" }, + { url = "https://files.pythonhosted.org/packages/56/be/76eaa36c9cd032d3b01b001e2c5a05943df75f26211f68fae79e62f87734/torch-2.9.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d033ff0ac3f5400df862a51bdde9bad83561f3739ea0046e68f5401ebfa67c1b", size = 899821446, upload-time = "2025-11-12T15:20:15.544Z" }, + { url = "https://files.pythonhosted.org/packages/47/cc/7a2949e38dfe3244c4df21f0e1c27bce8aedd6c604a587dd44fc21017cb4/torch-2.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:0d06b30a9207b7c3516a9e0102114024755a07045f0c1d2f2a56b1819ac06bcb", size = 110973074, upload-time = "2025-11-12T15:21:39.958Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ce/7d251155a783fb2c1bb6837b2b7023c622a2070a0a72726ca1df47e7ea34/torch-2.9.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:52347912d868653e1528b47cafaf79b285b98be3f4f35d5955389b1b95224475", size = 74463887, upload-time = "2025-11-12T15:20:36.611Z" }, + { url = "https://files.pythonhosted.org/packages/0f/27/07c645c7673e73e53ded71705045d6cb5bae94c4b021b03aa8d03eee90ab/torch-2.9.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:da5f6f4d7f4940a173e5572791af238cb0b9e21b1aab592bd8b26da4c99f1cd6", size = 104126592, upload-time = "2025-11-12T15:20:41.62Z" }, + { url = "https://files.pythonhosted.org/packages/19/17/e377a460603132b00760511299fceba4102bd95db1a0ee788da21298ccff/torch-2.9.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:27331cd902fb4322252657f3902adf1c4f6acad9dcad81d8df3ae14c7c4f07c4", size = 899742281, upload-time = "2025-11-12T15:22:17.602Z" }, + { url = "https://files.pythonhosted.org/packages/b1/1a/64f5769025db846a82567fa5b7d21dba4558a7234ee631712ee4771c436c/torch-2.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:81a285002d7b8cfd3fdf1b98aa8df138d41f1a8334fd9ea37511517cedf43083", size = 110940568, upload-time = "2025-11-12T15:21:18.689Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ab/07739fd776618e5882661d04c43f5b5586323e2f6a2d7d84aac20d8f20bd/torch-2.9.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:c0d25d1d8e531b8343bea0ed811d5d528958f1dcbd37e7245bc686273177ad7e", size = 74479191, upload-time = "2025-11-12T15:21:25.816Z" }, + { url = "https://files.pythonhosted.org/packages/20/60/8fc5e828d050bddfab469b3fe78e5ab9a7e53dda9c3bdc6a43d17ce99e63/torch-2.9.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c29455d2b910b98738131990394da3e50eea8291dfeb4b12de71ecf1fdeb21cb", size = 104135743, upload-time = "2025-11-12T15:21:34.936Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b7/6d3f80e6918213babddb2a37b46dbb14c15b14c5f473e347869a51f40e1f/torch-2.9.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:524de44cd13931208ba2c4bde9ec7741fd4ae6bfd06409a604fc32f6520c2bc9", size = 899749493, upload-time = "2025-11-12T15:24:36.356Z" }, + { url = "https://files.pythonhosted.org/packages/a6/47/c7843d69d6de8938c1cbb1eba426b1d48ddf375f101473d3e31a5fc52b74/torch-2.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:545844cc16b3f91e08ce3b40e9c2d77012dd33a48d505aed34b7740ed627a1b2", size = 110944162, upload-time = "2025-11-12T15:21:53.151Z" }, + { url = "https://files.pythonhosted.org/packages/28/0e/2a37247957e72c12151b33a01e4df651d9d155dd74d8cfcbfad15a79b44a/torch-2.9.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5be4bf7496f1e3ffb1dd44b672adb1ac3f081f204c5ca81eba6442f5f634df8e", size = 74830751, upload-time = "2025-11-12T15:21:43.792Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f7/7a18745edcd7b9ca2381aa03353647bca8aace91683c4975f19ac233809d/torch-2.9.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:30a3e170a84894f3652434b56d59a64a2c11366b0ed5776fab33c2439396bf9a", size = 104142929, upload-time = "2025-11-12T15:21:48.319Z" }, + { url = "https://files.pythonhosted.org/packages/f4/dd/f1c0d879f2863ef209e18823a988dc7a1bf40470750e3ebe927efdb9407f/torch-2.9.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:8301a7b431e51764629208d0edaa4f9e4c33e6df0f2f90b90e261d623df6a4e2", size = 899748978, upload-time = "2025-11-12T15:23:04.568Z" }, + { url = "https://files.pythonhosted.org/packages/1f/9f/6986b83a53b4d043e36f3f898b798ab51f7f20fdf1a9b01a2720f445043d/torch-2.9.1-cp313-cp313t-win_amd64.whl", hash = "sha256:2e1c42c0ae92bf803a4b2409fdfed85e30f9027a66887f5e7dcdbc014c7531db", size = 111176995, upload-time = "2025-11-12T15:22:01.618Z" }, + { url = "https://files.pythonhosted.org/packages/40/60/71c698b466dd01e65d0e9514b5405faae200c52a76901baf6906856f17e4/torch-2.9.1-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:2c14b3da5df416cf9cb5efab83aa3056f5b8cd8620b8fde81b4987ecab730587", size = 74480347, upload-time = "2025-11-12T15:21:57.648Z" }, + { url = "https://files.pythonhosted.org/packages/48/50/c4b5112546d0d13cc9eaa1c732b823d676a9f49ae8b6f97772f795874a03/torch-2.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1edee27a7c9897f4e0b7c14cfc2f3008c571921134522d5b9b5ec4ebbc69041a", size = 74433245, upload-time = "2025-11-12T15:22:39.027Z" }, + { url = "https://files.pythonhosted.org/packages/81/c9/2628f408f0518b3bae49c95f5af3728b6ab498c8624ab1e03a43dd53d650/torch-2.9.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:19d144d6b3e29921f1fc70503e9f2fc572cde6a5115c0c0de2f7ca8b1483e8b6", size = 104134804, upload-time = "2025-11-12T15:22:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/28/fc/5bc91d6d831ae41bf6e9e6da6468f25330522e92347c9156eb3f1cb95956/torch-2.9.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:c432d04376f6d9767a9852ea0def7b47a7bbc8e7af3b16ac9cf9ce02b12851c9", size = 899747132, upload-time = "2025-11-12T15:23:36.068Z" }, + { url = "https://files.pythonhosted.org/packages/63/5d/e8d4e009e52b6b2cf1684bde2a6be157b96fb873732542fb2a9a99e85a83/torch-2.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:d187566a2cdc726fc80138c3cdb260970fab1c27e99f85452721f7759bbd554d", size = 110934845, upload-time = "2025-11-12T15:22:48.367Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b2/2d15a52516b2ea3f414643b8de68fa4cb220d3877ac8b1028c83dc8ca1c4/torch-2.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cb10896a1f7fedaddbccc2017ce6ca9ecaaf990f0973bdfcf405439750118d2c", size = 74823558, upload-time = "2025-11-12T15:22:43.392Z" }, + { url = "https://files.pythonhosted.org/packages/86/5c/5b2e5d84f5b9850cd1e71af07524d8cbb74cba19379800f1f9f7c997fc70/torch-2.9.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:0a2bd769944991c74acf0c4ef23603b9c777fdf7637f115605a4b2d8023110c7", size = 104145788, upload-time = "2025-11-12T15:23:52.109Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8c/3da60787bcf70add986c4ad485993026ac0ca74f2fc21410bc4eb1bb7695/torch-2.9.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:07c8a9660bc9414c39cac530ac83b1fb1b679d7155824144a40a54f4a47bfa73", size = 899735500, upload-time = "2025-11-12T15:24:08.788Z" }, + { url = "https://files.pythonhosted.org/packages/db/2b/f7818f6ec88758dfd21da46b6cd46af9d1b3433e53ddbb19ad1e0da17f9b/torch-2.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c88d3299ddeb2b35dcc31753305612db485ab6f1823e37fb29451c8b2732b87e", size = 111163659, upload-time = "2025-11-12T15:23:20.009Z" }, +] + [[package]] name = "tqdm" version = "4.67.1" @@ -1746,6 +2297,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, ] +[[package]] +name = "transformers" +version = "4.57.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "requests" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/35/67252acc1b929dc88b6602e8c4a982e64f31e733b804c14bc24b47da35e6/transformers-4.57.6.tar.gz", hash = "sha256:55e44126ece9dc0a291521b7e5492b572e6ef2766338a610b9ab5afbb70689d3", size = 10134912, upload-time = "2026-01-16T10:38:39.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/b8/e484ef633af3887baeeb4b6ad12743363af7cce68ae51e938e00aaa0529d/transformers-4.57.6-py3-none-any.whl", hash = "sha256:4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550", size = 11993498, upload-time = "2026-01-16T10:38:31.289Z" }, +] + +[[package]] +name = "triton" +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/6e/676ab5019b4dde8b9b7bab71245102fc02778ef3df48218b298686b9ffd6/triton-3.5.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5fc53d849f879911ea13f4a877243afc513187bc7ee92d1f2c0f1ba3169e3c94", size = 170320692, upload-time = "2025-11-11T17:40:46.074Z" }, + { url = "https://files.pythonhosted.org/packages/b0/72/ec90c3519eaf168f22cb1757ad412f3a2add4782ad3a92861c9ad135d886/triton-3.5.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61413522a48add32302353fdbaaf92daaaab06f6b5e3229940d21b5207f47579", size = 170425802, upload-time = "2025-11-11T17:40:53.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/50/9a8358d3ef58162c0a415d173cfb45b67de60176e1024f71fbc4d24c0b6d/triton-3.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d2c6b915a03888ab931a9fd3e55ba36785e1fe70cbea0b40c6ef93b20fc85232", size = 170470207, upload-time = "2025-11-11T17:41:00.253Z" }, + { url = "https://files.pythonhosted.org/packages/27/46/8c3bbb5b0a19313f50edcaa363b599e5a1a5ac9683ead82b9b80fe497c8d/triton-3.5.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3f4346b6ebbd4fad18773f5ba839114f4826037c9f2f34e0148894cd5dd3dba", size = 170470410, upload-time = "2025-11-11T17:41:06.319Z" }, + { url = "https://files.pythonhosted.org/packages/37/92/e97fcc6b2c27cdb87ce5ee063d77f8f26f19f06916aa680464c8104ef0f6/triton-3.5.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0b4d2c70127fca6a23e247f9348b8adde979d2e7a20391bfbabaac6aebc7e6a8", size = 170579924, upload-time = "2025-11-11T17:41:12.455Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e6/c595c35e5c50c4bc56a7bac96493dad321e9e29b953b526bbbe20f9911d0/triton-3.5.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0637b1efb1db599a8e9dc960d53ab6e4637db7d4ab6630a0974705d77b14b60", size = 170480488, upload-time = "2025-11-11T17:41:18.222Z" }, + { url = "https://files.pythonhosted.org/packages/16/b5/b0d3d8b901b6a04ca38df5e24c27e53afb15b93624d7fd7d658c7cd9352a/triton-3.5.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bac7f7d959ad0f48c0e97d6643a1cc0fd5786fe61cb1f83b537c6b2d54776478", size = 170582192, upload-time = "2025-11-11T17:41:23.963Z" }, +] + [[package]] name = "twine" version = "6.1.0" From fb991d6e60014c1dd205e44d4afcb0a60559695b Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Wed, 21 Jan 2026 23:23:53 +0200 Subject: [PATCH 13/25] Fix `numpy` version error --- requirements/pandas.txt | 2 +- requirements/transformers.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/pandas.txt b/requirements/pandas.txt index 763566ec..752ecc1e 100644 --- a/requirements/pandas.txt +++ b/requirements/pandas.txt @@ -86,7 +86,7 @@ nh3==0.2.18 # via # -c requirements/requirements.txt # readme-renderer -numpy==2.4.1 +numpy==2.0.2 # via pandas pandas==2.2.3 # via hirundo (pyproject.toml) diff --git a/requirements/transformers.txt b/requirements/transformers.txt index 5a9eef12..056b0bb1 100644 --- a/requirements/transformers.txt +++ b/requirements/transformers.txt @@ -115,7 +115,7 @@ nh3==0.2.18 # via # -c requirements/requirements.txt # readme-renderer -numpy==2.4.1 +numpy==2.0.2 # via # accelerate # peft From 5eea81a46d86c5d873499815b0c1d0980502cee8 Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Thu, 22 Jan 2026 17:35:13 +0200 Subject: [PATCH 14/25] Fix requirements files --- pyproject.toml | 2 + requirements/dev.txt | 126 ++++++++++++++--------------- requirements/docs.txt | 148 ++++++++++++++++++---------------- requirements/pandas.txt | 96 +++++++++++----------- requirements/polars.txt | 88 ++++++++++---------- requirements/requirements.txt | 78 +++++++++--------- requirements/transformers.txt | 92 ++++++++++----------- uv.lock | 2 + 8 files changed, 316 insertions(+), 316 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e37d6ec6..0af15c33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,8 @@ dependencies = [ # ⬆️ Required to fix vulnerability CVE-2026-21441 "setuptools>=78.1.1", # ⬆️ Required to fix vulnerability GHSA-5rjg-fvgr-3xxf + "docutils<0.22.0", + # ⬆️ Required for sphinx compatibility ] [project.scripts] diff --git a/requirements/dev.txt b/requirements/dev.txt index 8f688546..a0578325 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -4,7 +4,7 @@ annotated-types==0.7.0 # via # -c requirements/requirements.txt # pydantic -anyio==4.4.0 +anyio==4.12.1 # via # -c requirements/requirements.txt # httpx @@ -12,38 +12,39 @@ authlib==1.6.6 # via hirundo (pyproject.toml) basedpyright==1.37.1 # via hirundo (pyproject.toml) -bumpver==2023.1129 +bumpver==2025.1131 # via hirundo (pyproject.toml) -certifi==2024.7.4 +certifi==2026.1.4 # via # -c requirements/requirements.txt # httpcore # httpx # requests -cffi==1.17.1 +cffi==2.0.0 # via cryptography -cfgv==3.4.0 +cfgv==3.5.0 # via pre-commit -charset-normalizer==3.3.2 +charset-normalizer==3.4.4 # via # -c requirements/requirements.txt # requests -click==8.1.7 +click==8.3.1 # via # -c requirements/requirements.txt # bumpver # typer colorama==0.4.6 # via bumpver -cryptography==45.0.5 +cryptography==46.0.3 # via # hirundo (pyproject.toml) # authlib -distlib==0.3.8 +distlib==0.4.0 # via virtualenv docutils==0.21.2 # via # -c requirements/requirements.txt + # hirundo (pyproject.toml) # readme-renderer filelock==3.20.3 # via @@ -58,51 +59,49 @@ httpcore==1.0.9 # via # -c requirements/requirements.txt # httpx -httpx==0.27.0 +httpx==0.28.1 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -httpx-sse==0.4.0 +httpx-sse==0.4.3 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -identify==2.6.0 +id==1.5.0 + # via + # -c requirements/requirements.txt + # twine +identify==2.6.16 # via pre-commit -idna==3.7 +idna==3.11 # via # -c requirements/requirements.txt # anyio # httpx # requests -importlib-metadata==8.0.0 - # via - # -c requirements/requirements.txt - # twine -iniconfig==2.0.0 +iniconfig==2.3.0 # via pytest jaraco-classes==3.4.0 # via # -c requirements/requirements.txt # keyring -jaraco-context==5.3.0 +jaraco-context==6.1.0 # via # -c requirements/requirements.txt # keyring -jaraco-functools==4.0.1 +jaraco-functools==4.4.0 # via # -c requirements/requirements.txt # keyring jinja2==3.1.6 # via hirundo (pyproject.toml) -keyring==25.2.1 +keyring==25.7.0 # via # -c requirements/requirements.txt # twine lexid==2021.1006 # via bumpver -looseversion==1.3.0 - # via bumpver -markdown-it-py==3.0.0 +markdown-it-py==4.0.0 # via # -c requirements/requirements.txt # rich @@ -114,59 +113,59 @@ mdurl==0.1.2 # via # -c requirements/requirements.txt # markdown-it-py -more-itertools==10.3.0 +more-itertools==10.8.0 # via # -c requirements/requirements.txt # jaraco-classes # jaraco-functools -nh3==0.2.18 +nh3==0.3.2 # via # -c requirements/requirements.txt # readme-renderer -nodeenv==1.9.1 +nodeenv==1.10.0 # via pre-commit -nodejs-wheel-binaries==24.12.0 +nodejs-wheel-binaries==24.13.0 # via basedpyright packaging==26.0 - # via pytest -pkginfo==1.10.0 # via # -c requirements/requirements.txt + # pytest # twine -platformdirs==4.3.6 +platformdirs==4.5.1 # via # hirundo (pyproject.toml) # virtualenv -pluggy==1.5.0 +pluggy==1.6.0 # via pytest -pre-commit==3.7.1 +pre-commit==4.5.1 # via hirundo (pyproject.toml) -pycparser==2.22 +pycparser==3.0 # via cffi -pydantic==2.8.2 +pydantic==2.12.5 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -pydantic-core==2.20.1 +pydantic-core==2.41.5 # via # -c requirements/requirements.txt # pydantic -pygments==2.18.0 +pygments==2.19.2 # via # -c requirements/requirements.txt + # pytest # readme-renderer # rich -pytest==8.2.2 +pytest==9.0.2 # via # hirundo (pyproject.toml) # pytest-asyncio -pytest-asyncio==0.23.7 +pytest-asyncio==1.3.0 # via hirundo (pyproject.toml) -python-dotenv==1.0.1 +python-dotenv==1.2.1 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -pyyaml==6.0.1 +pyyaml==6.0.3 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) @@ -175,10 +174,11 @@ readme-renderer==44.0 # via # -c requirements/requirements.txt # twine -requests==2.32.4 +requests==2.32.5 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) + # id # requests-toolbelt # twine requests-toolbelt==1.0.0 @@ -189,14 +189,14 @@ rfc3986==2.0.0 # via # -c requirements/requirements.txt # twine -rich==13.7.1 +rich==14.2.0 # via # -c requirements/requirements.txt # twine # typer -ruff==0.12.1 +ruff==0.14.13 # via hirundo (pyproject.toml) -setuptools==80.9.0 +setuptools==80.10.1 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) @@ -204,49 +204,49 @@ shellingham==1.5.4 # via # -c requirements/requirements.txt # typer -sniffio==1.3.1 - # via - # -c requirements/requirements.txt - # anyio - # httpx -stamina==24.2.0 +stamina==25.2.0 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -tenacity==8.5.0 +tenacity==9.1.2 # via # -c requirements/requirements.txt # stamina toml==0.10.2 # via bumpver -tqdm==4.66.5 +tqdm==4.67.1 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -twine==5.1.1 +twine==6.2.0 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -typer==0.12.3 +typer==0.21.1 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -types-pyyaml==6.0.12.20240311 +types-pyyaml==6.0.12.20250915 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -types-requests==2.32.0.20240712 +types-requests==2.32.4.20260107 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -types-setuptools==70.3.0.20240710 +types-setuptools==80.9.0.20251223 # via hirundo (pyproject.toml) -typing-extensions==4.12.2 +typing-extensions==4.15.0 # via # -c requirements/requirements.txt # pydantic # pydantic-core # typer + # typing-inspection +typing-inspection==0.4.2 + # via + # -c requirements/requirements.txt + # pydantic urllib3==2.6.3 # via # -c requirements/requirements.txt @@ -254,13 +254,9 @@ urllib3==2.6.3 # requests # twine # types-requests -uv==0.9.6 +uv==0.9.26 # via hirundo (pyproject.toml) -virtualenv==20.26.6 +virtualenv==20.36.1 # via # hirundo (pyproject.toml) # pre-commit -zipp==3.19.2 - # via - # -c requirements/requirements.txt - # importlib-metadata diff --git a/requirements/docs.txt b/requirements/docs.txt index 013633d9..23d09419 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -1,42 +1,47 @@ # This file was autogenerated by uv via the following command: # uv pip compile --extra docs -o requirements/docs.txt -c requirements/requirements.txt pyproject.toml -alabaster==0.7.16 +accessible-pygments==0.0.5 + # via furo +aiosqlite==0.22.1 + # via esbonio +alabaster==1.0.0 # via sphinx annotated-types==0.7.0 # via # -c requirements/requirements.txt # pydantic -anyio==4.4.0 +anyio==4.12.1 # via # -c requirements/requirements.txt # httpx # starlette # watchfiles -attrs==24.2.0 +attrs==25.4.0 # via # cattrs # lsprotocol + # pygls autodoc-pydantic==2.2.0 # via hirundo (pyproject.toml) -babel==2.15.0 +babel==2.17.0 # via sphinx -beautifulsoup4==4.12.3 +beautifulsoup4==4.14.3 # via furo -cattrs==24.1.2 +cattrs==25.3.0 # via # lsprotocol # pygls -certifi==2024.7.4 +certifi==2026.1.4 # via # -c requirements/requirements.txt # httpcore # httpx # requests -charset-normalizer==3.3.2 +charset-normalizer==3.4.4 # via # -c requirements/requirements.txt # requests -click==8.1.7 +click==8.3.1 # via # -c requirements/requirements.txt # sphinx-click @@ -47,12 +52,14 @@ colorama==0.4.6 docutils==0.21.2 # via # -c requirements/requirements.txt + # hirundo (pyproject.toml) + # esbonio # readme-renderer # sphinx # sphinx-click -esbonio==0.16.5 +esbonio==1.0.0 # via hirundo (pyproject.toml) -furo==2024.7.18 +furo==2025.12.19 # via hirundo (pyproject.toml) h11==0.16.0 # via @@ -64,15 +71,19 @@ httpcore==1.0.9 # via # -c requirements/requirements.txt # httpx -httpx==0.27.0 +httpx==0.28.1 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -httpx-sse==0.4.0 +httpx-sse==0.4.3 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -idna==3.7 +id==1.5.0 + # via + # -c requirements/requirements.txt + # twine +idna==3.11 # via # -c requirements/requirements.txt # anyio @@ -80,19 +91,15 @@ idna==3.7 # requests imagesize==1.4.1 # via sphinx -importlib-metadata==8.0.0 - # via - # -c requirements/requirements.txt - # twine jaraco-classes==3.4.0 # via # -c requirements/requirements.txt # keyring -jaraco-context==5.3.0 +jaraco-context==6.1.0 # via # -c requirements/requirements.txt # keyring -jaraco-functools==4.0.1 +jaraco-functools==4.4.0 # via # -c requirements/requirements.txt # keyring @@ -100,13 +107,13 @@ jinja2==3.1.6 # via # hirundo (pyproject.toml) # sphinx -keyring==25.2.1 +keyring==25.7.0 # via # -c requirements/requirements.txt # twine -lsprotocol==2023.0.1 +lsprotocol==2025.0.0 # via pygls -markdown-it-py==3.0.0 +markdown-it-py==4.0.0 # via # -c requirements/requirements.txt # rich @@ -118,52 +125,50 @@ mdurl==0.1.2 # via # -c requirements/requirements.txt # markdown-it-py -more-itertools==10.3.0 +more-itertools==10.8.0 # via # -c requirements/requirements.txt # jaraco-classes # jaraco-functools -nh3==0.2.18 +nh3==0.3.2 # via # -c requirements/requirements.txt # readme-renderer packaging==26.0 - # via sphinx -pkginfo==1.10.0 # via # -c requirements/requirements.txt + # sphinx # twine -platformdirs==4.3.6 +platformdirs==4.5.1 # via esbonio -pydantic==2.8.2 +pydantic==2.12.5 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) # autodoc-pydantic # pydantic-settings -pydantic-core==2.20.1 +pydantic-core==2.41.5 # via # -c requirements/requirements.txt # pydantic -pydantic-settings==2.3.4 +pydantic-settings==2.12.0 # via autodoc-pydantic -pygls==1.3.1 +pygls==2.0.0 # via esbonio -pygments==2.18.0 +pygments==2.19.2 # via # -c requirements/requirements.txt + # accessible-pygments # furo # readme-renderer # rich # sphinx -pyspellchecker==0.8.1 - # via esbonio -python-dotenv==1.0.1 +python-dotenv==1.2.1 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) # pydantic-settings -pyyaml==6.0.1 +pyyaml==6.0.3 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) @@ -171,10 +176,11 @@ readme-renderer==44.0 # via # -c requirements/requirements.txt # twine -requests==2.32.4 +requests==2.32.5 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) + # id # requests-toolbelt # sphinx # twine @@ -186,12 +192,12 @@ rfc3986==2.0.0 # via # -c requirements/requirements.txt # twine -rich==13.7.1 +rich==14.2.0 # via # -c requirements/requirements.txt # twine # typer -setuptools==80.9.0 +setuptools==80.10.1 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) @@ -199,20 +205,14 @@ shellingham==1.5.4 # via # -c requirements/requirements.txt # typer -sniffio==1.3.1 - # via - # -c requirements/requirements.txt - # anyio - # httpx -snowballstemmer==2.2.0 +snowballstemmer==3.0.1 # via sphinx -soupsieve==2.5 +soupsieve==2.8.3 # via beautifulsoup4 -sphinx==7.4.7 +sphinx==8.1.3 # via # hirundo (pyproject.toml) # autodoc-pydantic - # esbonio # furo # sphinx-autobuild # sphinx-basic-ng @@ -222,60 +222,68 @@ sphinx-autobuild==2024.10.3 # via hirundo (pyproject.toml) sphinx-basic-ng==1.0.0b2 # via furo -sphinx-click==6.0.0 +sphinx-click==6.2.0 # via hirundo (pyproject.toml) sphinx-multiversion==0.2.4 # via hirundo (pyproject.toml) -sphinxcontrib-applehelp==1.0.8 +sphinxcontrib-applehelp==2.0.0 # via sphinx -sphinxcontrib-devhelp==1.0.6 +sphinxcontrib-devhelp==2.0.0 # via sphinx -sphinxcontrib-htmlhelp==2.0.6 +sphinxcontrib-htmlhelp==2.1.0 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx -sphinxcontrib-qthelp==1.0.8 +sphinxcontrib-qthelp==2.0.0 # via sphinx -sphinxcontrib-serializinghtml==1.1.10 +sphinxcontrib-serializinghtml==2.0.0 # via sphinx -stamina==24.2.0 +stamina==25.2.0 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -starlette==0.49.1 +starlette==0.52.1 # via # hirundo (pyproject.toml) # sphinx-autobuild -tenacity==8.5.0 +tenacity==9.1.2 # via # -c requirements/requirements.txt # stamina -tqdm==4.66.5 +tqdm==4.67.1 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -twine==5.1.1 +twine==6.2.0 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -typer==0.12.3 +typer==0.21.1 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -types-pyyaml==6.0.12.20240311 +types-pyyaml==6.0.12.20250915 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -types-requests==2.32.0.20240712 +types-requests==2.32.4.20260107 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -typing-extensions==4.12.2 +typing-extensions==4.15.0 # via # -c requirements/requirements.txt + # beautifulsoup4 + # cattrs # pydantic # pydantic-core # typer + # typing-inspection +typing-inspection==0.4.2 + # via + # -c requirements/requirements.txt + # pydantic + # pydantic-settings urllib3==2.6.3 # via # -c requirements/requirements.txt @@ -283,13 +291,11 @@ urllib3==2.6.3 # requests # twine # types-requests -uvicorn==0.30.3 - # via sphinx-autobuild -watchfiles==0.22.0 +uvicorn==0.40.0 # via sphinx-autobuild -websockets==12.0 +watchfiles==1.1.1 # via sphinx-autobuild -zipp==3.19.2 +websockets==16.0 # via - # -c requirements/requirements.txt - # importlib-metadata + # esbonio + # sphinx-autobuild diff --git a/requirements/pandas.txt b/requirements/pandas.txt index 752ecc1e..d344621b 100644 --- a/requirements/pandas.txt +++ b/requirements/pandas.txt @@ -4,27 +4,28 @@ annotated-types==0.7.0 # via # -c requirements/requirements.txt # pydantic -anyio==4.4.0 +anyio==4.12.1 # via # -c requirements/requirements.txt # httpx -certifi==2024.7.4 +certifi==2026.1.4 # via # -c requirements/requirements.txt # httpcore # httpx # requests -charset-normalizer==3.3.2 +charset-normalizer==3.4.4 # via # -c requirements/requirements.txt # requests -click==8.1.7 +click==8.3.1 # via # -c requirements/requirements.txt # typer docutils==0.21.2 # via # -c requirements/requirements.txt + # hirundo (pyproject.toml) # readme-renderer h11==0.16.0 # via @@ -35,41 +36,41 @@ httpcore==1.0.9 # via # -c requirements/requirements.txt # httpx -httpx==0.27.0 +httpx==0.28.1 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -httpx-sse==0.4.0 +httpx-sse==0.4.3 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -idna==3.7 +id==1.5.0 + # via + # -c requirements/requirements.txt + # twine +idna==3.11 # via # -c requirements/requirements.txt # anyio # httpx # requests -importlib-metadata==8.0.0 - # via - # -c requirements/requirements.txt - # twine jaraco-classes==3.4.0 # via # -c requirements/requirements.txt # keyring -jaraco-context==5.3.0 +jaraco-context==6.1.0 # via # -c requirements/requirements.txt # keyring -jaraco-functools==4.0.1 +jaraco-functools==4.4.0 # via # -c requirements/requirements.txt # keyring -keyring==25.2.1 +keyring==25.7.0 # via # -c requirements/requirements.txt # twine -markdown-it-py==3.0.0 +markdown-it-py==4.0.0 # via # -c requirements/requirements.txt # rich @@ -77,45 +78,45 @@ mdurl==0.1.2 # via # -c requirements/requirements.txt # markdown-it-py -more-itertools==10.3.0 +more-itertools==10.8.0 # via # -c requirements/requirements.txt # jaraco-classes # jaraco-functools -nh3==0.2.18 +nh3==0.3.2 # via # -c requirements/requirements.txt # readme-renderer -numpy==2.0.2 +numpy==2.2.6 # via pandas -pandas==2.2.3 - # via hirundo (pyproject.toml) -pkginfo==1.10.0 +packaging==26.0 # via # -c requirements/requirements.txt # twine -pydantic==2.8.2 +pandas==2.3.3 + # via hirundo (pyproject.toml) +pydantic==2.12.5 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -pydantic-core==2.20.1 +pydantic-core==2.41.5 # via # -c requirements/requirements.txt # pydantic -pygments==2.18.0 +pygments==2.19.2 # via # -c requirements/requirements.txt # readme-renderer # rich python-dateutil==2.9.0.post0 # via pandas -python-dotenv==1.0.1 +python-dotenv==1.2.1 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -pytz==2024.1 +pytz==2025.2 # via pandas -pyyaml==6.0.1 +pyyaml==6.0.3 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) @@ -123,10 +124,11 @@ readme-renderer==44.0 # via # -c requirements/requirements.txt # twine -requests==2.32.4 +requests==2.32.5 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) + # id # requests-toolbelt # twine requests-toolbelt==1.0.0 @@ -137,12 +139,12 @@ rfc3986==2.0.0 # via # -c requirements/requirements.txt # twine -rich==13.7.1 +rich==14.2.0 # via # -c requirements/requirements.txt # twine # typer -setuptools==80.9.0 +setuptools==80.10.1 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) @@ -150,48 +152,48 @@ shellingham==1.5.4 # via # -c requirements/requirements.txt # typer -six==1.16.0 +six==1.17.0 # via python-dateutil -sniffio==1.3.1 - # via - # -c requirements/requirements.txt - # anyio - # httpx -stamina==24.2.0 +stamina==25.2.0 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -tenacity==8.5.0 +tenacity==9.1.2 # via # -c requirements/requirements.txt # stamina -tqdm==4.66.5 +tqdm==4.67.1 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -twine==5.1.1 +twine==6.2.0 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -typer==0.12.3 +typer==0.21.1 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -types-pyyaml==6.0.12.20240311 +types-pyyaml==6.0.12.20250915 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -types-requests==2.32.0.20240712 +types-requests==2.32.4.20260107 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -typing-extensions==4.12.2 +typing-extensions==4.15.0 # via # -c requirements/requirements.txt # pydantic # pydantic-core # typer -tzdata==2024.1 + # typing-inspection +typing-inspection==0.4.2 + # via + # -c requirements/requirements.txt + # pydantic +tzdata==2025.3 # via pandas urllib3==2.6.3 # via @@ -200,7 +202,3 @@ urllib3==2.6.3 # requests # twine # types-requests -zipp==3.19.2 - # via - # -c requirements/requirements.txt - # importlib-metadata diff --git a/requirements/polars.txt b/requirements/polars.txt index 78f096c1..2f86f332 100644 --- a/requirements/polars.txt +++ b/requirements/polars.txt @@ -4,27 +4,28 @@ annotated-types==0.7.0 # via # -c requirements/requirements.txt # pydantic -anyio==4.4.0 +anyio==4.12.1 # via # -c requirements/requirements.txt # httpx -certifi==2024.7.4 +certifi==2026.1.4 # via # -c requirements/requirements.txt # httpcore # httpx # requests -charset-normalizer==3.3.2 +charset-normalizer==3.4.4 # via # -c requirements/requirements.txt # requests -click==8.1.7 +click==8.3.1 # via # -c requirements/requirements.txt # typer docutils==0.21.2 # via # -c requirements/requirements.txt + # hirundo (pyproject.toml) # readme-renderer h11==0.16.0 # via @@ -35,41 +36,41 @@ httpcore==1.0.9 # via # -c requirements/requirements.txt # httpx -httpx==0.27.0 +httpx==0.28.1 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -httpx-sse==0.4.0 +httpx-sse==0.4.3 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -idna==3.7 +id==1.5.0 + # via + # -c requirements/requirements.txt + # twine +idna==3.11 # via # -c requirements/requirements.txt # anyio # httpx # requests -importlib-metadata==8.0.0 - # via - # -c requirements/requirements.txt - # twine jaraco-classes==3.4.0 # via # -c requirements/requirements.txt # keyring -jaraco-context==5.3.0 +jaraco-context==6.1.0 # via # -c requirements/requirements.txt # keyring -jaraco-functools==4.0.1 +jaraco-functools==4.4.0 # via # -c requirements/requirements.txt # keyring -keyring==25.2.1 +keyring==25.7.0 # via # -c requirements/requirements.txt # twine -markdown-it-py==3.0.0 +markdown-it-py==4.0.0 # via # -c requirements/requirements.txt # rich @@ -77,39 +78,41 @@ mdurl==0.1.2 # via # -c requirements/requirements.txt # markdown-it-py -more-itertools==10.3.0 +more-itertools==10.8.0 # via # -c requirements/requirements.txt # jaraco-classes # jaraco-functools -nh3==0.2.18 +nh3==0.3.2 # via # -c requirements/requirements.txt # readme-renderer -pkginfo==1.10.0 +packaging==26.0 # via # -c requirements/requirements.txt # twine -polars==1.17.1 +polars==1.37.1 # via hirundo (pyproject.toml) -pydantic==2.8.2 +polars-runtime-32==1.37.1 + # via polars +pydantic==2.12.5 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -pydantic-core==2.20.1 +pydantic-core==2.41.5 # via # -c requirements/requirements.txt # pydantic -pygments==2.18.0 +pygments==2.19.2 # via # -c requirements/requirements.txt # readme-renderer # rich -python-dotenv==1.0.1 +python-dotenv==1.2.1 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -pyyaml==6.0.1 +pyyaml==6.0.3 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) @@ -117,10 +120,11 @@ readme-renderer==44.0 # via # -c requirements/requirements.txt # twine -requests==2.32.4 +requests==2.32.5 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) + # id # requests-toolbelt # twine requests-toolbelt==1.0.0 @@ -131,12 +135,12 @@ rfc3986==2.0.0 # via # -c requirements/requirements.txt # twine -rich==13.7.1 +rich==14.2.0 # via # -c requirements/requirements.txt # twine # typer -setuptools==80.9.0 +setuptools==80.10.1 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) @@ -144,45 +148,45 @@ shellingham==1.5.4 # via # -c requirements/requirements.txt # typer -sniffio==1.3.1 - # via - # -c requirements/requirements.txt - # anyio - # httpx -stamina==24.2.0 +stamina==25.2.0 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -tenacity==8.5.0 +tenacity==9.1.2 # via # -c requirements/requirements.txt # stamina -tqdm==4.66.5 +tqdm==4.67.1 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -twine==5.1.1 +twine==6.2.0 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -typer==0.12.3 +typer==0.21.1 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -types-pyyaml==6.0.12.20240311 +types-pyyaml==6.0.12.20250915 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -types-requests==2.32.0.20240712 +types-requests==2.32.4.20260107 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -typing-extensions==4.12.2 +typing-extensions==4.15.0 # via # -c requirements/requirements.txt # pydantic # pydantic-core # typer + # typing-inspection +typing-inspection==0.4.2 + # via + # -c requirements/requirements.txt + # pydantic urllib3==2.6.3 # via # -c requirements/requirements.txt @@ -190,7 +194,3 @@ urllib3==2.6.3 # requests # twine # types-requests -zipp==3.19.2 - # via - # -c requirements/requirements.txt - # importlib-metadata diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 96adea09..62c62352 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -2,115 +2,115 @@ # uv pip compile -o requirements/requirements.txt pyproject.toml annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.12.1 # via httpx -certifi==2024.7.4 +certifi==2026.1.4 # via # httpcore # httpx # requests -charset-normalizer==3.3.2 +charset-normalizer==3.4.4 # via requests -click==8.1.7 +click==8.3.1 # via typer docutils==0.21.2 - # via readme-renderer + # via + # hirundo (pyproject.toml) + # readme-renderer h11==0.16.0 # via # hirundo (pyproject.toml) # httpcore httpcore==1.0.9 # via httpx -httpx==0.27.0 +httpx==0.28.1 # via hirundo (pyproject.toml) -httpx-sse==0.4.0 +httpx-sse==0.4.3 # via hirundo (pyproject.toml) -idna==3.7 +id==1.5.0 + # via twine +idna==3.11 # via # anyio # httpx # requests -importlib-metadata==8.0.0 - # via twine jaraco-classes==3.4.0 # via keyring -jaraco-context==5.3.0 +jaraco-context==6.1.0 # via keyring -jaraco-functools==4.0.1 +jaraco-functools==4.4.0 # via keyring -keyring==25.2.1 +keyring==25.7.0 # via twine -markdown-it-py==3.0.0 +markdown-it-py==4.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -more-itertools==10.3.0 +more-itertools==10.8.0 # via # jaraco-classes # jaraco-functools -nh3==0.2.18 +nh3==0.3.2 # via readme-renderer -pkginfo==1.10.0 +packaging==26.0 # via twine -pydantic==2.8.2 +pydantic==2.12.5 # via hirundo (pyproject.toml) -pydantic-core==2.20.1 +pydantic-core==2.41.5 # via pydantic -pygments==2.18.0 +pygments==2.19.2 # via # readme-renderer # rich -python-dotenv==1.0.1 +python-dotenv==1.2.1 # via hirundo (pyproject.toml) -pyyaml==6.0.1 +pyyaml==6.0.3 # via hirundo (pyproject.toml) readme-renderer==44.0 # via twine -requests==2.32.4 +requests==2.32.5 # via # hirundo (pyproject.toml) + # id # requests-toolbelt # twine requests-toolbelt==1.0.0 # via twine rfc3986==2.0.0 # via twine -rich==13.7.1 +rich==14.2.0 # via # twine # typer -setuptools==80.9.0 +setuptools==80.10.1 # via hirundo (pyproject.toml) shellingham==1.5.4 # via typer -sniffio==1.3.1 - # via - # anyio - # httpx -stamina==24.2.0 +stamina==25.2.0 # via hirundo (pyproject.toml) -tenacity==8.5.0 +tenacity==9.1.2 # via stamina -tqdm==4.66.5 +tqdm==4.67.1 # via hirundo (pyproject.toml) -twine==5.1.1 +twine==6.2.0 # via hirundo (pyproject.toml) -typer==0.12.3 +typer==0.21.1 # via hirundo (pyproject.toml) -types-pyyaml==6.0.12.20240311 +types-pyyaml==6.0.12.20250915 # via hirundo (pyproject.toml) -types-requests==2.32.0.20240712 +types-requests==2.32.4.20260107 # via hirundo (pyproject.toml) -typing-extensions==4.12.2 +typing-extensions==4.15.0 # via # pydantic # pydantic-core # typer + # typing-inspection +typing-inspection==0.4.2 + # via pydantic urllib3==2.6.3 # via # hirundo (pyproject.toml) # requests # twine # types-requests -zipp==3.19.2 - # via importlib-metadata diff --git a/requirements/transformers.txt b/requirements/transformers.txt index 056b0bb1..5ac08939 100644 --- a/requirements/transformers.txt +++ b/requirements/transformers.txt @@ -8,27 +8,28 @@ annotated-types==0.7.0 # via # -c requirements/requirements.txt # pydantic -anyio==4.4.0 +anyio==4.12.1 # via # -c requirements/requirements.txt # httpx -certifi==2024.7.4 +certifi==2026.1.4 # via # -c requirements/requirements.txt # httpcore # httpx # requests -charset-normalizer==3.3.2 +charset-normalizer==3.4.4 # via # -c requirements/requirements.txt # requests -click==8.1.7 +click==8.3.1 # via # -c requirements/requirements.txt # typer docutils==0.21.2 # via # -c requirements/requirements.txt + # hirundo (pyproject.toml) # readme-renderer filelock==3.20.3 # via @@ -50,11 +51,11 @@ httpcore==1.0.9 # via # -c requirements/requirements.txt # httpx -httpx==0.27.0 +httpx==0.28.1 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -httpx-sse==0.4.0 +httpx-sse==0.4.3 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) @@ -64,35 +65,35 @@ huggingface-hub==0.36.0 # peft # tokenizers # transformers -idna==3.7 +id==1.5.0 + # via + # -c requirements/requirements.txt + # twine +idna==3.11 # via # -c requirements/requirements.txt # anyio # httpx # requests -importlib-metadata==8.0.0 - # via - # -c requirements/requirements.txt - # twine jaraco-classes==3.4.0 # via # -c requirements/requirements.txt # keyring -jaraco-context==5.3.0 +jaraco-context==6.1.0 # via # -c requirements/requirements.txt # keyring -jaraco-functools==4.0.1 +jaraco-functools==4.4.0 # via # -c requirements/requirements.txt # keyring jinja2==3.1.6 # via torch -keyring==25.2.1 +keyring==25.7.0 # via # -c requirements/requirements.txt # twine -markdown-it-py==3.0.0 +markdown-it-py==4.0.0 # via # -c requirements/requirements.txt # rich @@ -102,58 +103,56 @@ mdurl==0.1.2 # via # -c requirements/requirements.txt # markdown-it-py -more-itertools==10.3.0 +more-itertools==10.8.0 # via # -c requirements/requirements.txt # jaraco-classes # jaraco-functools mpmath==1.3.0 # via sympy -networkx==3.6.1 +networkx==3.4.2 # via torch -nh3==0.2.18 +nh3==0.3.2 # via # -c requirements/requirements.txt # readme-renderer -numpy==2.0.2 +numpy==2.2.6 # via # accelerate # peft # transformers packaging==26.0 # via + # -c requirements/requirements.txt # accelerate # huggingface-hub # peft # transformers + # twine peft==0.18.1 # via hirundo (pyproject.toml) -pkginfo==1.10.0 - # via - # -c requirements/requirements.txt - # twine psutil==7.2.1 # via # accelerate # peft -pydantic==2.8.2 +pydantic==2.12.5 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -pydantic-core==2.20.1 +pydantic-core==2.41.5 # via # -c requirements/requirements.txt # pydantic -pygments==2.18.0 +pygments==2.19.2 # via # -c requirements/requirements.txt # readme-renderer # rich -python-dotenv==1.0.1 +python-dotenv==1.2.1 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -pyyaml==6.0.1 +pyyaml==6.0.3 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) @@ -167,11 +166,12 @@ readme-renderer==44.0 # twine regex==2026.1.15 # via transformers -requests==2.32.4 +requests==2.32.5 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) # huggingface-hub + # id # requests-toolbelt # transformers # twine @@ -183,7 +183,7 @@ rfc3986==2.0.0 # via # -c requirements/requirements.txt # twine -rich==13.7.1 +rich==14.2.0 # via # -c requirements/requirements.txt # twine @@ -193,7 +193,7 @@ safetensors==0.7.0 # accelerate # peft # transformers -setuptools==80.9.0 +setuptools==80.10.1 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) @@ -202,18 +202,13 @@ shellingham==1.5.4 # via # -c requirements/requirements.txt # typer -sniffio==1.3.1 - # via - # -c requirements/requirements.txt - # anyio - # httpx -stamina==24.2.0 +stamina==25.2.0 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) sympy==1.14.0 # via torch -tenacity==8.5.0 +tenacity==9.1.2 # via # -c requirements/requirements.txt # stamina @@ -223,7 +218,7 @@ torch==2.10.0 # via # accelerate # peft -tqdm==4.66.5 +tqdm==4.67.1 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) @@ -234,23 +229,23 @@ transformers==4.57.6 # via # hirundo (pyproject.toml) # peft -twine==5.1.1 +twine==6.2.0 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -typer==0.12.3 +typer==0.21.1 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -types-pyyaml==6.0.12.20240311 +types-pyyaml==6.0.12.20250915 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -types-requests==2.32.0.20240712 +types-requests==2.32.4.20260107 # via # -c requirements/requirements.txt # hirundo (pyproject.toml) -typing-extensions==4.12.2 +typing-extensions==4.15.0 # via # -c requirements/requirements.txt # huggingface-hub @@ -258,6 +253,11 @@ typing-extensions==4.12.2 # pydantic-core # torch # typer + # typing-inspection +typing-inspection==0.4.2 + # via + # -c requirements/requirements.txt + # pydantic urllib3==2.6.3 # via # -c requirements/requirements.txt @@ -265,7 +265,3 @@ urllib3==2.6.3 # requests # twine # types-requests -zipp==3.19.2 - # via - # -c requirements/requirements.txt - # importlib-metadata diff --git a/uv.lock b/uv.lock index 682425e3..82686b7a 100644 --- a/uv.lock +++ b/uv.lock @@ -549,6 +549,7 @@ name = "hirundo" version = "0.1.21" source = { editable = "." } dependencies = [ + { name = "docutils" }, { name = "h11" }, { name = "httpx" }, { name = "httpx-sse" }, @@ -626,6 +627,7 @@ requires-dist = [ { name = "basedpyright", marker = "extra == 'dev'", specifier = "==1.37.1" }, { name = "bumpver", marker = "extra == 'dev'" }, { name = "cryptography", marker = "extra == 'dev'", specifier = ">=44.0.1" }, + { name = "docutils", specifier = "<0.22.0" }, { name = "esbonio", marker = "extra == 'docs'" }, { name = "filelock", marker = "extra == 'dev'", specifier = ">=3.20.1" }, { name = "furo", marker = "extra == 'docs'" }, From e10613e44ef3f416fd561b5cd11fdf01ae422e75 Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Thu, 22 Jan 2026 18:44:59 +0200 Subject: [PATCH 15/25] Fix retries when HTTP SSE request fails --- hirundo/_run_checking.py | 168 +++++++++++++++++++-------------------- 1 file changed, 82 insertions(+), 86 deletions(-) diff --git a/hirundo/_run_checking.py b/hirundo/_run_checking.py index a0245039..4866454b 100644 --- a/hirundo/_run_checking.py +++ b/hirundo/_run_checking.py @@ -80,6 +80,30 @@ def get_state(payload: dict, status_keys: tuple[str, ...]) -> str | None: return None +def _extract_event_data(event: dict, error_cls: type[Exception]) -> dict: + if "data" in event: + return event["data"] + if "detail" in event: + raise error_cls(event["detail"]) + if "reason" in event: + raise error_cls(event["reason"]) + raise error_cls("Unknown error") + + +def _should_retry_after_stream( + last_event: dict | None, + status_keys: tuple[str, ...], + pending_state_value: str, +) -> bool: + if not last_event: + return True + data = last_event.get("data") + if data is None: + return False + last_state = get_state(data, status_keys) + return last_state == pending_state_value + + def iter_run_events( url: str, *, @@ -107,51 +131,35 @@ def iter_run_events( Yields: Event payloads decoded from the SSE data field. """ - if retry > max_retries: - raise error_cls("Max retries reached") - last_event = None - with httpx.Client(timeout=httpx.Timeout(None, connect=5.0)) as client: - for sse in iter_sse_retrying( - client, - "GET", - url, - headers=headers, - ): - if sse.event == "ping": - continue - log.debug( - "[SYNC] received event: %s with data: %s and ID: %s and retry: %s", - sse.event, - sse.data, - sse.id, - sse.retry, - ) - last_event = json.loads(sse.data) - if not last_event: - continue - if "data" in last_event: - data = last_event["data"] - else: - if "detail" in last_event: - raise error_cls(last_event["detail"]) - if "reason" in last_event: - raise error_cls(last_event["reason"]) - raise error_cls("Unknown error") - yield data - last_state = None - if last_event and "data" in last_event: - last_state = get_state(last_event["data"], status_keys) - if not last_event or last_state == pending_state_value: - iter_run_events( - url, - headers=headers, - retry=retry + 1, - max_retries=max_retries, - pending_state_value=pending_state_value, - status_keys=status_keys, - error_cls=error_cls, - log=log, - ) + while True: + if retry > max_retries: + raise error_cls("Max retries reached") + last_event = None + with httpx.Client(timeout=httpx.Timeout(None, connect=5.0)) as client: + for sse in iter_sse_retrying( + client, + "GET", + url, + headers=headers, + ): + if sse.event == "ping": + continue + log.debug( + "[SYNC] received event: %s with data: %s and ID: %s and retry: %s", + sse.event, + sse.data, + sse.id, + sse.retry, + ) + last_event = json.loads(sse.data) + if not last_event: + continue + data = _extract_event_data(last_event, error_cls) + yield data + if _should_retry_after_stream(last_event, status_keys, pending_state_value): + retry += 1 + continue + return async def aiter_run_events( @@ -181,48 +189,36 @@ async def aiter_run_events( Yields: Event payloads decoded from the SSE data field. """ - if retry > max_retries: - raise error_cls("Max retries reached") - last_event = None - async with httpx.AsyncClient(timeout=httpx.Timeout(None, connect=5.0)) as client: - async_iterator = await aiter_sse_retrying( - client, - "GET", - url, - headers=headers or {}, - ) - async for sse in async_iterator: - if sse.event == "ping": - continue - log.debug( - "[ASYNC] Received event: %s with data: %s and ID: %s and retry: %s", - sse.event, - sse.data, - sse.id, - sse.retry, + while True: + if retry > max_retries: + raise error_cls("Max retries reached") + last_event = None + async with httpx.AsyncClient( + timeout=httpx.Timeout(None, connect=5.0) + ) as client: + async_iterator = await aiter_sse_retrying( + client, + "GET", + url, + headers=headers or {}, ) - last_event = json.loads(sse.data) - if "data" not in last_event: - if "detail" in last_event: - raise error_cls(last_event["detail"]) - if "reason" in last_event: - raise error_cls(last_event["reason"]) - raise error_cls("Unknown error") - yield last_event["data"] - last_state = None - if last_event and "data" in last_event: - last_state = get_state(last_event["data"], status_keys) - if not last_event or last_state == pending_state_value: - aiter_run_events( - url, - headers=headers, - retry=retry + 1, - max_retries=max_retries, - pending_state_value=pending_state_value, - status_keys=status_keys, - error_cls=error_cls, - log=log, - ) + async for sse in async_iterator: + if sse.event == "ping": + continue + log.debug( + "[ASYNC] Received event: %s with data: %s and ID: %s and retry: %s", + sse.event, + sse.data, + sse.id, + sse.retry, + ) + last_event = json.loads(sse.data) + data = _extract_event_data(last_event, error_cls) + yield data + if _should_retry_after_stream(last_event, status_keys, pending_state_value): + retry += 1 + continue + return def update_progress_from_result( From 898c7f8eea4696fedcb1292217ac9ae8b554efc7 Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Thu, 22 Jan 2026 22:09:11 +0200 Subject: [PATCH 16/25] Fix Misha's PR comment --- scripts/cleanup_test_artifacts.py | 109 +++++++++++++++--------------- 1 file changed, 56 insertions(+), 53 deletions(-) diff --git a/scripts/cleanup_test_artifacts.py b/scripts/cleanup_test_artifacts.py index 5699bde9..1307075e 100644 --- a/scripts/cleanup_test_artifacts.py +++ b/scripts/cleanup_test_artifacts.py @@ -178,56 +178,7 @@ def _cleanup_storage_configs(one_week_ago: datetime.datetime) -> None: ) -def _handle_llm_cleanup(one_week_ago: datetime.datetime): - archived_runs = set[str]() - trying_to_delete_llms = set[int]() - deleted_llms = set[int]() - llms = LlmModel.list() - llm_dict: dict[int, LlmModelOut] = {llm.id: llm for llm in llms} - all_live_llm_unlearning_runs = LlmUnlearningRun.list() - all_archived_llm_unlearning_runs = LlmUnlearningRun.list(archived=True) - runs_by_llm = _collect_runs_by_llm( - llms, - all_live_llm_unlearning_runs, - all_archived_llm_unlearning_runs, - ) - - for llm_id, llm_runs in runs_by_llm.items(): - llm = llm_dict.get(llm_id) - - if llm and _should_delete_resource(llm.model_name, llm_runs, one_week_ago): - trying_to_delete_llms.add(llm_id) - for run in llm_runs: - if run.deleted_at is not None: - continue - try: - LlmUnlearningRun.archive(run.run_id) - archived_runs.add(run.run_id) - except (HirundoError, requests.HTTPError) as exc: - logger.warning( - "Failed to archive run with ID %s: %s", run.run_id, exc - ) - deleted_llms = _delete_llm( - llm_id, - deleted_llms, - ) - - logger.info( - "Deleted %s (%s) LLMs and archived %s (%s) runs", - deleted_llms, - len(deleted_llms), - archived_runs, - len(archived_runs), - ) - if trying_to_delete_llms != deleted_llms: - logger.warning( - "Tried to delete %s LLMs, but only deleted %s LLMs", - trying_to_delete_llms, - deleted_llms, - ) - - -def main() -> None: +def _handle_datasets_cleanup(one_week_ago: datetime.datetime): all_live_data_qa_runs = QADataset.list_runs() all_archived_data_qa_runs = QADataset.list_runs(archived=True) datasets = { @@ -235,9 +186,6 @@ def main() -> None: for dataset_entry in QADataset.list_datasets() if dataset_entry.id is not None } - now = datetime.datetime.now(timezone.utc) - one_week_ago = now - timedelta(days=7) - runs_by_dataset = _collect_runs_by_dataset( datasets, all_live_data_qa_runs, all_archived_data_qa_runs ) @@ -292,6 +240,61 @@ def main() -> None: deleted_datasets, ) + +def _handle_llm_cleanup(one_week_ago: datetime.datetime): + archived_runs = set[str]() + trying_to_delete_llms = set[int]() + deleted_llms = set[int]() + llms = LlmModel.list() + llm_dict: dict[int, LlmModelOut] = {llm.id: llm for llm in llms} + all_live_llm_unlearning_runs = LlmUnlearningRun.list() + all_archived_llm_unlearning_runs = LlmUnlearningRun.list(archived=True) + runs_by_llm = _collect_runs_by_llm( + llms, + all_live_llm_unlearning_runs, + all_archived_llm_unlearning_runs, + ) + + for llm_id, llm_runs in runs_by_llm.items(): + llm = llm_dict.get(llm_id) + + if llm and _should_delete_resource(llm.model_name, llm_runs, one_week_ago): + trying_to_delete_llms.add(llm_id) + for run in llm_runs: + if run.deleted_at is not None: + continue + try: + LlmUnlearningRun.archive(run.run_id) + archived_runs.add(run.run_id) + except (HirundoError, requests.HTTPError) as exc: + logger.warning( + "Failed to archive run with ID %s: %s", run.run_id, exc + ) + deleted_llms = _delete_llm( + llm_id, + deleted_llms, + ) + + logger.info( + "Deleted %s (%s) LLMs and archived %s (%s) runs", + deleted_llms, + len(deleted_llms), + archived_runs, + len(archived_runs), + ) + if trying_to_delete_llms != deleted_llms: + logger.warning( + "Tried to delete %s LLMs, but only deleted %s LLMs", + trying_to_delete_llms, + deleted_llms, + ) + + +def main() -> None: + now = datetime.datetime.now(timezone.utc) + one_week_ago = now - timedelta(days=7) + + _handle_datasets_cleanup(one_week_ago) _handle_llm_cleanup(one_week_ago) _cleanup_storage_configs(one_week_ago) From 9b42aa56baaf297ce90aa80d141bc53293bce9b8 Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Thu, 22 Jan 2026 22:24:12 +0200 Subject: [PATCH 17/25] Fix ChatGPT PR review comments --- hirundo/dataset_qa.py | 2 ++ hirundo/unlearning_llm.py | 9 +++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/hirundo/dataset_qa.py b/hirundo/dataset_qa.py index ced45cb5..1deacb9e 100644 --- a/hirundo/dataset_qa.py +++ b/hirundo/dataset_qa.py @@ -688,6 +688,8 @@ async def acheck_run(self) -> AsyncGenerator[dict, None]: This generator will produce values to show progress of the run. + Note: This function does not handle errors nor show progress. It is expected that you do that. + Yields: Each event will be a dict, where: - `"state"` is PENDING, STARTED, RETRY, FAILURE or SUCCESS diff --git a/hirundo/unlearning_llm.py b/hirundo/unlearning_llm.py index 47a38a2c..78de9cb0 100644 --- a/hirundo/unlearning_llm.py +++ b/hirundo/unlearning_llm.py @@ -324,7 +324,6 @@ class LlmRunInfo(BaseModel): organization_id: int | None = None name: str | None = None - model_id: int | None = None target_behaviors: list[TargetBehavior] target_utilities: list[TargetUtility] advanced_options: UnlearningLlmAdvancedOptions | None = None @@ -400,11 +399,7 @@ def launch(model_id: int, run_info: LlmRunInfo | BiasRunInfo) -> str: run_response_json = run_response.json() if run_response.content else {} if isinstance(run_response_json, str): return run_response_json - run_id = ( - run_response_json.get("run_id") - or run_response_json.get("hir_run_id") - or run_response_json.get("id") - ) + run_id = run_response_json.get("run_id") if not run_id: raise ValueError("No run ID returned from launch request") return run_id @@ -575,6 +570,8 @@ async def acheck_run_by_id(run_id: str, retry=0) -> AsyncGenerator[dict, None]: This generator will produce values to show progress of the run. + Note: This function does not handle errors nor show progress. It is expected that you do that. + Args: run_id: The `run_id` produced by a `launch` call retry: A number used to track the number of retries to limit re-checks. *Do not* provide this value manually. From 785155b7a19ecdcb0133a4633b9f4ad8c46f00d1 Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Thu, 22 Jan 2026 23:23:03 +0200 Subject: [PATCH 18/25] Fix Gemini's PR comment --- hirundo/unlearning_llm.py | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/hirundo/unlearning_llm.py b/hirundo/unlearning_llm.py index 78de9cb0..75922c70 100644 --- a/hirundo/unlearning_llm.py +++ b/hirundo/unlearning_llm.py @@ -108,7 +108,7 @@ def get_by_id(llm_model_id: int) -> "LlmModelOut": timeout=READ_TIMEOUT, ) raise_for_status_with_reason(llm_model_response) - return LlmModelOut.from_response(llm_model_response.json()) + return LlmModelOut.model_validate(llm_model_response.json()) @staticmethod def get_by_name(llm_model_name: str) -> "LlmModelOut": @@ -118,7 +118,7 @@ def get_by_name(llm_model_name: str) -> "LlmModelOut": timeout=READ_TIMEOUT, ) raise_for_status_with_reason(llm_model_response) - return LlmModelOut.from_response(llm_model_response.json()) + return LlmModelOut.model_validate(llm_model_response.json()) @staticmethod def list(organization_id: int | None = None) -> list["LlmModelOut"]: @@ -133,7 +133,7 @@ def list(organization_id: int | None = None) -> list["LlmModelOut"]: ) raise_for_status_with_reason(llm_model_response) llm_model_json = llm_model_response.json() - return [LlmModelOut.from_response(llm_model) for llm_model in llm_model_json] + return [LlmModelOut.model_validate(llm_model) for llm_model in llm_model_json] @staticmethod def delete_by_id(llm_model_id: int) -> None: @@ -205,19 +205,6 @@ class LlmModelOut(BaseModel): model_name: str model_source: LlmSourcesOutput - @staticmethod - def from_response(response_payload: dict[str, typing.Any]) -> "LlmModelOut": - return LlmModelOut( - id=response_payload["id"], - organization_id=response_payload["organization_id"], - creator_id=response_payload["creator_id"], - creator_name=response_payload["creator_name"], - created_at=response_payload["created_at"], - updated_at=response_payload["updated_at"], - model_name=response_payload["model_name"], - model_source=response_payload["model_source"], - ) - def get_pipeline_for_run( self, run_id: str, From d02a588b261d7c2db31605aac6dd444ed4e25d81 Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Thu, 22 Jan 2026 23:25:04 +0200 Subject: [PATCH 19/25] Fix bug in `deleted_at` field for `OutputUnlearningLlmRun` Found while testing `.list()` --- hirundo/unlearning_llm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hirundo/unlearning_llm.py b/hirundo/unlearning_llm.py index 75922c70..731fcc0d 100644 --- a/hirundo/unlearning_llm.py +++ b/hirundo/unlearning_llm.py @@ -364,7 +364,7 @@ class OutputUnlearningLlmRun(BaseModel): optimization_progress: float post_process_progress: float - deleted_at: datetime.datetime | None + deleted_at: datetime.datetime | None = None STATUS_TO_TEXT_MAP = build_status_text_map("LLM unlearning") From 73d85974d1d23c498da7150ecd63d03369836b2f Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Thu, 22 Jan 2026 23:52:32 +0200 Subject: [PATCH 20/25] Rename `get_pipeline_for_run` to `get_hf_pipeline_for_run` --- README.md | 2 +- hirundo/_llm_pipeline.py | 2 +- hirundo/unlearning_llm.py | 10 +++++----- tests/unlearning-llm/unlearn_llm_behavior_test.py | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ea23c2fe..7cc31382 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ run_id = LlmUnlearningRun.launch( llm_id, run_info, ) -new_adapter = llm.get_pipeline_for_run(run_id) +new_adapter = llm.get_hf_pipeline_for_run(run_id) ``` ### Dataset QA diff --git a/hirundo/_llm_pipeline.py b/hirundo/_llm_pipeline.py index 41b75825..a0ca999b 100644 --- a/hirundo/_llm_pipeline.py +++ b/hirundo/_llm_pipeline.py @@ -24,7 +24,7 @@ REQUIRED_PACKAGES_FOR_PIPELINE = ["peft", "transformers", "accelerate"] -def get_pipeline_for_run_given_model( +def get_hf_pipeline_for_run_given_model( llm: "LlmModel | LlmModelOut", run_id: str, config: "PretrainedConfig | None" = None, diff --git a/hirundo/unlearning_llm.py b/hirundo/unlearning_llm.py index 731fcc0d..b36ea14f 100644 --- a/hirundo/unlearning_llm.py +++ b/hirundo/unlearning_llm.py @@ -11,7 +11,7 @@ from hirundo._env import API_HOST from hirundo._headers import get_headers from hirundo._http import raise_for_status_with_reason, requests -from hirundo._llm_pipeline import get_pipeline_for_run_given_model +from hirundo._llm_pipeline import get_hf_pipeline_for_run_given_model from hirundo._run_checking import ( STATUS_TO_PROGRESS_MAP, RunStatus, @@ -180,7 +180,7 @@ def update( if archive_existing_runs is not None: self.archive_existing_runs = archive_existing_runs - def get_pipeline_for_run( + def get_hf_pipeline_for_run( self, run_id: str, config: "PretrainedConfig | None" = None, @@ -188,7 +188,7 @@ def get_pipeline_for_run( device_map: str | dict[str, int | str] | None = None, trust_remote_code: bool = False, ) -> "Pipeline": - return get_pipeline_for_run_given_model( + return get_hf_pipeline_for_run_given_model( self, run_id, config, device, device_map, trust_remote_code ) @@ -205,7 +205,7 @@ class LlmModelOut(BaseModel): model_name: str model_source: LlmSourcesOutput - def get_pipeline_for_run( + def get_hf_pipeline_for_run( self, run_id: str, config: "PretrainedConfig | None" = None, @@ -214,7 +214,7 @@ def get_pipeline_for_run( trust_remote_code: bool = False, token: str | None = None, ) -> "Pipeline": - return get_pipeline_for_run_given_model( + return get_hf_pipeline_for_run_given_model( self, run_id, config, diff --git a/tests/unlearning-llm/unlearn_llm_behavior_test.py b/tests/unlearning-llm/unlearn_llm_behavior_test.py index caea3558..95f4a96a 100644 --- a/tests/unlearning-llm/unlearn_llm_behavior_test.py +++ b/tests/unlearning-llm/unlearn_llm_behavior_test.py @@ -30,5 +30,5 @@ def test_unlearn_llm_behavior(): llm_id, run_info, ) - new_adapter = llm.get_pipeline_for_run(run_id) + new_adapter = llm.get_hf_pipeline_for_run(run_id) assert isinstance(new_adapter, Pipeline) From 1bd8c923bd3520acae31bf221a3535afb6c190c4 Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Thu, 22 Jan 2026 23:54:46 +0200 Subject: [PATCH 21/25] Fix `MODEL_FOR_IMAGE_TEXT_TO_TEXT_MAPPING_NAMES` being used instead of `MODEL_FOR_VISION_2_SEQ_MAPPING_NAMES` --- hirundo/_llm_pipeline.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hirundo/_llm_pipeline.py b/hirundo/_llm_pipeline.py index a0ca999b..5224e732 100644 --- a/hirundo/_llm_pipeline.py +++ b/hirundo/_llm_pipeline.py @@ -41,7 +41,7 @@ def get_hf_pipeline_for_run_given_model( from peft import PeftModel from transformers.models.auto.configuration_auto import AutoConfig from transformers.models.auto.modeling_auto import ( - MODEL_FOR_VISION_2_SEQ_MAPPING_NAMES, + MODEL_FOR_IMAGE_TEXT_TO_TEXT_MAPPING_NAMES, AutoModelForCausalLM, AutoModelForImageTextToText, ) @@ -121,7 +121,8 @@ def get_hf_pipeline_for_run_given_model( ) config_dict = config.to_dict() if hasattr(config, "to_dict") else config is_multimodal = ( - config_dict.get("model_type") in MODEL_FOR_VISION_2_SEQ_MAPPING_NAMES.keys() + config_dict.get("model_type") + in MODEL_FOR_IMAGE_TEXT_TO_TEXT_MAPPING_NAMES.keys() ) if is_multimodal: base_model = AutoModelForImageTextToText.from_pretrained( From efa2e01af86aeb8fc71ac40f736aebf065a4f724 Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Fri, 23 Jan 2026 00:09:57 +0200 Subject: [PATCH 22/25] Fix pydantic error due to class not really being imported at runtime --- hirundo/labeling.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/hirundo/labeling.py b/hirundo/labeling.py index 6091f05d..ea4b9eeb 100644 --- a/hirundo/labeling.py +++ b/hirundo/labeling.py @@ -3,11 +3,9 @@ from pydantic import BaseModel, Field +from hirundo._urls import HirundoUrl from hirundo.dataset_enum import DatasetMetadataType -if typing.TYPE_CHECKING: - from hirundo._urls import HirundoUrl - class Metadata(BaseModel, ABC, frozen=True): type: DatasetMetadataType @@ -21,7 +19,7 @@ class HirundoCSV(Metadata, frozen=True): type: typing.Literal[DatasetMetadataType.HIRUNDO_CSV] = ( DatasetMetadataType.HIRUNDO_CSV ) - csv_url: "HirundoUrl" + csv_url: HirundoUrl """ The URL to access the dataset metadata CSV file. e.g. `s3://my-bucket-name/my-folder/my-metadata.csv`, `gs://my-bucket-name/my-folder/my-metadata.csv`, @@ -36,7 +34,7 @@ class COCO(Metadata, frozen=True): """ type: typing.Literal[DatasetMetadataType.COCO] = DatasetMetadataType.COCO - json_url: "HirundoUrl" + json_url: HirundoUrl """ The URL to access the dataset metadata JSON file. e.g. `s3://my-bucket-name/my-folder/my-metadata.json`, `gs://my-bucket-name/my-folder/my-metadata.json`, @@ -47,8 +45,8 @@ class COCO(Metadata, frozen=True): class YOLO(Metadata, frozen=True): type: typing.Literal[DatasetMetadataType.YOLO] = DatasetMetadataType.YOLO - data_yaml_url: "HirundoUrl | None" = None - labels_dir_url: "HirundoUrl" + data_yaml_url: HirundoUrl | None = None + labels_dir_url: HirundoUrl class HuggingFaceAudio(Metadata, frozen=True): @@ -73,7 +71,7 @@ class Keylabs(Metadata, frozen=True): Keylabs project ID. """ - labels_dir_url: "HirundoUrl" + labels_dir_url: HirundoUrl """ URL to the directory containing the Keylabs labels. """ From 47c7acf195092576c55ce235575acfeb27f09dae Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Fri, 23 Jan 2026 00:24:06 +0200 Subject: [PATCH 23/25] Fix `deleted_at` being missing crashing the tests --- hirundo/dataset_qa.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hirundo/dataset_qa.py b/hirundo/dataset_qa.py index 1deacb9e..44f0bca9 100644 --- a/hirundo/dataset_qa.py +++ b/hirundo/dataset_qa.py @@ -779,4 +779,4 @@ class DataQARunOut(BaseModel): created_at: datetime.datetime run_args: RunArgs | None - deleted_at: datetime.datetime | None + deleted_at: datetime.datetime | None = None From 3eabf6ce87a2f60bf4abb43b9fd8cf01adfb840b Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Fri, 23 Jan 2026 00:49:32 +0200 Subject: [PATCH 24/25] Switch to `API_HOST2` and `API_KEY2` for now until `test` contains LLM behavior unlearning Currently only `test2` does --- .github/workflows/pytest-sanity.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pytest-sanity.yaml b/.github/workflows/pytest-sanity.yaml index 03802fa4..7b771426 100644 --- a/.github/workflows/pytest-sanity.yaml +++ b/.github/workflows/pytest-sanity.yaml @@ -68,8 +68,8 @@ jobs: if: github.event_name != 'pull_request' && runner.os != 'Windows' && steps.changes.outputs.non_workflow == 'true' run: .venv/bin/pytest env: - API_HOST: ${{ secrets.API_HOST }} - API_KEY: ${{ secrets.API_KEY }} + API_HOST: ${{ secrets.API_HOST2 }} + API_KEY: ${{ secrets.API_KEY2 }} GCP_CREDENTIALS: ${{ secrets.GCP_CREDENTIALS }} AWS_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} @@ -80,8 +80,8 @@ jobs: if: github.event_name != 'pull_request' && runner.os == 'Windows' && steps.changes.outputs.non_workflow == 'true' run: .venv/Scripts/pytest env: - API_HOST: ${{ secrets.API_HOST }} - API_KEY: ${{ secrets.API_KEY }} + API_HOST: ${{ secrets.API_HOST2 }} + API_KEY: ${{ secrets.API_KEY2 }} GCP_CREDENTIALS: ${{ secrets.GCP_CREDENTIALS }} AWS_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} From 60ebe02acbd67df9bafce375070574c502f3b4bf Mon Sep 17 00:00:00 2001 From: Ben Lewis Date: Sun, 25 Jan 2026 23:09:12 +0200 Subject: [PATCH 25/25] Fix unlearning LLM behavior test running on every platform --- tests/unlearning-llm/unlearn_llm_behavior_test.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/unlearning-llm/unlearn_llm_behavior_test.py b/tests/unlearning-llm/unlearn_llm_behavior_test.py index 95f4a96a..13144936 100644 --- a/tests/unlearning-llm/unlearn_llm_behavior_test.py +++ b/tests/unlearning-llm/unlearn_llm_behavior_test.py @@ -1,4 +1,5 @@ import logging +import os from hirundo import ( BiasRunInfo, @@ -26,9 +27,11 @@ def test_unlearn_llm_behavior(): run_info = BiasRunInfo( bias_type=BiasType.ALL, ) - run_id = LlmUnlearningRun.launch( - llm_id, - run_info, - ) - new_adapter = llm.get_hf_pipeline_for_run(run_id) - assert isinstance(new_adapter, Pipeline) + assert llm_id is not None + if os.getenv("FULL_TEST", "false") == "true": + run_id = LlmUnlearningRun.launch( + llm_id, + run_info, + ) + new_adapter = llm.get_hf_pipeline_for_run(run_id) + assert isinstance(new_adapter, Pipeline)